本文中调试的go源码为1.14.12版本,本文介绍的调试方法与go版本没有关系
我们在go的学习过程中,有可能会需要对go的源码进行调试
但是我们直接跑程序的话,是没法实现源码调试的
所以这里来介绍一下go源码的调试方法
使用goland进行调试,能够有比较清楚的图形化界面,这有助于我们在调试过程中对一些相关参数的查看,也能让调试变简单,所以我们使用goland进行调试
编写你的程序
想要进行源码调试首先肯定得有你自己的代码,你自己的代码在运行的过程中会调用到你要调试的那部分源码
我这里用判断结构体是否实现了某interface做例子
package main
import (
"fmt"
)
type Duck interface {
Quack()
}
type Cat struct{}
func (c Cat) Quacks() {
fmt.Println("meow")
}
func main() {
//./main.go:19:6: cannot use Cat literal (type Cat) as type Duck in assignment: Cat does not implement Duck (missing Quack method)
var d Duck = Cat{}
println(d)
}
因为结构体Cat没有实现Quack方法,所以这段代码在19行是执行不下去的,会报错【报错的内容就是18行】,也就是会被检查出来结构体Cat没有实现接口Duck
用goland打开go源码
然后我们用goland打开go源码,也就是goroot下的src文件夹,这里需要注意一下就是,goland调用的是windows版的go,我们想要调试的话实际就是让goland去调用go源码然后一步步执行,所以这里要打开的是windows的go,当然你要是苹果电脑你就开苹果版goland调的go就是了,一个道理,总之我们就是用goland去跑的。
打断点并设置goland参数
我们先在go源码中找到你要调试的部分,这个找法多种多样,比如我上面那样故意写了一个错误的代码对不对,我们可以直接在src下搜索这段报错,看看这个报错是来自哪里的,也可以根据你丰富的知识,判断你要调试的这个行为是发生在go源码的哪个包中,然后直接去找那个包的main.go
因为go判断结构体是否实现某interface是在抽象语法树生成之后的编译阶段,且刚才那段报错经过搜索发现其存在于go源码的src\cmd\compile\internal\gc\subr.go:610
,固我们可以猜测调试的入口应当为src\cmd\compile\main.go
中的main方法,如下图
通过阅读这段代码,我们很明显可以看出来,真正核心的逻辑肯定是在52行的gc.Main(archInit)
方法中,固我们将52行定为调试的起点。
我们在这里打一个断点。
goland的打断点大家应该都会我就不多解释了,点击行数52右边的空白部分即可。
打完断点效果如下
此时我们需要设置一下goland的参数
我们右键点击截图中main函数左边的绿色开始箭头,会出现三个选项,分别是【运行】【调试】和【修改运行配置】,选择【修改运行配置】,会出现一个弹窗如下图所示
就是配置里的参数我们需要进行调整
第一项运行种类要选【文件】
第三项输出目录写你刚才自己写的哪个代码的路径,写到文件夹,也可以点击输入框后面的文件图标直接打开文件选择框去找,我的路径是$GOPATH\src\test
第五项工作目录同上,我填的也是$GOPATH\src\test
第八项程序参数,需要填写你刚才写的那段代码的go文件的路径,我写的是$GOPATH\src\test\main.go
另外第八项上面那个【使用所有自定义构建标记】要勾起来
调整好以后如下图
然后点击【应用】或者【确定】进行保存
开始调试
现在我们就已经可以进行源码调试了 我们右键点击main.go左边的绿色箭头【对,还是刚才那个绿色箭头】,这次我们选择调试
待goland运行一小段时间后,我们就会进入调试状态
可以看到底部出现了调试面板,调试面板最左侧和顶部有一些按钮,面板左侧是调用栈,面板右侧是变量
我们先简单讲一下顶部的五个按钮吧,他们会比较常用
第一个是【显示执行点】,就是在调试的过程中会有光标指向当前在执行的逻辑【我觉得这个开不开无所谓】
第二个叫【步过】,其实就是执行当前行的逻辑,不去探究当前行调用的函数内部的逻辑,对于那些我们不关注的函数就要使用步过
第三个叫【步进】,也就是按步执行,如果执行的是一个函数则会跳转到函数中,注意使用步进的话所有函数都会进,这使得你可以一路走到汇编代码的地方
第四个叫【步出】,也就是本函数后续逻辑自动执行,我们回到本函数的调用处的下个逻辑
第五个是【执行到光标处】,就字面意思,我们在调试过程中可以直接用鼠标点击我们关注的行,然后用这个按钮直接略过中间的逻辑,执行到我们刚才设置了光标的行
比如我们在52行打了断点并且我们执行了调试,此时程序执行到52行就会停下,我们可以点击【步进】进入这个main函数,这样我们就来到了src\cmd\compile\internal\gc\main.go:144
进入以后我们可以一直使用【步过】跳转到我们想看的位置,也可以鼠标点击一下我们想看的行然后使用【执行到光标处】,这里我关注的行是594行,于是我在594行点击一下,然后使用【执行到光标处】直接跳过144行到594行中间的其他逻辑,效果如下图
这时使用【步进】进入typecheckslice函数
……
如果你有跟着操作,就会发现,此时点击【步进】不是进入typecheckslice
函数而是进入了Slice
函数,当然啦,我们调用函数前需要先搞清楚传入的参数到底是啥,这没问题。此时我们可以【步过】【步进】快速的走完Slice
函数,也可以直接使用【步出】离开Slice
函数,都是一样的,最后都会回到上图处,依然是594行,这时我们再点击一次【步进】,就可以进入typecheckslice
函数了
连续点击【步进】,我们就会进入到typecheckslice
中的typecheck
函数,稍微看一下这个函数就会发现他实际的核心逻辑都在300行调用的typecheck1
函数中,所以我们直接在300行点击一下,然后使用【执行到光标处】直接执行到300行
此时点击【步进】进入typecheck1函数
可以看到我们此时就来到了327行,typecheck1
函数,这是一个大几百行的巨型函数,我们先停一停
大家注意,在变量框中出现了n、top、res三个变量,他们分别是typecheck1
函数的两个参数和一个返回值
top就是一个值为1的int没什么好说的,res现在必然是个nil我们也不管他,我们看这个n
毕竟,既然函数名都叫typecheck了,这个函数必然是用来做类型检查的,那top是一个int,所以这个检查的对象肯定就是第一个参数n,n是Node的指针类型的,这个Node结构体我们进去看一下就能知道,这是go的抽象语法树结点的结构体,所以这个typecheck函数就是用来对参数n做类型检查的
在变量框中我们右键n选择检查
就会打开变量详情弹框
在这个弹框中,我们可以很容易的对n这个对象里面各属性的值进行确认
我们其实可以通过这个n具体的值来判断此次对这个函数的调用是不是我们想要的那一次,因为在逻辑执行的过程中,同一个函数可能使用不同的参数调用许多次,而只有其中一次是我们需要的,我们检查参数发现此次调用不是我们关注的之后,可以点击【步退】直接离开此次调用
下述详细流程类似于开荒,实际调试代码,如果你能够通过参数中的某个属性判断
在调试的过程中,任何时候我们都可以这么做
好的我们继续调试
观察typecheck1
函数,发现函数主要逻辑都在352行开始的switch中,所以我们将光标移到352行并点击【执行到光标处】,直接执行到352行,然后我们点击【步进】会去到549行,再次点击【步进】会渠道1227行,这时再点击【步进】就会开始执行1228行,也就是1227行的case下的逻辑,所以看起来549行的case,虽然我们【步进】的时候会走到那里,但是似乎并没有进入其中的逻辑里,这个我也不是很清楚
1227行是ONCALL,并不是我想看的,说明这个n并不是我关注的目标,我这里可以使用【步出】直接跳过本函数剩余的逻辑。 如果你是自己在调试,哪里要跳过哪里要一步步跟着看你要自己做判断,最好是先把相关源码看一下
连续点击两次【步出】之后会回到typecheckslice
方法进行下一次循环
然而这里没有下一次循环,接着【步进】就会发现我们退出了寻穿最终回到了gc\main.go的循环中
但是这个循环是有下一次的,我们【步过】和【步进】并用,可以再次进入typecheckslice
方法,并用和之前一样的流程再次来到typecheck1
方法中的switch
不过这次进的case是OCOPY,也不是我们想要的,【步退】出来,退到typecheckslice
方法进行下一次循环
通过【步进】我们可以在第二次循环中再次进入typecheck方法,并通过与上述相同的流程来到typecheck1
方法中的switch
这一次,我们会进入一个叫OAS的case
我们使用【步进】进入typecheckas方法
使用【执行到光标处】直接执行到3181行,使用【步进】进入assignconv
函数,再次点击【步进】进入assignconvfn
函数
点击838行并使用【执行到光标处】直接执行到838行,使用【步进】进入assignop
函数
这个函数就是最开始我们搜索到的,生成报错的函数
注意583行的注释:dst是一种interface,src实现了dst
这说明我们想看的,判断结构体是否实现了interface的逻辑就在这里
哪个IsInterrface进去看一眼就能知道是用来确定src是不是interface的
所以我们关心的逻辑就在587行的implements
函数中
点击587行并使用【执行到光标处】直接执行到587行,使用【步进】进入implements
函数
这个时候其实可以看一眼变量,第一个参数,t就是src,根据注释,我们可以猜到,src应该是一个结构体;第二个参数,iface就是dst,前面的注释里也说的很清楚,dst是一个interface
我们来检查一下这两个参数
右键变量列表中的t,选择 检查 ,出现检查弹框,查看Sym属性,可以看到Name=Cat
也就是说这个t参数,其实就是我们在自己程序中定义的Cat结构体
我们再用同样的方法看一下iface
同样是查看iface下的Sym属性,可见Name=Duck
由此可确认,iface就是我们自己程序中定义的Duck接口
那就,逻辑接着往下走呗
1660行的逻辑是t是interface时才会进入的,我们不管他,接着向下就来到了这里
我们先看1692行
iface是我们定义的Duck接口,Fields方法会返回调用者的字段/方法,如果调用者是结构体则返回字段,如果调用者是interface则返回方法,显然此时他会返回我们定义的Duck接口的方法,也就是Quack方法;Slice方法会将Fields方法的返回值处理成切片格式 所以1692行,就是在遍历这个切片【当然我们知道这个切片的长度只有1
1693行不知道在检查什么,无所谓。我们看1696行
当i小于tms的长度……等下,tms是什么玩意?
我们回头看看,1686行到1689行,会看到tms = t.AllMethods().Slice()
,这说明tms是t的全部函数的切片,我们之前说过了,t就是我们定义的Cat结构体,他有一个Quacks方法;所以这里我们可以知道,tms就是一个函数切片,长度为1,里面的内容是Quacks函数
好的回到1696行,i在for开始之前定义,初始值为0,固此时i为0,你不信的话也可以直接在编辑器下方的变量列表里面找i,看是不是0【截图中我编辑器里面逻辑是已经走到1699行了,所以显示i=1】
此时i < len(tms)
,我们看第二个条件,tms[i].Sym != im.Sym
前面已经说过了,tms[0]就是Cat结构体的Quacks方法,im就是Duck接口的Quack方法,这里显然是在对他们进行比较,那他俩一样吗?当然不一样啦Quacks函数名多了个s怎么会一样呢,所以我们就会进入这个for,让i++
此时我们也可以通过下方的变量框对tms和im进行检查,看我们的判断对不对
tms的第0个,Sym.Name
为Quacks
im的Sym.Name
为Quack
印证了我们上面对这两个变量的推断
我们同时也能够意识到就是在这里,src\cmd\compile\internal\gc\subr.go:1696
的这个tms[i].Sym != im.Sym
逻辑中,进行了【某结构体是否实现了某interface】的判断,当然啦这是在一个循环里面,如果我们的interface和结构体各有好多函数的话,他会循环遍历一个个去判断,但总之,判断,是这一行的这个逻辑在做判断
i++以后i就是1了,不再小于len(tms),固这个for只会循环1次,然后就会来到1699行,此时i等于1,len(tms)
也等于1,所以我们会进这个if,并最终,在1703行,return false
于是,我们又回到了subr的587行
继续使用【步进】
最后我们会来到610行,也就是最开始我们搜索到的报错的位置
就是在610行,构建了【此结构体未实现此interface】这样的报错
之后就是一些返回的逻辑,你有兴趣自己去看,我这里就不再往下讲了
经此次探索,我们学习了如何使用goland进行go源码调试,并成功找到了,go是在哪里判断结构体是否实现了某个interface