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

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import "fmt"

func main() {
	ch := make(chan int, 100)
	ch <- 1
	fmt.Println(<-ch)
	close(ch)
}

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

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

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

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

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

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

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

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

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
type hchan struct {
	qcount   uint           // total data in the queue
	dataqsiz uint           // size of the circular queue
	buf      unsafe.Pointer // points to an array of dataqsiz elements
	elemsize uint16
	closed   uint32
	elemtype *_type // element type
	sendx    uint   // send index
	recvx    uint   // receive index
	recvq    waitq  // list of recv waiters
	sendq    waitq  // list of send waiters

	// lock protects all fields in hchan, as well as several
	// fields in sudogs blocked on this channel.
	//
	// Do not change another G's status while holding this lock
	// (in particular, do not ready a G), as this can deadlock
	// with stack shrinking.
	lock mutex
}

后边就可以通过阅读源码和查看注释,搞清楚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

参考