「Go 實戰(zhàn)營系列」源碼調(diào)試:Go是如何判斷實現(xiàn)了interface
本文來自于《Go 高級工程師實戰(zhàn)營》一期學(xué)員:鬼鸮
原文地址:https://blog.csdn.net/zxxshaycormac/article/details/117606285

本文中調(diào)試的go源碼為1.14.12版本,本文介紹的調(diào)試方法與go版本沒有關(guān)系
我們在go的學(xué)習(xí)過程中,有可能會需要對go的源碼進行調(diào)試;但是我們直接跑程序的話,是沒法實現(xiàn)源碼調(diào)試的;所以這里來介紹一下go源碼的調(diào)試方法。
使用goland進行調(diào)試,能夠有比較清楚的圖形化界面,這有助于我們在調(diào)試過程中對一些相關(guān)參數(shù)的查看,也能讓調(diào)試變簡單,所以我們使用goland進行調(diào)試。

編寫你的程序
想要進行源碼調(diào)試首先肯定得有你自己的代碼,你自己的代碼在運行的過程中會調(diào)用到你要調(diào)試的那部分源碼
我這里用判斷結(jié)構(gòu)體是否實現(xiàn)了某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)
}因為結(jié)構(gòu)體Cat沒有實現(xiàn)Quack方法,所以這段代碼在19行是執(zhí)行不下去的,會報錯【報錯的內(nèi)容就是18行】,也就是會被檢查出來結(jié)構(gòu)體Cat沒有實現(xiàn)接口Duck

用goland打開go源碼
然后我們用goland打開go源碼,也就是goroot下的src文件夾,這里需要注意一下就是,goland調(diào)用的是windows版的go,我們想要調(diào)試的話實際就是讓goland去調(diào)用go源碼然后一步步執(zhí)行,所以這里要打開的是windows的go,當然你要是蘋果電腦你就開蘋果版goland調(diào)的go就是了,一個道理,總之我們就是用goland去跑的。

打斷點并設(shè)置goland參數(shù)
我們先在go源碼中找到你要調(diào)試的部分,這個找法多種多樣,比如我上面那樣故意寫了一個錯誤的代碼對不對,我們可以直接在src下搜索這段報錯,看看這個報錯是來自哪里的,也可以根據(jù)你豐富的知識,判斷你要調(diào)試的這個行為是發(fā)生在go源碼的哪個包中,然后直接去找那個包的main.go
因為go判斷結(jié)構(gòu)體是否實現(xiàn)某interface是在抽象語法樹生成之后的編譯階段,且剛才那段報錯經(jīng)過搜索發(fā)現(xiàn)其存在于go源碼的src\cmd\compile\internal\gc\subr.go:610,固我們可以猜測調(diào)試的入口應(yīng)當為src\cmd\compile\main.go中的main方法,如下圖

通過閱讀這段代碼,我們很明顯可以看出來,真正核心的邏輯肯定是在52行的gc.Main(archInit)方法中,固我們將52行定為調(diào)試的起點。
我們在這里打一個斷點。
goland的打斷點大家應(yīng)該都會我就不多解釋了,點擊行數(shù)52右邊的空白部分即可。
打完斷點效果如下

此時我們需要設(shè)置一下goland的參數(shù)
我們右鍵點擊截圖中main函數(shù)左邊的綠色開始箭頭,會出現(xiàn)三個選項,分別是【運行】【調(diào)試】和【修改運行配置】,選擇【修改運行配置】,會出現(xiàn)一個彈窗如下圖所示

就是配置里的參數(shù)我們需要進行調(diào)整
第一項運行種類要選【文件】
第三項輸出目錄寫你剛才自己寫的哪個代碼的路徑,寫到文件夾,也可以點擊輸入框后面的文件圖標直接打開文件選擇框去找,我的路徑是$GOPATH\src\test
第五項工作目錄同上,我填的也是$GOPATH\src\test
第八項程序參數(shù),需要填寫你剛才寫的那段代碼的go文件的路徑,我寫的是$GOPATH\src\test\main.go
另外第八項上面那個【使用所有自定義構(gòu)建標記】要勾起來
調(diào)整好以后如下圖

然后點擊【應(yīng)用】或者【確定】進行保存

開始調(diào)試
現(xiàn)在我們就已經(jīng)可以進行源碼調(diào)試了
我們右鍵點擊main.go左邊的綠色箭頭【對,還是剛才那個綠色箭頭】,這次我們選擇調(diào)試

待goland運行一小段時間后,我們就會進入調(diào)試狀態(tài)

可以看到底部出現(xiàn)了調(diào)試面板,調(diào)試面板最左側(cè)和頂部有一些按鈕,面板左側(cè)是調(diào)用棧,面板右側(cè)是變量
我們先簡單講一下頂部的五個按鈕吧,他們會比較常用

第一個是【顯示執(zhí)行點】,就是在調(diào)試的過程中會有光標指向當前在執(zhí)行的邏輯【我覺得這個開不開無所謂】
第二個叫【步過】,其實就是執(zhí)行當前行的邏輯,不去探究當前行調(diào)用的函數(shù)內(nèi)部的邏輯,對于那些我們不關(guān)注的函數(shù)就要使用步過
第三個叫【步進】,也就是按步執(zhí)行,如果執(zhí)行的是一個函數(shù)則會跳轉(zhuǎn)到函數(shù)中,注意使用步進的話所有函數(shù)都會進,這使得你可以一路走到匯編代碼的地方
第四個叫【步出】,也就是本函數(shù)后續(xù)邏輯自動執(zhí)行,我們回到本函數(shù)的調(diào)用處的下個邏輯
第五個是【執(zhí)行到光標處】,就字面意思,我們在調(diào)試過程中可以直接用鼠標點擊我們關(guān)注的行,然后用這個按鈕直接略過中間的邏輯,執(zhí)行到我們剛才設(shè)置了光標的行
比如我們在52行打了斷點并且我們執(zhí)行了調(diào)試,此時程序執(zhí)行到52行就會停下,我們可以點擊【步進】進入這個main函數(shù),這樣我們就來到了src\cmd\compile\internal\gc\main.go:144
進入以后我們可以一直使用【步過】跳轉(zhuǎn)到我們想看的位置,也可以鼠標點擊一下我們想看的行然后使用【執(zhí)行到光標處】,這里我關(guān)注的行是594行,于是我在594行點擊一下,然后使用【執(zhí)行到光標處】直接跳過144行到594行中間的其他邏輯,效果如下圖

這時使用【步進】進入typecheckslice函數(shù)
……
如果你有跟著操作,就會發(fā)現(xiàn),此時點擊【步進】不是進入typecheckslice函數(shù)而是進入了Slice函數(shù),當然啦,我們調(diào)用函數(shù)前需要先搞清楚傳入的參數(shù)到底是啥,這沒問題。此時我們可以【步過】【步進】快速的走完Slice函數(shù),也可以直接使用【步出】離開Slice函數(shù),都是一樣的,最后都會回到上圖處,依然是594行,這時我們再點擊一次【步進】,就可以進入typecheckslice函數(shù)了


連續(xù)點擊【步進】,我們就會進入到typecheckslice中的typecheck函數(shù),稍微看一下這個函數(shù)就會發(fā)現(xiàn)他實際的核心邏輯都在300行調(diào)用的typecheck1函數(shù)中,所以我們直接在300行點擊一下,然后使用【執(zhí)行到光標處】直接執(zhí)行到300行

此時點擊【步進】進入typecheck1函數(shù)

可以看到我們此時就來到了327行,typecheck1函數(shù),這是一個大幾百行的巨型函數(shù),我們先停一停
大家注意,在變量框中出現(xiàn)了n、top、res三個變量,他們分別是typecheck1函數(shù)的兩個參數(shù)和一個返回值
top就是一個值為1的int沒什么好說的,res現(xiàn)在必然是個nil我們也不管他,我們看這個n
畢竟,既然函數(shù)名都叫typecheck了,這個函數(shù)必然是用來做類型檢查的,那top是一個int,所以這個檢查的對象肯定就是第一個參數(shù)n,n是Node的指針類型的,這個Node結(jié)構(gòu)體我們進去看一下就能知道,這是go的抽象語法樹結(jié)點的結(jié)構(gòu)體,所以這個typecheck函數(shù)就是用來對參數(shù)n做類型檢查的
在變量框中我們右鍵n選擇檢查

就會打開變量詳情彈框

在這個彈框中,我們可以很容易的對n這個對象里面各屬性的值進行確認
我們其實可以通過這個n具體的值來判斷此次對這個函數(shù)的調(diào)用是不是我們想要的那一次,因為在邏輯執(zhí)行的過程中,同一個函數(shù)可能使用不同的參數(shù)調(diào)用許多次,而只有其中一次是我們需要的,我們檢查參數(shù)發(fā)現(xiàn)此次調(diào)用不是我們關(guān)注的之后,可以點擊【步退】直接離開此次調(diào)用
下述詳細流程類似于開荒,實際調(diào)試代碼,如果你能夠通過參數(shù)中的某個屬性判斷
在調(diào)試的過程中,任何時候我們都可以這么做
好的我們繼續(xù)調(diào)試
觀察typecheck1函數(shù),發(fā)現(xiàn)函數(shù)主要邏輯都在352行開始的switch中,所以我們將光標移到352行并點擊【執(zhí)行到光標處】,直接執(zhí)行到352行,然后我們點擊【步進】會去到549行,再次點擊【步進】會渠道1227行,這時再點擊【步進】就會開始執(zhí)行1228行,也就是1227行的case下的邏輯,所以看起來549行的case,雖然我們【步進】的時候會走到那里,但是似乎并沒有進入其中的邏輯里,這個我也不是很清楚
1227行是ONCALL,并不是我想看的,說明這個n并不是我關(guān)注的目標,我這里可以使用【步出】直接跳過本函數(shù)剩余的邏輯。
如果你是自己在調(diào)試,哪里要跳過哪里要一步步跟著看你要自己做判斷,最好是先把相關(guān)源碼看一下
連續(xù)點擊兩次【步出】之后會回到typecheckslice方法進行下一次循環(huán)
然而這里沒有下一次循環(huán),接著【步進】就會發(fā)現(xiàn)我們退出了尋穿最終回到了gc\main.go的循環(huán)中

但是這個循環(huán)是有下一次的,我們【步過】和【步進】并用,可以再次進入typecheckslice方法,并用和之前一樣的流程再次來到typecheck1方法中的switch
不過這次進的case是OCOPY,也不是我們想要的,【步退】出來,退到typecheckslice方法進行下一次循環(huán)
通過【步進】我們可以在第二次循環(huán)中再次進入typecheck方法,并通過與上述相同的流程來到typecheck1方法中的switch
這一次,我們會進入一個叫OAS的case

我們使用【步進】進入typecheckas方法

使用【執(zhí)行到光標處】直接執(zhí)行到3181行,使用【步進】進入assignconv函數(shù),再次點擊【步進】進入assignconvfn函數(shù)

點擊838行并使用【執(zhí)行到光標處】直接執(zhí)行到838行,使用【步進】進入assignop函數(shù)

這個函數(shù)就是最開始我們搜索到的,生成報錯的函數(shù)
注意583行的注釋:dst是一種interface,src實現(xiàn)了dst
這說明我們想看的,判斷結(jié)構(gòu)體是否實現(xiàn)了interface的邏輯就在這里
哪個IsInterrface進去看一眼就能知道是用來確定src是不是interface的
所以我們關(guān)心的邏輯就在587行的implements函數(shù)中
點擊587行并使用【執(zhí)行到光標處】直接執(zhí)行到587行,使用【步進】進入implements函數(shù)

這個時候其實可以看一眼變量,第一個參數(shù),t就是src,根據(jù)注釋,我們可以猜到,src應(yīng)該是一個結(jié)構(gòu)體;第二個參數(shù),iface就是dst,前面的注釋里也說的很清楚,dst是一個interface
我們來檢查一下這兩個參數(shù)
右鍵變量列表中的t,選擇 檢查 ,出現(xiàn)檢查彈框,查看Sym屬性,可以看到Name=Cat

也就是說這個t參數(shù),其實就是我們在自己程序中定義的Cat結(jié)構(gòu)體
我們再用同樣的方法看一下iface

同樣是查看iface下的Sym屬性,可見Name=Duck
由此可確認,iface就是我們自己程序中定義的Duck接口
那就,邏輯接著往下走唄
1660行的邏輯是t是interface時才會進入的,我們不管他,接著向下就來到了這里

我們先看1692行
iface是我們定義的Duck接口,F(xiàn)ields方法會返回調(diào)用者的字段/方法,如果調(diào)用者是結(jié)構(gòu)體則返回字段,如果調(diào)用者是interface則返回方法,顯然此時他會返回我們定義的Duck接口的方法,也就是Quack方法;Slice方法會將Fields方法的返回值處理成切片格式
所以1692行,就是在遍歷這個切片【當然我們知道這個切片的長度只有1
1693行不知道在檢查什么,無所謂。我們看1696行
當i小于tms的長度……等下,tms是什么玩意?
我們回頭看看,1686行到1689行,會看到tms = t.AllMethods().Slice(),這說明tms是t的全部函數(shù)的切片,我們之前說過了,t就是我們定義的Cat結(jié)構(gòu)體,他有一個Quacks方法;所以這里我們可以知道,tms就是一個函數(shù)切片,長度為1,里面的內(nèi)容是Quacks函數(shù)
好的回到1696行,i在for開始之前定義,初始值為0,固此時i為0,你不信的話也可以直接在編輯器下方的變量列表里面找i,看是不是0【截圖中我編輯器里面邏輯是已經(jīng)走到1699行了,所以顯示i=1】
此時i < len(tms),我們看第二個條件,tms[i].Sym != im.Sym
前面已經(jīng)說過了,tms[0]就是Cat結(jié)構(gòu)體的Quacks方法,im就是Duck接口的Quack方法,這里顯然是在對他們進行比較,那他倆一樣嗎?當然不一樣啦Quacks函數(shù)名多了個s怎么會一樣呢,所以我們就會進入這個for,讓i++
此時我們也可以通過下方的變量框?qū)ms和im進行檢查,看我們的判斷對不對

tms的第0個,Sym.Name為Quacks

im的Sym.Name為Quack
印證了我們上面對這兩個變量的推斷
我們同時也能夠意識到就是在這里,src\cmd\compile\internal\gc\subr.go:1696的這個tms[i].Sym != im.Sym邏輯中,進行了【某結(jié)構(gòu)體是否實現(xiàn)了某interface】的判斷,當然啦這是在一個循環(huán)里面,如果我們的interface和結(jié)構(gòu)體各有好多函數(shù)的話,他會循環(huán)遍歷一個個去判斷,但總之,判斷,是這一行的這個邏輯在做判斷
i++以后i就是1了,不再小于len(tms),固這個for只會循環(huán)1次,然后就會來到1699行,此時i等于1,len(tms)也等于1,所以我們會進這個if,并最終,在1703行,return false
于是,我們又回到了subr的587行

繼續(xù)使用【步進】
最后我們會來到610行,也就是最開始我們搜索到的報錯的位置

就是在610行,構(gòu)建了【此結(jié)構(gòu)體未實現(xiàn)此interface】這樣的報錯
之后就是一些返回的邏輯,你有興趣自己去看,我這里就不再往下講了
經(jīng)此次探索,我們學(xué)習(xí)了如何使用goland進行g(shù)o源碼調(diào)試,并成功找到了,go是在哪里判斷結(jié)構(gòu)體是否實現(xiàn)了某個interface

想要和曹大深入交流的,趕緊掃描下方二維碼進群交流吧~
如果群人數(shù)已滿,可加小助理 Judy,拉你進群哦
