结合ChatGPT辅助学习Go语言底层原理

简介: 在当今的信息时代,编程语言已经成为了科技领域的基础工具之一。其中,Go语言以其简洁、高效和并发性强等优点,在开发者中赢得了广泛的好评。然而,对于初学者或是想要深入理解Go语言底层原理的人来说,直接阅读和理解其源码可能会面临一定困难。我主要从以下几个方面分享一些使用ChatGPT辅助学习Go语言底层源码的小技巧。

在当今的信息时代,编程语言已经成为了科技领域的基础工具之一。其中,Go语言以其简洁、高效和并发性强等优点,在开发者中赢得了广泛的好评。然而,对于初学者或是想要深入理解Go语言底层原理的人来说,直接阅读和理解其源码可能会面临一定困难。我主要从以下几个方面分享一些使用ChatGPT辅助学习Go语言底层源码的小技巧。

1、什么情况下使用ChatGPT?

首先我觉着有很多Go开发工程师和我有类似的困惑,就是想要深入学习Go语言底层原理,但自己学着学着,发现很多知识点非常深奥,又很难找到该领域的专家有足够的时间帮你答疑解惑,这个时候我想到了ChatGPT,当遇到难以理解的难题时,可以把自己的问题更加细化去问ChatGPT,让它提供更加详细的解答,直到自己理解为止。所以我的观点是在学习Go语言底层知识之前,需要自己对Go语言相关基础知识有一定的了解,然后带着疑问向ChatGPT提问,当ChatGPT给出答案时,仅供参考,需要有自己的判断,只要ChatGPT给出的解释相对合理,可以姑且相信它,也可以不断地追问加深理解。我这里主要总结了使用ChatGPT的三个方面:

  • 基础的概念和原理
  • 实际应用示例代码
  • 解读代码

    2、汇编基础

  • 推荐阅读:https://chai2010.cn/asmgo-talk/#/4

  • 推荐阅读:https://go.dev/doc/asm
  • 精简指令集
  • 数据传输: MOV/LEA
  • 跳转指令: CMP/TEST/JMP/JCC
  • 栈指令: PUSH/POP
  • 函数调用指令: CALL/RET
  • 算术指令: ADD/SUB/MUL/DIV
  • 逻辑指令: AND/OR/XOR/NOT
  • 移位指令: SHL/SHR
  • JCC有条件跳转: JEQ/JNE/JLT/JLE/JGT/JGE
  • 还有针对无符号数的比较条件
  • MOV指令
    image.png
    image.png
    注:图片来源于https://chai2010.cn/asmgo-talk/#/4/6
  • 伪寄存器

    • SB: 静态基址指针, 全局符号
    • FP: 帧指针, 参数和局部变量
    • SP: 栈指针, 栈的顶端
    • PC: 程序计数器, 跳转和分支
    • 注:伪寄存器仅仅存在于Go汇编中
      image.png
  • 伪寄存器用法

    • GLOBL text(SB),$1: 全局变量
    • MOVQ a+0(FP) AX: 函数参数
    • MOVQ b+8(SP) AX: 局部变量
    • JMP 2(PC): 向前跳转, 常用于宏函数
    • JMP -2(PC): 向后跳转, 常用于宏函数

3、代码调试工具

3.1 dlv 简介

image.png

  • dlv(delve 的简写) 是一个用于 Go 源代码级调试器。
  • dlv 通过控制进程的执行、计算变量、提供线程/协程的状态、CPU寄存器状态等信息,可以方便地与程序进行交互。
  • 这个工具的目标是为调试 Go 程序提供一个简单但功能强大的界面。
    dlv 将标志传递给正在调试的程序,例如:
dlv exec ./hello -- server --config conf/config.toml
  • macOs 使用 brew install dlv 命令下载安装即可。

3.2 dlv debug 常用命令

  • b 打断点,例如使用 b main.main 来打断点
  • p 打印变量
  • n: 执行到下一行
  • c: 跳过此断点
  • args: 打印所有的方法参数
  • locals 打印出所有的本地变量
  • l 列出断点最近几行的代码
  • bp: 展示出所有的断点
  • q: 退出

3.3dlv 调试代码

image.png
image.png

  • dlv 剖析 strings.Contains 函数
    新建 a.go 文件,文件内容如下:
package main

import (
    "fmt"
    "strings"
)

func main()  {
    str := "Go语言是世界上最好的语言"
    if strings.Contains(str,"世界") {
        fmt.Println("字符串中包含","世界")
    }else {
        fmt.Println("字符串中不包含","世界")
    }
}

使用 dlv debug a.go 命令可以开始对上述代码进行断点调试:
image.png
image.png
image.png
image.png
image.png

qinshixian@qinshixiandeMacBook-Pro qinshixian % dlv debug a.go
Type 'help' for list of commands.
(dlv) b main.main
Breakpoint 1 set at 0x10acd6f for main.main() ./a.go:8
(dlv) c
> main.main() ./a.go:8 (hits goroutine(1):1 total:1) (PC: 0x10acd6f)
     3:    import (
     4:        "fmt"
     5:        "strings"
     6:    )
     7:
=>   8:    func main()  {
     9:        str := "Go语言是世界上最好的语言"
    10:        if strings.Contains(str,"世界") {
    11:            fmt.Println("字符串中包含","世界")
    12:        }else {
    13:            fmt.Println("字符串中不包含","世界")
(dlv) n
> main.main() ./a.go:9 (PC: 0x10acd86)
     4:        "fmt"
     5:        "strings"
     6:    )
     7:
     8:    func main()  {
=>   9:        str := "Go语言是世界上最好的语言"
    10:        if strings.Contains(str,"世界") {
    11:            fmt.Println("字符串中包含","世界")
    12:        }else {
    13:            fmt.Println("字符串中不包含","世界")
    14:        }
(dlv) n
> main.main() ./a.go:10 (PC: 0x10acd9b)
     5:        "strings"
     6:    )
     7:
     8:    func main()  {
     9:        str := "Go语言是世界上最好的语言"
=>  10:        if strings.Contains(str,"世界") {
    11:            fmt.Println("字符串中包含","世界")
    12:        }else {
    13:            fmt.Println("字符串中不包含","世界")
    14:        }
    15:    }
(dlv) p str
"Go语言是世界上最好的语言"
(dlv) s strings.Contains
> strings.Contains() /Users/qinshixian/.g/go/src/strings/strings.go:61 (PC: 0x10ac1e6)
    56:            s = s[i+len(substr):]
    57:        }
    58:    }
    59:
    60:    // Contains reports whether substr is within s.
=>  61:    func Contains(s, substr string) bool {
    62:        return Index(s, substr) >= 0
    63:    }
    64:
    65:    // ContainsAny reports whether any Unicode code points in chars are within s.
    66:    func ContainsAny(s, chars string) bool {
(dlv) n
> strings.Contains() /Users/qinshixian/.g/go/src/strings/strings.go:62 (PC: 0x10ac20d)
    57:        }
    58:    }
    59:
    60:    // Contains reports whether substr is within s.
    61:    func Contains(s, substr string) bool {
=>  62:        return Index(s, substr) >= 0
    63:    }
    64:
    65:    // ContainsAny reports whether any Unicode code points in chars are within s.
    66:    func ContainsAny(s, chars string) bool {
    67:        return IndexAny(s, chars) >= 0
(dlv) b Contains
Breakpoint 2 set at 0x10ac1e6 for strings.Contains() /Users/qinshixian/.g/go/src/strings/strings.go:61
(dlv) p s
"Go语言是世界上最好的语言"
(dlv) p substr
"世界"
(dlv) s Index
> strings.Index() /Users/qinshixian/.g/go/src/strings/strings.go:1024 (PC: 0x10ac32f)
  1019:        // One string is empty. Are both?
  1020:        return s == t
  1021:    }
  1022:
  1023:    // Index returns the index of the first instance of substr in s, or -1 if substr is not present in s.
=>1024:    func Index(s, substr string) int {
  1025:        n := len(substr)
  1026:        switch {
  1027:        case n == 0:
  1028:            return 0
  1029:        case n == 1:
(dlv) n
> strings.Index() /Users/qinshixian/.g/go/src/strings/strings.go:1025 (PC: 0x10ac36f)
  1020:        return s == t
  1021:    }
  1022:
  1023:    // Index returns the index of the first instance of substr in s, or -1 if substr is not present in s.
  1024:    func Index(s, substr string) int {
=>1025:        n := len(substr)
  1026:        switch {
  1027:        case n == 0:
  1028:            return 0
  1029:        case n == 1:
  1030:            return IndexByte(s, substr[0])
(dlv) n
> strings.Index() /Users/qinshixian/.g/go/src/strings/strings.go:1027 (PC: 0x10ac380)
  1022:
  1023:    // Index returns the index of the first instance of substr in s, or -1 if substr is not present in s.
  1024:    func Index(s, substr string) int {
  1025:        n := len(substr)
  1026:        switch {
=>1027:        case n == 0:
  1028:            return 0
  1029:        case n == 1:
  1030:            return IndexByte(s, substr[0])
  1031:        case n == len(s):
  1032:            if substr == s {
(dlv) p n
6
(dlv) p len(s)
35
(dlv) n
> strings.Index() /Users/qinshixian/.g/go/src/strings/strings.go:1029 (PC: 0x10ac3a4)
  1024:    func Index(s, substr string) int {
  1025:        n := len(substr)
  1026:        switch {
  1027:        case n == 0:
  1028:            return 0
=>1029:        case n == 1:
  1030:            return IndexByte(s, substr[0])
  1031:        case n == len(s):
  1032:            if substr == s {
  1033:                return 0
  1034:            }
(dlv) n
> strings.Index() /Users/qinshixian/.g/go/src/strings/strings.go:1031 (PC: 0x10ac403)
  1026:        switch {
  1027:        case n == 0:
  1028:            return 0
  1029:        case n == 1:
  1030:            return IndexByte(s, substr[0])
=>1031:        case n == len(s):
  1032:            if substr == s {
  1033:                return 0
  1034:            }
  1035:            return -1
  1036:        case n > len(s):
(dlv)
> strings.Index() /Users/qinshixian/.g/go/src/strings/strings.go:1036 (PC: 0x10ac494)
  1031:        case n == len(s):
  1032:            if substr == s {
  1033:                return 0
  1034:            }
  1035:            return -1
=>1036:        case n > len(s):
  1037:            return -1
  1038:        case n <= bytealg.MaxLen:
  1039:            // Use brute force when s and substr both are small
  1040:            if len(s) <= bytealg.MaxBruteForce {
  1041:                return bytealg.IndexString(s, substr)
(dlv) n
> strings.Index() /Users/qinshixian/.g/go/src/strings/strings.go:1038 (PC: 0x10ac4cf)
  1033:                return 0
  1034:            }
  1035:            return -1
  1036:        case n > len(s):
  1037:            return -1
=>1038:        case n <= bytealg.MaxLen:
  1039:            // Use brute force when s and substr both are small
  1040:            if len(s) <= bytealg.MaxBruteForce {
  1041:                return bytealg.IndexString(s, substr)
  1042:            }
  1043:            c0 := substr[0]
(dlv) p bytealg.MaxLen
63
(dlv) n
> strings.Index() /Users/qinshixian/.g/go/src/strings/strings.go:1040 (PC: 0x10ac4e7)
  1035:            return -1
  1036:        case n > len(s):
  1037:            return -1
  1038:        case n <= bytealg.MaxLen:
  1039:            // Use brute force when s and substr both are small
=>1040:            if len(s) <= bytealg.MaxBruteForce {
  1041:                return bytealg.IndexString(s, substr)
  1042:            }
  1043:            c0 := substr[0]
  1044:            c1 := substr[1]
  1045:            i := 0
(dlv) p len(s)
35
(dlv) p bytealg.MaxBruteForce
64
(dlv) n
> strings.Index() /Users/qinshixian/.g/go/src/strings/strings.go:1041 (PC: 0x10ac4ff)
  1036:        case n > len(s):
  1037:            return -1
  1038:        case n <= bytealg.MaxLen:
  1039:            // Use brute force when s and substr both are small
  1040:            if len(s) <= bytealg.MaxBruteForce {
=>1041:                return bytealg.IndexString(s, substr)
  1042:            }
  1043:            c0 := substr[0]
  1044:            c1 := substr[1]
  1045:            i := 0
  1046:            t := len(s) - n + 1
(dlv) s bytealg.IndexString
> internal/bytealg.IndexString() /Users/qinshixian/.g/go/src/internal/bytealg/index_amd64.s:18 (PC: 0x1002840)
Warning: debugging optimized function
    13:        MOVQ DI, R10
    14:        LEAQ ret+48(FP), R11
    15:        JMP  indexbody<>(SB)
    16:
    17:    TEXT ·IndexString(SB),NOSPLIT,$0-40
=>  18:        MOVQ a_base+0(FP), DI
    19:        MOVQ a_len+8(FP), DX
    20:        MOVQ b_base+16(FP), R8
    21:        MOVQ b_len+24(FP), AX
    22:        MOVQ DI, R10
    23:        LEAQ ret+32(FP), R11
(dlv) bp
Breakpoint runtime-fatal-throw (enabled) at 0x1032e20 for runtime.throw() /Users/qinshixian/.g/go/src/runtime/panic.go:1188 (0)
Breakpoint unrecovered-panic (enabled) at 0x1033180 for runtime.fatalpanic() /Users/qinshixian/.g/go/src/runtime/panic.go:1271 (0)
    print runtime.curg._panic.arg
Breakpoint 1 (enabled) at 0x10acd6f for main.main() ./a.go:8 (1)
Breakpoint 2 (enabled) at 0x10ac1e6 for strings.Contains() /Users/qinshixian/.g/go/src/strings/strings.go:61 (0)
(dlv)

4、示例代码

go version:go 1.14.15
package main

func main() {
   n := 10
   println(read(&n))
}


//go:noinline
func read(p *int) (v int) {
   v = *p
   return
}
  • //go:noinline:禁止Go对函数进行内联
  • 内联:内联是一种手动或编译器优化,用于将简短函数的调用替换为函数体本身。这么做的原因是它可以消除函数调用本身的开销,也使得编译器能更高效地执行其他的优化策略
  • 使用objdump工具反编译
    image.png

5、结合ChatGPT反编译调试

  • 反汇编的两种命令
go tool compile -S a.go
或
go tool compile -N -l a.go
go tool objdump a.o

Tips:两者的区别是汇编格式不一样,-N -l加上去除优化和内联选项
  • 对示例代码反编译
go build main.go                                 //编译

go tool objdump -S -s "main.read" main      //反编译

image.png

  • 图中是用go自带的objdump工具对main.read反编译得到的汇编指令
  • 不懂就问,咱们直接问ChatGPT这段汇编指令的含义(首先需要给ChatGPT一些引导和背景介绍),如下图
    image.png
    image.png
  • 上面解释的很清楚了,为了进一步理解这些指令的含义,追问
    image.png
    image.png
    image.png

6、Go代码原理剖析实战

下面通过列举一些例子,结合ChatGPT来辅助深入理解Go代码实际运行机制,以后遇到类似困惑的时候,都可以通过该方法了解其中的原理。

6.1 指针

  • 指针本身是一个无符号整型
    go version:go 1.14.15
    
package main


func main() {
   n := int32(10)
   println(read32(&n))
}

//go:noinline
func read32(p *int32) (v int32) {
   v = *p
   return
}

image.png
image.png

可以看到上述汇编指令中,第一行从参数p中取地址值的操作没变化,只是从AX寄存器中取值的时候,命令有MOVQ(8字节)变为MOVL(4字节),可见不同类型的指针地址本身是一样的类型(无符号整型) 。

  • 取地址
    go version:go 1.14.15
    
package main


var n int


func main() {
   println(addr())
}


//go:noinline
func addr() (p *int) {
   return &n
}

image.png

直接问ChatGPT,给出的解释是:

  • 从上图可以看到全局变量n是存在main包的静态基地址上(SB),被不同的函数和代码块共享访问,SB 是静态基地址的缩写,它是指向静态基地址的寄存器。因此,"main.n(SB)" 就是通过 SB 指向 main 包的静态基地址上的 n 变量。
  • LEAQ 指令将全局变量 n 的有效地址存储到 AX 寄存器中,这样 AX 寄存器就包含了 n 变量的地址,可以用于读取或写入该变量的值。
  • LEAQ 指令用于将有效地址存储到一个寄存器中

  • 强制类型转换

    go version:go 1.14.15
    
package main


import "unsafe"


func main() {
   p := 3
   convert(&p)
}


//go:noinline
func convert(p *int) {
   q := (*int32)(unsafe.Pointer(p))
   *q = 0
}

image.png
image.png

  • 这段汇编代码是将一个指向int类型变量的指针,转换为指向int32类型变量的指针,并将其所指向的内存空间的值设置为0
  • 把指针的类型强转换为int32后,原本的MOVQ指令变成了MOVL,没有产生任何额外指令,所以转换效率是非常高的

    6.2go 函数调用栈

  • 程序的基本分段

    .data : 有初始化值的全局变量;定义常量。
    .bss : 没有初始化值的全局变量。
    .text : 代码段。
    .rodata: 只读数据段。
    

    image.png
    image.png

  • CALL 和 RET 作用
    当 A 函数调用 B 函数时,编译器就会对应生成一条 CALL 指令,程序执行到 CALL 指令时,就会跳转到被调用函数的入口处开始执行,每个被调用的函数执行结束会有一条 RET 指令,负责函数结束后跳回调用处。
    image.png

  • go语言函数调用规约




                                       caller                                                                                 
                                 +------------------+                                                                         
                                 |                  |                                                                         
       +---------------------->  --------------------                                                                         
       |                         |                  |                                                                         
       |                         | caller parent BP |                                                                         
       |           BP(pseudo SP) --------------------                                                                         
       |                         |                  |                                                                         
       |                         |   Local Var0     |                                                                         
       |                         --------------------                                                                         
       |                         |                  |                                                                         
       |                         |   .......        |                                                                         
       |                         --------------------                                                                         
       |                         |                  |                                                                         
       |                         |   Local VarN     |                                                                         
                                 --------------------                                                                         
 caller stack frame              |                  |                                                                         
                                 |   callee arg2    |                                                                         
       |                         |------------------|                                                                         
       |                         |                  |                                                                         
       |                         |   callee arg1    |                                                                         
       |                         |------------------|                                                                         
       |                         |                  |                                                                         
       |                         |   callee arg0    |                                                                         
       |                         ----------------------------------------------+   FP(virtual register)                       
       |                         |                  |                          |                                              
       |                         |   return addr    |  parent return address   |                                              
       +---------------------->  +------------------+---------------------------    <-------------------------------+         
                                                    |  caller BP               |                                    |         
                                                    |  (caller frame pointer)  |                                    |         
                                     BP(pseudo SP)  ----------------------------                                    |         
                                                    |                          |                                    |         
                                                    |     Local Var0           |                                    |         
                                                    ----------------------------                                    |         
                                                    |                          |                                              
                                                    |     Local Var1           |                                              
                                                    ----------------------------                            callee stack frame
                                                    |                          |                                              
                                                    |       .....              |                                              
                                                    ----------------------------                                    |         
                                                    |                          |                                    |         
                                                    |     Local VarN           |                                    |         
                                  SP(Real Register) ----------------------------                                    |         
                                                    |                          |                                    |         
                                                    |                          |                                    |         
                                                    |                          |                                    |         
                                                    |                          |                                    |         
                                                    |                          |                                    |         
                                                    +--------------------------+    <-------------------------------+         

                                                              callee
                                      -----------------                                           
                       current func arg0                                           
                       ----------------- <----------- FP(pseudo FP)                
                        caller ret addr                                            
                       +---------------+                                           
                       | caller BP(*)  |                                           
                       ----------------- <----------- SP(pseudo SP,实际上是当前栈帧的 BP 位置)
                       |   Local Var0  |                                           
                       -----------------                                           
                       |   Local Var1  |                                           
                       -----------------                                           
                       |   Local Var2  |                                           
                       -----------------                -                          
                       |   ........    |                                           
                       -----------------                                           
                       |   Local VarN  |                                           
                       -----------------                                           
                       |               |                                           
                       |               |                                           
                       |  temporarily  |                                           
                       |  unused space |                                           
                       |               |                                           
                       |               |                                           
                       -----------------                                           
                       |  call retn    |                                           
                       -----------------                                           
                       |  call ret(n-1)|                                           
                       -----------------                                           
                       |  ..........   |                                           
                       -----------------                                           
                       |  call ret1    |                                           
                       -----------------                                           
                       |  call argn    |                                           
                       -----------------                                           
                       |   .....       |                                           
                       -----------------                                           
                       |  call arg3    |                                           
                       -----------------                                           
                       |  call arg2    |                                           
                       |---------------|                                           
                       |  call arg1    |                                           
                       -----------------   <------------  hardware SP 位置           
                       | return addr   |                                           
                       +---------------+

函数执行时需要有足够的内存空间,供它存放局部变量、参数、返回值,这段空间对应到虚拟地址空间的栈。
image.png

  • 示例代码
    go version:go 1.17.2
    
package main

import "fmt"

func main()  {
    a,b := 1,2
    c := sum(a,b)
    fmt.Println(c)
}

func sum(x,y int) (z int) {
    z = x + y
    return z
}
  • 入栈和出战规则
    image.png
    image.png

6.3go defer、panic、recover

image.png
image.png

  • panic 能立即终止程序,并在当前 Goroutine 中递归执行调用方的 defer
  • recover 可以终止 panic 造成的程序崩溃,并且它只能在defer中起到作用
  • _panic结构体
    image.png

  • _defer结构体
    image.png

  • defer 示例代码调试

    go version:go 1.14.15
    

    下面这段关于defer的go代码,你知道defer是如何影响incr函数中的变量a变量b的吗?

    package main
    import "fmt"
    func main(){
      a,b := 1,2
      c := incr(a,b)
      fmt.Println(c)
    }
    func incr(m,n int) int {
      var b int
      a := m + n
      defer func(x int) {
          a = a + x
          b++
      }(a + 1)
      a = 99
      b = a
      return b
    }
    

    函数调用栈示意图:
    image.png

反编译调试:

go tool compile -N -l main.go
go tool objdump mian.o

image.png
image.png
image.png
image.png

结合上面的分析incr函数中的变量a和变量b的分析如下:

(1)incr中变量a作为局部变量,值是 m + n 等于 3;

(2)在入栈给的时候,传给 x 的值是 a+1 等于 4;

(3)给a赋值99,a的值变为99;

(4)给b赋值,b的值为99;

(5)赋值给返回地址,取b的值 99;

(6)执行defer函数,a 的值为 a + x,就是 99 + 4,a的值变为 103;

(7)b++,b的值变为100(注意在此之前b的值为99时,已经赋给返回地址了);

综上所述,main函数中的c最终的打印值是 99

Tips:go函数defer执行时是先给返回值地址赋值,再执行defer
  • panic 正常执行 defer
    go version:go 1.14.15
    
package main

import "fmt"

func main()  {
    defer A()
    defer B()
    defer C()
    panic("panic A")
    fmt.Println("func main")
}

func A()  {
    fmt.Println("func A")
}

func B()  {
    fmt.Println("func B")
}

func C(){
    fmt.Println("func C")
}

使用如下命令可以查看对应的底层汇编和函数调用栈信息:

go tool compile -N -l t1.go
go tool objdump t1.o

image.png
可以看出来遇到 panic 时,底层调用的是 runtime.gopanic,其对应的函数做如下事情:

  • (1)往Goroutine的panic链表表头插入panic
  • (2)循环执行Goroutine的_defer链表
  • (3)_panic 执行 defer时会先把 started 字段置为 true
  • (4)把 _defer 结构体 _panic 字段指针指向当前的执行 _panic 的结构体

    情况一:panic 正常执行defer,此时被正常执行的_defer节点会被移除
    情况二:panic 执行 defer 中有 panic
    

    image.png

  • panic 执行 defer 中有 panic

    go version:go 1.14.15
    
package main

import "fmt"

func main()  {
    defer A()
    defer B()
    defer C()
    panic("panic A")
    fmt.Println("func main")
}

func A()  {
    fmt.Println("func A")
}

func B()  {
    panic("panic B")
    fmt.Println("func B")
}

func C(){
    fmt.Println("func C")
}
  • 该例子前面步骤和例子 t1.go 中一样,defer C节点被正常执行会被移除
  • 执行到 defer B 的时候,该defer 节点B.startred=true,B.panic = &panicB, 并且 func B() 函数中有 panic,会调用底层 runtime.gopanic,执行和t1.go中相同的动作
  • func B() 中的 panicB 会向 goroutine 中的 _panic 链表头部插入 panicB 节点
  • panicB 会遍历 defer 链表,并且遍历到第一个 defer B 的时候,发现 defer B.startred 字段已经被设置为 true(是之前由panicA 设置的),此时就会把defer B.panic 指向的 panicA 结构体中的 aborted 字段设置为true,表示 panicA 被终止了
  • 移除 defer B节点,继续正常执行 defer A
  • 接下来打印 panic 信息,_panic 链表是从链表尾部向头部打印的,所以是先打印 panicA,在打印 panicB的信息
  • panic 执行 defer 中有recover
    go version:go 1.14.15
    
package main

import "fmt"

func main()  {
    defer A()
    defer B()
    defer C()
    panic("panic A")
    fmt.Println("func main")
}

func A()  {
    x := recover()
    fmt.Println("func A,recover:",x)
}

func B()  {
    panic("panic B")
    fmt.Println("func B")
}

func C(){
    fmt.Println("func C")
}
  • 该例子中前面步骤参考 t1.go 和 t2.go,最后panicB 在遍历defer A 节点时,触发 func A() 函数,该函数中,有recover函数
  • recover() 函数只做一件事,把当前执行的panic节点的 recovered字段设置为true,并且此时recover()函数捕获到的信息为panicB的信息,即x=”panic B”,该例子中 func A() 函数中的 recover 设置的是 panicB节点中的 recovered字段,此时panicB节点已经被恢复,就会被移除,移除后会跳出panicB的处理流程,接着panicA的处理流程
  • panic 执行defer中有recove&panic
    go version:go 1.14.15
    
package main

import "fmt"

func main()  {
    defer A()
    defer B()
    defer C()
    panic("panic A")
    fmt.Println("func main")
}

func A()  {
    x := recover()
    fmt.Println("func A,recover:",x)
    panic("panic R")
}

func B()  {
    panic("panic B")
    fmt.Println("func B")
}

func C(){
    fmt.Println("func C")
}
  • 该例子中前面步骤参考 t1.go、t2.go、t3.go,当执行到 recover 函数时,panic B被recover()函数设置为已回复,但recover()函数后面还有 panicR
  • panicR 会在goroutine 头部插入 panic 节点,并且 panicR 也会执行底层 runtime.gopanic函数,遍历defer 链表,发现此时 defer A startred 为 true,且会把 defer A链表指向的 panic B设置为已终止,并且移除 defer A,此时 panic B 的 aborted 和 recovered 都是true
  • 接下来就是打印 panic 信息,和之前一样,_panic 链表也是从链表尾部开始的,首先打印 panic A的信息,由于 panic B被恢复了,所以先打印 panic B的信息时候,会提示[recovered],然后打印 panic R。

    7、小结

上面只是介绍如何用ChatGPT辅助学习,通过细节追问可以更加深入的了解Go语言原理,ChatGPT拥有大量的知识和信息,可以提供关于Go语言底层原理的详细解释和示例,也可以帮助我们理解底层原理的概念、工作原理和实际应用。我们也可以根据自己的兴趣和理解程度向ChatGPT提问,它支持多轮对话,和它进行交互式的学习,这种交互式学习方式可以帮助我们更好地理解和应用Go语言底层原理。

相关文章
|
23天前
|
安全 Java 编译器
对比Java学习Go——基础理论篇
本章介绍了Java开发者学习Go语言的必要性。Go语言以简单、高效、并发为核心设计哲学,摒弃了传统的类继承和异常机制,采用组合、接口和多返回值错误处理,提升了代码清晰度与开发效率。Go直接编译为静态二进制文件,启动迅速、部署简便,其基于Goroutine和Channel的并发模型相较Java的线程与锁机制更轻量安全。此外,Go Modules简化了依赖管理,与Java的Maven/Gradle形成鲜明对比,提升了构建与部署效率。
|
13天前
|
存储 Java Go
对比Java学习Go——函数、集合和OOP
Go语言的函数支持声明与调用,具备多返回值、命名返回值等特性,结合`func`关键字与类型后置语法,使函数定义简洁直观。函数可作为一等公民传递、赋值或作为参数,支持匿名函数与闭包。Go通过组合与接口实现面向对象编程,结构体定义数据,方法定义行为,接口实现多态,体现了Go语言的简洁与高效设计。
|
23天前
|
存储 Java 编译器
对比Java学习Go——程序结构与变量
本节对比了Java与Go语言的基础结构,包括“Hello, World!”程序、代码组织方式、入口函数定义、基本数据类型及变量声明方式。Java强调严格的面向对象结构,所有代码需置于类中,入口方法需严格符合`public static void main(String[] args)`格式;而Go语言结构更简洁,使用包和函数组织代码,入口函数为`func main()`。两种语言在变量声明、常量定义、类型系统等方面也存在显著差异,体现了各自的设计哲学。
|
3月前
|
人工智能 安全 Java
Go与Java泛型原理简介
本文介绍了Go与Java泛型的实现原理。Go通过单态化为不同类型生成函数副本,提升运行效率;而Java则采用类型擦除,将泛型转为Object类型处理,保持兼容性但牺牲部分类型安全。两种机制各有优劣,适用于不同场景。
95 24
|
3月前
|
存储 人工智能 安全
深入理解 go sync.Map - 基本原理
本文介绍了 Go 语言中 `map` 在并发使用时的常见问题及其解决方案,重点对比了 `sync.Mutex`、`sync.RWMutex` 和 `sync.Map` 的性能差异及适用场景。文章指出,普通 `map` 不支持并发读写,容易引发错误;而 `sync.Map` 通过原子操作和优化设计,在某些场景下能显著提升性能。同时详细讲解了 `sync.Map` 的基本用法及其适合的应用环境,如读多写少或不同 goroutine 操作不同键的场景。
143 1
|
2月前
|
数据采集 Go API
Go语言实战案例:多协程并发下载网页内容
本文是《Go语言100个实战案例 · 网络与并发篇》第6篇,讲解如何使用 Goroutine 和 Channel 实现多协程并发抓取网页内容,提升网络请求效率。通过实战掌握高并发编程技巧,构建爬虫、内容聚合器等工具,涵盖 WaitGroup、超时控制、错误处理等核心知识点。
|
2月前
|
数据采集 JSON Go
Go语言实战案例:实现HTTP客户端请求并解析响应
本文是 Go 网络与并发实战系列的第 2 篇,详细介绍如何使用 Go 构建 HTTP 客户端,涵盖请求发送、响应解析、错误处理、Header 与 Body 提取等流程,并通过实战代码演示如何并发请求多个 URL,适合希望掌握 Go 网络编程基础的开发者。
|
3月前
|
JSON 前端开发 Go
Go语言实战:创建一个简单的 HTTP 服务器
本篇是《Go语言101实战》系列之一,讲解如何使用Go构建基础HTTP服务器。涵盖Go语言并发优势、HTTP服务搭建、路由处理、日志记录及测试方法,助你掌握高性能Web服务开发核心技能。
|
3月前
|
Go
如何在Go语言的HTTP请求中设置使用代理服务器
当使用特定的代理时,在某些情况下可能需要认证信息,认证信息可以在代理URL中提供,格式通常是:
244 0
|
4月前
|
算法 Java Go
Go内存原理-GC原理
本文介绍了Go语言中垃圾回收(GC)机制的发展与实现原理,涵盖从标记-清除算法到三色标记法,再到三色标记加混合写屏障的演进过程,重点解析各版本GC的核心思想、优缺点及性能优化方向。