原文連結:分享如何閱讀Go語言原始碼
前言
哈嘍,大家好,我是asong
;最近在看Go語言排程器相關的原始碼,發現看原始碼真是個技術活,所以本文就簡單總結一下該如何檢視Go
原始碼,希望對你們有幫助。
Go原始碼包括哪些?
以我個人理解,Go
原始碼主要分為兩部分,一部分是官方提供的標準庫,一部分是Go
語言的底層實現,Go
語言的所有原始碼/標準庫/編譯器都在src
目錄下:https://github.com/golang/go/...,想看什麼庫的原始碼任君選擇;
觀看Go
標準庫 and Go
底層實現的原始碼難易度也是不一樣的,我們一般也可以先從標準庫入手,挑選你感興趣的模組,把它吃透,有了這個基礎後,我們在看Go
語言底層實現的原始碼會稍微輕鬆一些;下面就針對我個人的一點學習心得分享一下如何檢視Go
原始碼;
檢視標準庫原始碼
標準庫的原始碼看起來稍容易些,因為標準庫也屬於上層應用,我們可以藉助IDE的幫忙,其在IDE上就可以跳轉到原始碼包,我們只需要不斷來回跳轉檢視各個函式實現做好筆記即可,因為一些原始碼設計的比較複雜,大家在看時最好通過畫圖輔助一下,個人覺得畫UML
是最有助於理解的,能更清晰的理清各個實體的關係;
有些時候只看程式碼是很難理解的,這時我們使用線上除錯輔助我們理解,使用IDE提供的偵錯程式或者GDB
都可以達到目的,寫一個簡單的demo
,斷點一打,單步除錯走起來,比如你要檢視fmt.Println
的原始碼,開局一個小紅點,然後就是點點點;
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/020ee5d660264362b42dab2a142bc369~tplv-k3u1fbpfcp-zoom-1.image" />
檢視Go語言底層實現
人都是會對未知領域充滿好奇,當使用一段時間Go
語言後,就想更深入的搞明白一些事情,例如:Go程式的啟動過程是怎樣的,goroutine
是怎麼排程的,map
是怎麼實現的等等一些Go
底層的實現,這種直接依靠IDE跳轉追溯程式碼是辦不到的,這些都屬於Go
語言的內部實現,大都在src
目錄下的runtime
包內實現,其實現了垃圾回收,併發控制, 棧管理以及其他一些 Go 語言的關鍵特性,在編譯Go
程式碼為機器程式碼時也會將其也編譯進來,runtime
就是Go
程式執行時候使用的庫,所以一些Go
底層原理都在這個包內,我們需要藉助一些方式才能檢視到Go
程式執行時的程式碼,這裡分享兩種方式:分析彙編程式碼、dlv除錯;
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/43155623d60042d98539d4885bb73020~tplv-k3u1fbpfcp-zoom-1.image" alt="" />
分析彙編程式碼
前面我們已經介紹了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
檔案,並輸出彙編程式碼,方式二是反彙編,這裡推薦使用方式一,執行方式一命令後,我們可以看到對應的彙編程式碼如下:
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6071b1f1459144f5b891c3c4d60559df~tplv-k3u1fbpfcp-zoom-1.image" />
s := make([]int, 10, 20)
對應的原始碼就是 runtime.makeslice(SB)
,這時候我們就去runtime
包下找makeslice
函式,不斷追蹤下去就可檢視原始碼實現了,可在runtime/slice.go
中找到:
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d9905faa0ef2479b997279691ddba98f~tplv-k3u1fbpfcp-zoom-1.image" />
線上除錯
雖然上面的方法可以幫助我們定位到原始碼,但是後續的操作全靠review
還是難於理解的,如果能線上除錯跟蹤程式碼可以更好助於我們理解,目前Go
語言支援GDB
、LLDB
、Delve
偵錯程式,但只有Delve
是專門為Go
語言設計開發的除錯工具,所以使用Delve
可以輕鬆除錯Go
彙編程式,Delve
的入門文章有很多,這篇就不在介紹Delve
的詳細使用方法,入門大家可以看曹大的文章:https://chai2010.cn/advanced-...,本文就使用一個小例子帶大家來看一看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
那行加上斷點,執行如下命令:
(dlv) break main.go:7
Breakpoint 1 set at 0x10aba57 for main.main() ./main.go:7
執行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: }
接下來我們執行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
之後我們再次執行continue
執行到該斷點處:
(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
函式進行擴容,具體擴容規則大家可以繼續追蹤,打臉網上那些瞎寫的文章。
上文我們介紹除錯彙編的一個基本流程,下面在介紹兩個我在看原始碼時經常使用的命令;
- 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
命令可以檢視全部的暫存器狀態,可以通過單步執行來觀察暫存器的變化:
(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
總結
看原始碼的過程是沒有捷徑可走的,如果說有,那就是可以先看一些大佬輸出的底層原理的文章,然後參照其文章一步步入門原始碼閱讀,最終還是要自己去克服這個困難,本文介紹了我自己檢視原始碼的一些方式,你是否有更簡便的方式呢?歡迎評論區分享出來~。
好啦,本文到這裡就結束了,我是asong,我們下期見。
歡迎關注公眾號:Golang夢工廠