Delve是一个专门用于Go语言的调试器,之前整理过Delve的基本使用,使用Delve调试Go程序。 本文进一步介绍如何使用Delve阅读Go的源码并找到一些语法在Go的内部是如何实现的。这里将一下在Go里创建channel的语法为例make(chan int, 100),演示如何使用Delve如何找到make chan在Go内部的源码实现以及channel具体的数据结构是什么样的。

首先使用go mod创建一个Go项目:

1mkdir make-chan-showcase
2cd make-chan-showcase
3go mod init make-chan-showcase

在项目根目录中创建一个简单的main.go,并编写如下make chan的代码:

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6	ch := make(chan int, 100)
 7	ch <- 1
 8	fmt.Println(<-ch)
 9	close(ch)
10}

main.go中的代码十分简单,首先使用make(chan int, 100)创建了一个缓冲区大小为100的channel,然后往channel中写入了数据1,之后从channel中将数据读取了出来,最后将channel关闭。

为了搞清楚channel的内部结构,以及make函数的内部实现,需要使用Delve进行调试和查看具体的汇编代码的功能。具体操作如下,在main.go的第6行也就是make函数调用那一行打上断点,并输入c执行程序到这个断点上:

 1dlv debug
 2Type 'help' for list of commands.
 3(dlv) b main.go:6
 4Breakpoint 1 set at 0x10abc18 for main.main() ./main.go:6
 5(dlv) c
 6> main.main() ./main.go:6 (hits goroutine(1):1 total:1) (PC: 0x10abc18)
 7     1: package main
 8     2:
 9     3: import "fmt"
10     4:
11     5: func main() {
12=>   6:         ch := make(chan int, 100)
13     7:         ch <- 1
14     8:         fmt.Println(<-ch)
15     9:         close(ch)
16    10: }

使用dlv的子命令disassemble简写为disass查看汇编代码:

 1(dlv) disass 
 2TEXT main.main(SB) /Workspace/make-chan-showcase/main.go
 3        main.go:5       0x10abc00       493b6610                cmp rsp, qword ptr [r14+0x10]
 4        main.go:5       0x10abc04       0f86d5000000            jbe 0x10abcdf
 5        main.go:5       0x10abc0a       4883ec68                sub rsp, 0x68
 6        main.go:5       0x10abc0e       48896c2460              mov qword ptr [rsp+0x60], rbp
 7        main.go:5       0x10abc13       488d6c2460              lea rbp, ptr [rsp+0x60]
 8=>      main.go:6       0x10abc18*      488d05e16e0000          lea rax, ptr [rip+0x6ee1]
 9        main.go:6       0x10abc1f       bb64000000              mov ebx, 0x64
10        main.go:6       0x10abc24       e81785f5ff              call $runtime.makechan
11        main.go:6       0x10abc29       4889442420              mov qword ptr [rsp+0x20], rax
12        main.go:7       0x10abc2e       488d1d3b3a0200          lea rbx, ptr [rip+0x23a3b]
13        main.go:7       0x10abc35       e88687f5ff              call $runtime.chansend1
14        main.go:8       0x10abc3a       48c744241800000000      mov qword ptr [rsp+0x18], 0x0
15        main.go:8       0x10abc43       488b442420              mov rax, qword ptr [rsp+0x20]
16        main.go:8       0x10abc48       488d5c2418              lea rbx, ptr [rsp+0x18]
17        main.go:8       0x10abc4d       e86e91f5ff              call $runtime.chanrecv1
18        main.go:8       0x10abc52       440f117c2438            movups xmmword ptr [rsp+0x38], xmm15
19        main.go:8       0x10abc58       488d4c2438              lea rcx, ptr [rsp+0x38]
20        main.go:8       0x10abc5d       48894c2430              mov qword ptr [rsp+0x30], rcx
21        main.go:8       0x10abc62       488b442418              mov rax, qword ptr [rsp+0x18]
22        main.go:8       0x10abc67       e814ddf5ff              call $runtime.convT64
23        main.go:8       0x10abc6c       4889442428              mov qword ptr [rsp+0x28], rax
24        main.go:8       0x10abc71       488b4c2430              mov rcx, qword ptr [rsp+0x30]
25        main.go:8       0x10abc76       8401                    test byte ptr [rcx], al
26        main.go:8       0x10abc78       488d1541740000          lea rdx, ptr [rip+0x7441]
27        main.go:8       0x10abc7f       488911                  mov qword ptr [rcx], rdx
28        main.go:8       0x10abc82       488d7908                lea rdi, ptr [rcx+0x8]
29        main.go:8       0x10abc86       833d73620d0000          cmp dword ptr [runtime.writeBarrier], 0x0
30        main.go:8       0x10abc8d       7402                    jz 0x10abc91
31        main.go:8       0x10abc8f       eb06                    jmp 0x10abc97
32        main.go:8       0x10abc91       48894108                mov qword ptr [rcx+0x8], rax
33        main.go:8       0x10abc95       eb07                    jmp 0x10abc9e
34        main.go:8       0x10abc97       e8c43dfbff              call $runtime.gcWriteBarrier
35        main.go:8       0x10abc9c       eb00                    jmp 0x10abc9e
36        main.go:8       0x10abc9e       488b442430              mov rax, qword ptr [rsp+0x30]
37        main.go:8       0x10abca3       8400                    test byte ptr [rax], al
38        main.go:8       0x10abca5       eb00                    jmp 0x10abca7
39        main.go:8       0x10abca7       4889442448              mov qword ptr [rsp+0x48], rax
40        main.go:8       0x10abcac       48c744245001000000      mov qword ptr [rsp+0x50], 0x1
41        main.go:8       0x10abcb5       48c744245801000000      mov qword ptr [rsp+0x58], 0x1
42        main.go:8       0x10abcbe       bb01000000              mov ebx, 0x1
43        main.go:8       0x10abcc3       4889d9                  mov rcx, rbx
44        main.go:8       0x10abcc6       e8d5a8ffff              call $fmt.Println
45        main.go:9       0x10abccb       488b442420              mov rax, qword ptr [rsp+0x20]
46        main.go:9       0x10abcd0       e86b8ef5ff              call $runtime.closechan
47        main.go:10      0x10abcd5       488b6c2460              mov rbp, qword ptr [rsp+0x60]
48        main.go:10      0x10abcda       4883c468                add rsp, 0x68
49        main.go:10      0x10abcde       c3                      ret
50        main.go:5       0x10abcdf       90                      nop
51        main.go:5       0x10abce0       e89b1dfbff              call $runtime.morestack_noctxt
52        .:0             0x10abce5       e916ffffff              jmp $main.main

从汇编代码中可以看出main.go的第6行调用了runtime.makechan,说明make(chan int, 100)的语法实际上会调用runtime.makechan

继续使用dlv在runtime.makechan上打个断点,并输入c将代码执行到runtime.makechan上:

 1(dlv) b runtime.makechan
 2Breakpoint 2 set at 0x100414a for runtime.makechan() /usr/local/Cellar/go/1.17.2/libexec/src/runtime/chan.go:71
 3(dlv) c
 4> runtime.makechan() /usr/local/Cellar/go/1.17.2/libexec/src/runtime/chan.go:71 (hits goroutine(1):1 total:1) (PC: 0x100414a)
 5Warning: debugging optimized function
 6    66:         }
 7    67:
 8    68:         return makechan(t, int(size))
 9    69: }
10    70:
11=>  71: func makechan(t *chantype, size int) *hchan {
12    72:         elem := t.elem
13    73:
14    74:         // compiler checks this but be safe.
15    75:         if elem.size >= 1<<16 {
16    76:                 throw("makechan: invalid channel element type")
17(dlv) 

这样我们就找到了runtime.makechan位于Go源码的src/runtime/chan.go的第71行,接下来就可以继续调试或直接去查看runtime.makechan的代码了。 在第91行,声明了一个var c *hchan, *hchan类型的结构体指针,hchan结构体就是channel的数据结构。

 1(dlv) n
 2> runtime.makechan() /usr/local/Cellar/go/1.17.2/libexec/src/runtime/chan.go:93 (PC: 0x10041ae)
 3Warning: debugging optimized function
 4    88:         // buf points into the same allocation, elemtype is persistent.
 5    89:         // SudoG's are referenced from their owning thread so they can't be collected.
 6    90:         // TODO(dvyukov,rlh): Rethink when collector can move allocated objects.
 7    91:         var c *hchan
 8    92:         switch {
 9=>  93:         case mem == 0:
10    94:                 // Queue or element size is zero.
11    95:                 c = (*hchan)(mallocgc(hchanSize, nil, true))
12    96:                 // Race detector uses this location for synchronization.
13    97:                 c.buf = c.raceaddr()
14    98:         case elem.ptrdata == 0:

在chan.go的第32行找到了hchan结构体的具体定义:

 1type hchan struct {
 2	qcount   uint           // total data in the queue
 3	dataqsiz uint           // size of the circular queue
 4	buf      unsafe.Pointer // points to an array of dataqsiz elements
 5	elemsize uint16
 6	closed   uint32
 7	elemtype *_type // element type
 8	sendx    uint   // send index
 9	recvx    uint   // receive index
10	recvq    waitq  // list of recv waiters
11	sendq    waitq  // list of send waiters
12
13	// lock protects all fields in hchan, as well as several
14	// fields in sudogs blocked on this channel.
15	//
16	// Do not change another G's status while holding this lock
17	// (in particular, do not ready a G), as this can deadlock
18	// with stack shrinking.
19	lock mutex
20}

后边就可以通过阅读源码和查看注释,搞清楚hchan的各个字段的具体含义了:

  • qcount channel中当前的元素个数
  • dataqsiz 循环队列(数组)的长度,channel使用循环队列实现其缓冲区
  • buf 指向循环队列(数组)的指针
  • sendx 向channel执行的发送操作到了的循环数组的索引位置
  • recvx 从channel执行的接收操作到了的循环数组的索引位置
  • recvq 如果缓冲区为空时,所有的接收操作将会被阻塞并放到这个等待队列中(具体是将接收操作所在的G的执行情况保存创建了一个sudog结构,并挂在recvq等待队列上)
  • sendq 如果缓冲区已满时,所有的发送操作将会被阻塞并放到这个等待队列中(具体是将发送操作所在的G的执行情况保存创建了一个sudog结构,并挂在sendq这个等待队列上)

本文使用的是Go 1.17.2,Go的不同版本的代码位置和具体实现可能会不同

继续调试例子中这段简单Go源码,通过dlv的调试和disassemble,还可以找到:

  • 往channel中写入数据ch <- 1对应的实现是runtime.chansend1,看源码调用了runtime.chansend
  • 从channel中读数据<-ch对应的实现是runtime.chanrecv1,看源码调用了runtime.chanrecv
  • 关闭channel close(ch)对应实现是runtime.closechan

参考