如何閱讀 Go 源碼?
前言
最近在看Go語言調度器相關的源碼,發(fā)現看源碼真是個技術活,所以本文就簡單總結一下該如何查看
Go源碼,希望對你們有幫助。
Go源碼包括哪些?
以我個人理解,Go源碼主要分為兩部分,一部分是官方提供的標準庫,一部分是Go語言的底層實現,Go語言的所有源碼/標準庫/編譯器都在src目錄下:https://github.com/golang/go/tree/master/src,想看什么庫的源碼任君選擇;
觀看Go標準庫 and Go底層實現的源代碼難易度也是不一樣的,我們一般也可以先從標準庫入手,挑選你感興趣的模塊,把它吃透,有了這個基礎后,我們在看Go語言底層實現的源代碼會稍微輕松一些;下面就針對我個人的一點學習心得分享一下如何查看Go源碼;
查看標準庫源代碼
標準庫的源代碼看起來稍容易些,因為標準庫也屬于上層應用,我們可以借助IDE的幫忙,其在IDE上就可以跳轉到源代碼包,我們只需要不斷來回跳轉查看各個函數實現做好筆記即可,因為一些源代碼設計的比較復雜,大家在看時最好通過畫圖輔助一下,個人覺得畫UML是最有助于理解的,能更清晰的理清各個實體的關系;
有些時候只看代碼是很難理解的,這時我們使用在線調試輔助我們理解,使用IDE提供的調試器或者GDB都可以達到目的,寫一個簡單的demo,斷點一打,單步調試走起來,比如你要查看fmt.Println的源代碼,開局一個小紅點,然后就是點點點;

查看Go語言底層實現
人都是會對未知領域充滿好奇,當使用一段時間Go語言后,就想更深入的搞明白一些事情,例如:Go程序的啟動過程是怎樣的,goroutine是怎么調度的,map是怎么實現的等等一些Go底層的實現,這種直接依靠IDE跳轉追溯代碼是辦不到的,這些都屬于Go語言的內部實現,大都在src目錄下的runtime包內實現,其實現了垃圾回收,并發(fā)控制, 棧管理以及其他一些 Go 語言的關鍵特性,在編譯Go代碼為機器代碼時也會將其也編譯進來,runtime就是Go程序執(zhí)行時候使用的庫,所以一些Go底層原理都在這個包內,我們需要借助一些方式才能查看到Go程序執(zhí)行時的代碼,這里分享兩種方式:分析匯編代碼、dlv調試;

分析匯編代碼
前面我們已經介紹了Go語言實現了runtime庫,我們想看到一些Go語言關鍵字特性對應runtime里的那個函數,可以查看匯編代碼,Go語言的匯編使用的plan9,與x86匯編差別還是很大,很多朋友都不熟悉plan9的匯編,但是要想看懂Go源碼還是要對plan9匯編有一個基本的了解的,這里推薦曹大的文章:plan9 assembly 完全解析,會一點匯編我們就可以看源代碼了,比如想在我們想看make是怎么初始化slice的,這時我們可以先寫一個簡單的demo:
//?main.go
import?"fmt"
func?main()?{
?s?:=?make([]int,?10,?20)
?fmt.Println(s)
}
有兩種方式可以查看匯編代碼:
1.?go?tool?compile?-S?-N?-l?main.go
2.?go?build?main.go?&&?go?tool?objdump?./main
方式一是將源代碼編譯成.o文件,并輸出匯編代碼,方式二是反匯編,這里推薦使用方式一,執(zhí)行方式一命令后,我們可以看到對應的匯編代碼如下:

s := make([]int, 10, 20)對應的源代碼就是 runtime.makeslice(SB),這時候我們就去runtime包下找makeslice函數,不斷追蹤下去就可查看源碼實現了,可在runtime/slice.go中找到:

在線調試
雖然上面的方法可以幫助我們定位到源代碼,但是后續(xù)的操作全靠review還是難于理解的,如果能在線調試跟蹤代碼可以更好助于我們理解,目前Go語言支持GDB、LLDB、Delve調試器,但只有Delve是專門為Go語言設計開發(fā)的調試工具,所以使用Delve可以輕松調試Go匯編程序,Delve的入門文章有很多,這篇就不在介紹Delve的詳細使用方法,入門大家可以看曹大的文章:https://chai2010.cn/advanced-go-programming-book/ch3-asm/ch3-09-debug.html,本文就使用一個小例子帶大家來看一看dlv如何調試Go源碼,大家都知道向一個nil的切片追加元素,不會有任何問題,在源碼中是怎么實現的呢?接下老我們使用dlv調試跟蹤一下,先寫一個小demo:
import?"fmt"
func?main()?{
?var?s?[]int
?s?=?append(s,?1)
?fmt.Println(s)
}
進入命令行包目錄,然后輸入dlv debug進入調試
$?dlv?debug
Type?'help'?for?list?of?commands.
(dlv)
因為這里我們想看到append的內部實現,所以在append那行加上斷點,執(zhí)行如下命令:
(dlv)?break?main.go:7
Breakpoint?1?set?at?0x10aba57?for?main.main()?./main.go:7
執(zhí)行continue命令,運行到斷點處:
(dlv)?continue
>?main.main()?./main.go:7?(hits?goroutine(1):1?total:1)?(PC:?0x10aba57)
?????2:?
?????3:?import?"fmt"
?????4:?
?????5:?func?main()?{
?????6:?????????var?s?[]int
=>???7:?????????s?=?append(s,?1)
?????8:?????????fmt.Println(s)
?????9:?}
接下來我們執(zhí)行disassemble反匯編命令查看main函數對應的匯編代碼:
(dlv)?disassemble
TEXT?main.main(SB)?/Users/go/src/asong.cloud/Golang_Dream/code_demo/src_code/main.go
????????main.go:5???????0x10aba20???????4c8d6424e8??????????????????????lea?r12,?ptr?[rsp-0x18]
????????main.go:5???????0x10aba25???????4d3b6610????????????????????????cmp?r12,?qword?ptr?[r14+0x10]
????????main.go:5???????0x10aba29???????0f86f6000000????????????????????jbe?0x10abb25
????????main.go:5???????0x10aba2f???????4881ec98000000??????????????????sub?rsp,?0x98
????????main.go:5???????0x10aba36???????4889ac2490000000????????????????mov?qword?ptr?[rsp+0x90],?rbp
????????main.go:5???????0x10aba3e???????488dac2490000000????????????????lea?rbp,?ptr?[rsp+0x90]
????????main.go:6???????0x10aba46???????48c744246000000000??????????????mov?qword?ptr?[rsp+0x60],?0x0
????????main.go:6???????0x10aba4f???????440f117c2468????????????????????movups?xmmword?ptr?[rsp+0x68],?xmm15
????????main.go:7???????0x10aba55???????eb00????????????????????????????jmp?0x10aba57
=>??????main.go:7???????0x10aba57*??????488d05a2740000??????????????????lea?rax,?ptr?[rip+0x74a2]
????????main.go:7???????0x10aba5e???????31db????????????????????????????xor?ebx,?ebx
????????main.go:7???????0x10aba60???????31c9????????????????????????????xor?ecx,?ecx
????????main.go:7???????0x10aba62???????4889cf??????????????????????????mov?rdi,?rcx
????????main.go:7???????0x10aba65???????be01000000??????????????????????mov?esi,?0x1
????????main.go:7???????0x10aba6a???????e871c3f9ff??????????????????????call?$runtime.growslice
????????main.go:7???????0x10aba6f???????488d5301????????????????????????lea?rdx,?ptr?[rbx+0x1]
????????main.go:7???????0x10aba73???????eb00????????????????????????????jmp?0x10aba75
????????main.go:7???????0x10aba75???????48c70001000000??????????????????mov?qword?ptr?[rax],?0x1
????????main.go:7???????0x10aba7c???????4889442460??????????????????????mov?qword?ptr?[rsp+0x60],?rax
????????main.go:7???????0x10aba81???????4889542468??????????????????????mov?qword?ptr?[rsp+0x68],?rdx
????????main.go:7???????0x10aba86???????48894c2470??????????????????????mov?qword?ptr?[rsp+0x70],?rcx
????????main.go:8???????0x10aba8b???????440f117c2450????????????????????movups?xmmword?ptr?[rsp+0x50],?xmm15
????????main.go:8???????0x10aba91???????488d542450??????????????????????lea?rdx,?ptr?[rsp+0x50]
????????main.go:8???????0x10aba96???????4889542448??????????????????????mov?qword?ptr?[rsp+0x48],?rdx
????????main.go:8???????0x10aba9b???????488b442460??????????????????????mov?rax,?qword?ptr?[rsp+0x60]
????????main.go:8???????0x10abaa0???????488b5c2468??????????????????????mov?rbx,?qword?ptr?[rsp+0x68]
????????main.go:8???????0x10abaa5???????488b4c2470??????????????????????mov?rcx,?qword?ptr?[rsp+0x70]
????????main.go:8???????0x10abaaa???????e8f1dff5ff??????????????????????call?$runtime.convTslice
????????main.go:8???????0x10abaaf???????4889442440??????????????????????mov?qword?ptr?[rsp+0x40],?rax
????????main.go:8???????0x10abab4???????488b542448??????????????????????mov?rdx,?qword?ptr?[rsp+0x48]
????????main.go:8???????0x10abab9???????8402????????????????????????????test?byte?ptr?[rdx],?al
????????main.go:8???????0x10ababb???????488d35be640000??????????????????lea?rsi,?ptr?[rip+0x64be]
????????main.go:8???????0x10abac2???????488932??????????????????????????mov?qword?ptr?[rdx],?rsi
????????main.go:8???????0x10abac5???????488d7a08????????????????????????lea?rdi,?ptr?[rdx+0x8]
????????main.go:8???????0x10abac9???????833d30540d0000??????????????????cmp?dword?ptr?[runtime.writeBarrier],?0x0
????????main.go:8???????0x10abad0???????7402????????????????????????????jz?0x10abad4
????????main.go:8???????0x10abad2???????eb06????????????????????????????jmp?0x10abada
????????main.go:8???????0x10abad4???????48894208????????????????????????mov?qword?ptr?[rdx+0x8],?rax
????????main.go:8???????0x10abad8???????eb08????????????????????????????jmp?0x10abae2
????????main.go:8???????0x10abada???????e8213ffbff??????????????????????call?$runtime.gcWriteBarrier
????????main.go:8???????0x10abadf???????90??????????????????????????????nop
????????main.go:8???????0x10abae0???????eb00????????????????????????????jmp?0x10abae2
????????main.go:8???????0x10abae2???????488b442448??????????????????????mov?rax,?qword?ptr?[rsp+0x48]
????????main.go:8???????0x10abae7???????8400????????????????????????????test?byte?ptr?[rax],?al
????????main.go:8???????0x10abae9???????eb00????????????????????????????jmp?0x10abaeb
????????main.go:8???????0x10abaeb???????4889442478??????????????????????mov?qword?ptr?[rsp+0x78],?rax
????????main.go:8???????0x10abaf0???????48c784248000000001000000????????mov?qword?ptr?[rsp+0x80],?0x1
????????main.go:8???????0x10abafc???????48c784248800000001000000????????mov?qword?ptr?[rsp+0x88],?0x1
????????main.go:8???????0x10abb08???????bb01000000??????????????????????mov?ebx,?0x1
????????main.go:8???????0x10abb0d???????4889d9??????????????????????????mov?rcx,?rbx
????????main.go:8???????0x10abb10???????e8aba8ffff??????????????????????call?$fmt.Println
????????main.go:9???????0x10abb15???????488bac2490000000????????????????mov?rbp,?qword?ptr?[rsp+0x90]
????????main.go:9???????0x10abb1d???????4881c498000000??????????????????add?rsp,?0x98
????????main.go:9???????0x10abb24???????c3??????????????????????????????ret
????????main.go:5???????0x10abb25???????e8f61efbff??????????????????????call?$runtime.morestack_noctxt
????????.:0?????????????0x10abb2a???????e9f1feffff??????????????????????jmp?$main.main
從以上內容我們看到調用了runtime.growslice方法,我們在這里加一個斷點:
(dlv)?break?runtime.growslice
Breakpoint?2?set?at?0x1047dea?for?runtime.growslice()?/usr/local/opt/go/libexec/src/runtime/slice.go:162
之后我們再次執(zhí)行continue執(zhí)行到該斷點處:
(dlv)?continue
>?runtime.growslice()?/usr/local/opt/go/libexec/src/runtime/slice.go:162?(hits?goroutine(1):1?total:1)?(PC:?0x1047dea)
Warning:?debugging?optimized?function
???157:?//?NOT?to?the?new?requested?capacity.
???158:?//?This?is?for?codegen?convenience.?The?old?slice's?length?is?used?immediately
???159:?//?to?calculate?where?to?write?new?values?during?an?append.
???160:?//?TODO:?When?the?old?backend?is?gone,?reconsider?this?decision.
???161:?//?The?SSA?backend?might?prefer?the?new?length?or?to?return?only?ptr/cap?and?save?stack?space.
=>?162:?func?growslice(et?*_type,?old?slice,?cap?int)?slice?{
???163:?????????if?raceenabled?{
???164:?????????????????callerpc?:=?getcallerpc()
???165:?????????????????racereadrangepc(old.array,?uintptr(old.len*int(et.size)),?callerpc,?funcPC(growslice))
???166:?????????}
???167:?????????if?msanenabled?{
之后就是不斷的單步調試可以看出來切片的擴容策略;到這里大家也就明白了為啥向nil的切片追加數據不會有問題了,因為在容量不夠時會調用growslice函數進行擴容,具體擴容規(guī)則大家可以繼續(xù)追蹤,打臉網上那些瞎寫的文章。
上文我們介紹調試匯編的一個基本流程,下面在介紹兩個我在看源代碼時經常使用的命令;
goroutines命令:通過 goroutines命令(簡寫grs),我們可以查看所goroutine,通過goroutine (alias: gr)命令可以查看當前的gourtine:
(dlv)?grs
*?Goroutine?1?-?User:?./main.go:7?main.main?(0x10aba6f)?(thread?218565)
??Goroutine?2?-?User:?/usr/local/opt/go/libexec/src/runtime/proc.go:367?runtime.gopark?(0x1035232)?[force?gc?(idle)]
??Goroutine?3?-?User:?/usr/local/opt/go/libexec/src/runtime/proc.go:367?runtime.gopark?(0x1035232)?[GC?sweep?wait]
??Goroutine?4?-?User:?/usr/local/opt/go/libexec/src/runtime/proc.go:367?runtime.gopark?(0x1035232)?[GC?scavenge?wait]
??Goroutine?5?-?User:?/usr/local/opt/go/libexec/src/runtime/proc.go:367?runtime.gopark?(0x1035232)?[finalizer?wait]
stack命令:通過stack命令(簡寫bt),我們可查看當前函數調用棧信息:
(dlv)?bt
0??0x0000000001047e15?in?runtime.growslice
???at?/usr/local/opt/go/libexec/src/runtime/slice.go:183
1??0x00000000010aba6f?in?main.main
???at?./main.go:7
2??0x0000000001034e13?in?runtime.main
???at?/usr/local/opt/go/libexec/src/runtime/proc.go:255
3??0x000000000105f9c1?in?runtime.goexit
???at?/usr/local/opt/go/libexec/src/runtime/asm_amd64.s:1581
regs命令:通過regs命令可以查看全部的寄存器狀態(tài),可以通過單步執(zhí)行來觀察寄存器的變化:
(dlv)?regs
???Rip?=?0x0000000001047e15
???Rsp?=?0x000000c00010de68
???Rax?=?0x00000000010b2f00
???Rbx?=?0x0000000000000000
???Rcx?=?0x0000000000000000
???Rdx?=?0x0000000000000008
???Rsi?=?0x0000000000000001
???Rdi?=?0x0000000000000000
???Rbp?=?0x000000c00010ded0
????R8?=?0x0000000000000000
????R9?=?0x0000000000000008
???R10?=?0x0000000001088c40
???R11?=?0x0000000000000246
???R12?=?0x000000c00010df60
???R13?=?0x0000000000000000
???R14?=?0x000000c0000001a0
???R15?=?0x00000000000000c8
Rflags?=?0x0000000000000202?????[IF?IOPL=0]
????Cs?=?0x000000000000002b
????Fs?=?0x0000000000000000
????Gs?=?0x0000000000000000
locals命令:通過locals命令,可以查看當前函數所有變量值:
(dlv)?locals
newcap?=?1
doublecap?=?0
總結
看源代碼的過程是沒有捷徑可走的,如果說有,那就是可以先看一些大佬輸出的底層原理的文章,然后參照其文章一步步入門源碼閱讀,最終還是要自己去克服這個困難,本文介紹了我自己查看源碼的一些方式,你是否有更簡便的方式呢?歡迎評論區(qū)分享出來~。
