golang 基础语法的一次深入理解

8 阅读11分钟

我们在前面的一篇讲了golang 常见语法入门,这篇讲了比较多的语法,这是语法关键词的运用可以通过熟练实践就可以达到的,那么大家想不想知道背后golang 是底层是怎么实现的;

make 和 new

在我们编程中,常见创建变量的关键字就那么几种(除了直接结构体声明这种),接下来讲的make和new,它们都是内存分配的魔法师,但各有神通。

概念

new:简单而强大的内存分配者。

它的作用是:

  • 分配内存
  • 将内存置零
  • 返回指向这块内存的指针

new(T)会为类型T分配零值内存,然后返回其地址,即*T类型的值。

make:复杂类型的构造者,make函数则更加复杂,它专门用于创建slice、map和channel这三种类型。make不仅分配内存,还会初始化这些类型的内部数据结构。

关键区别:

  1. 返回类型:new返回指针,make返回实例。
  2. 适用类型:new可以为任何类型分配内存,make只用于slice、map和channel。
  3. 初始化程度:new只分配内存,make还会初始化内部数据结构。

应用场景:

  • 使用new:当你需要为自定义类型或基本类型分配内存,并且只需要得到一个指针时。
  • 使用make:当你需要创建并初始化slice、map或channel时。

new和make两者的区别为:

  1. new的存在使得Go能够以统一的方式为各种类型分配内存,简化了内存管理。
  2. make的存在是因为slice、map和channel这三种类型都是复杂的数据结构,需要进行特殊的初始化。make确保了这些类型在使用前被正确初始化,提高了程序的安全性和效率。

示例

这个例子清楚地展示了new和make在返回类型和使用方式上的区别。new返回指针,需要解引用后才能使用append;而make返回的是已初始化的切片,可以直接使用。

package main
​
import "fmt"func main() {
  newSlice := new([]int)
  makeSlice := make([]int, 0)
  fmt.Printf("newSlice: %v, %T\n", newSlice, newSlice) //  &[], *[]int
  fmt.Printf("makeSlice: %v, %T\n", makeSlice, makeSlice) //[], []int
​
  *newSlice = append(*newSlice, 1)
  makeSlice = append(makeSlice, 1)
  fmt.Printf("newSlice: %v\n", *newSlice) //[1]
  fmt.Printf("makeSlice: %v\n", makeSlice) //[1]
}

底层实现

在编译期间的类型检查阶段,Go 语言会将代表 make 关键字的 OMAKE 节点根据参数类型的不同转换成了 OMAKESLICE、OMAKEMAP 和 OMAKECHAN 三种不同类型的节点,这些节点会调用不同的运行时函数来初始化相应的数据结构。

new底层是通过runtime.newobject 实现的,runtime.newobject 函数会获取传入类型占用空间的大小,调用 runtime.mallocgc 在堆上申请一片内存空间并返回指向这片内存空间的指针:

func newobject(typ *_type) unsafe.Pointer {
        return mallocgc(typ.size, typ, true)
}

defer

defer规则

规则一:延迟函数的参数在defer语句出现时就已经确定下来了

官方给出一个例子,如下所示:

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}

defer语句中的fmt.Println()参数i值在defer出现时就已经确定下来,实际上是拷贝了一份。后面对变量i的修改不会影响fmt.Println()函数的执行,仍然打印"0"。

注意:对于指针类型参数,规则仍然适用,只不过延迟函数的参数是一个地址值,这种情况下,defer后面的语句对变量的修改可能会影响延迟函数。

规则二:延迟函数执行按后进先出顺序执行,即先出现的defer最后执行

这个规则很好理解,定义defer类似于入栈操作,执行defer类似于出栈操作。

设计defer的初衷是简化函数返回时资源清理的动作,资源往往有依赖顺序,比如先申请A资源,再跟据A资源申请B资源,跟据B资源申请C资源,即申请顺序是:A-->B-->C,释放时往往又要反向进行。这就是把deffer设计成FIFO的原因。

每申请到一个用完需要释放的资源时,立即定义一个defer来释放资源是个很好的习惯。

func main() {
    defer fmt.Println(1)
    defer fmt.Println(2)
    defer fmt.Println(3)
}
​
输出
3
2
1

规则三:延迟函数可能操作主函数的具名返回值

定义defer的函数,即主函数可能有返回值,返回值有没有名字没有关系,defer所作用的函数,即延迟函数可能会影响到返回值。

若要理解延迟函数是如何影响主函数返回值的,只要明白函数是如何返回的就足够了。

函数返回过程

有一个事实必须要了解,关键字return不是一个原子操作,实际上return只代理汇编指令ret,即将跳转程序执行。比如语句return i,实际上分两步进行,即将i值存入栈中作为返回值,然后执行跳转,而defer的执行时机正是跳转前,所以说defer执行时还是有机会操作返回值的。

举个实际的例子进行说明这个过程:

func deferFuncReturn() (result int) {
    i := 1
    
    deferfunc() {
       result++
    }()
    
    return i
}

该函数的return语句可以拆分成下面两行:

result = i
return

而延迟函数的执行正是在return之前,即加入defer后的执行过程如下:

result = i
result++
return

所以上面函数实际返回i++值。

关于主函数有不同的返回方式,但返回机制就如上机介绍所说,只要把return语句拆开都可以很好的理解,下面分别举例说明

主函数拥有匿名返回值,返回字面值

一个主函数拥有一个匿名的返回值,返回时使用字面值,比如返回"1"、"2"、"Hello"这样的值,这种情况下defer语句是无法操作返回值的。

一个返回字面值的函数,如下所示:

func foo() int {
    var i int
    deferfunc() {
        i++
    }()
​
    return 1
}

上面的return语句,直接把1写入栈中作为返回值,延迟函数无法操作该返回值,所以就无法影响返回值。

主函数拥有匿名返回值,返回变量

一个主函数拥有一个匿名的返回值,返回使用本地或全局变量,这种情况下defer语句可以引用到返回值,但不会改变返回值。

一个返回本地变量的函数,如下所示:

func foo() int {
    var i int
    deferfunc() {
        i++
    }()
    
    return i
}

上面的函数,返回一个局部变量,同时defer函数也会操作这个局部变量。对于匿名返回值来说,可以假定仍然有一个变量存储返回值,假定返回值变量为"anony",上面的返回语句可以拆分成以下过程:

anony = i
i++
return

由于i是整型,会将值拷贝给anony,所以defer语句中修改i值,对函数返回值不造成影响。

主函数拥有具名返回值

主函声明语句中带名字的返回值,会被初始化成一个局部变量,函数内部可以像使用局部变量一样使用该返回值。如果defer语句操作该返回值,可能会改变返回结果。

一个影响函返回值的例子:

func foo() (ret int) {
    deferfunc() {
        ret++
    }()
​
    return0
}

上面的函数拆解出来,如下所示:

ret = 0
ret++
return

函数真正返回前,在defer中对返回值做了+1操作,所以函数最终返回1。

defer实现原理

func main() {
​
    defer greet("friend")
​
    println("welcome")
​
}
​
func greet(text string) {
    print("hello " + text)
​
}

上面代码翻译成汇编后代码如下:

"".main STEXT size=203 args=0x0 locals=0x68
        0x000000000 (main.go:5)        TEXT    "".main(SB), ABIInternal, $104-0
        // ...
        0x003400052 (main.go:6)        CALL    runtime.deferproc(SB)
        // ...
        0x00a200162 (main.go:11)       CALL    runtime.deferreturn(SB)
        // ...
        0x00b000176 (main.go:11)       RET

可以看到是通过调用 runtime.deferproc(SB) 和 runtime.deferreturn(SB) 函数来实现 defer 功能的。

runtime.deferproc

runtime.deferproc 会为 defer 创建一个新的 runtime._defer 结构体、设置它的函数指针 fn、程序计数器 pc 和栈指针 sp 并将相关的参数拷贝到相邻的内存空间中:

func deferproc(siz int32, fn *funcval) {
        sp := getcallersp()
        argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
        callerpc := getcallerpc()
​
        d := newdefer(siz)
        if d._panic != nil {
                throw("deferproc: d.panic != nil after newdefer")
        }
        d.fn = fnd.pc = callerpcd.sp = spswitch siz {
        case0:
        case sys.PtrSize:
                *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
        default:
                memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
        }
​
        return0()
}

runtime._defer 结构体定义如下:

type _defer struct {
        siz       int32// 是参数和结果的内存大小
        started   bool
        openDefer bool// 表示当前 defer 是否经过开放编码的优化
        sp        uintptr// 当前调用栈的栈顶指针
        pc        uintptr// 调用方的程序计数器
        fn        *funcval // defer 关键字中传入的函数
        _panic    *_panic
        link      *_defer
}

runtime._defer 结构体是延迟调用链表上的一个元素,所有的结构体都会通过 link 字段串联成链表(先进后出)。

runtime.deferreturn

runtime.deferreturn 会从 Goroutine 的 defer 链表中取出最前面的 runtime. defer 并调用 runtime.jmpdefer 传入需要执行的函数和参数:

func deferreturn() {
    gp := getg()
    for {
       d := gp._defer
       if d == nil {
          return
       }
       sp := getcallersp()
       if d.sp != sp {
          return
       }
       if d.openDefer {
          done := runOpenDeferFrame(d)
          if !done {
             throw("unfinished open-coded defers in deferreturn")
          }
          gp._defer = d.link
          freedefer(d)
          // If this frame uses open defers, then this
          // must be the only defer record for the
          // frame, so we can just return.
          return
       }
​
       fn := d.fn
       d.fn = nil
       gp._defer = d.link
       freedefer(d)
       fn()
    }
}

在执行defer时都会判断caller栈顶指针是否 defer 结构体中sp的相等,如果相等说明defer是由这个caller创建的,所以可以执行。

defer的优化

  1. 栈上分配:Go 1.13 中对 defer 关键字进行了优化,当该关键字在函数体中最多执行一次时,编译期间的 cmd/compile/internal/gc.state.call 会将结构体分配到栈上,将 defer 关键字的额外开销降低~30%。
  2. 开放编码: Go 语言在 1.14 中通过开放编码(Open Coded)实现 defer 关键字,该设计使用代码内联优化 defer 关键的额外开销并引入函数数据 funcdata 管理 panic 的调用(3),该优化可以将 defer 的调用开销从 1.13 版本的 ~35ns 降低至 ~6ns 左右,开放编码只会在满足以下的条件时启用:
    1. 函数的 defer 数量少于或者等于 8 个;
    2. 函数的 defer 关键字不能在循环中执行;
    3. 函数的 return 语句与 defer 语句的乘积小于或者等于 15 个;

panic 和 recover

panic 和 recover的使用

  1. panic 能够改变程序的控制流,调用 panic 后会立刻停止执行当前函数的剩余代码,并在当前 Goroutine 中递归执行调用方的 defer;
  2. recover 可以中止 panic 造成的程序崩溃。它是一个只能在 defer 中发挥作用的函数,在其他作用域中调用不会发挥作用;

注意:

跨协程无效

panic 只会触发当前 Goroutine 的延迟函数调用,我们可以通过如下所示的代码了解该现象:

func main() {
        defer println("in main")
        go func() {
                defer println("in goroutine")
                panic("")
        }()
​
        time.Sleep(1 * time.Second)
}
​
$ go run main.go
in goroutine
panic:
...

recover正确的使用姿势

recover 只有在 defer 调用的函数中才有效,defer函数外或者defer函数和recover之间还存在调用

// 无效的
func main() {
        defer fmt.Println("in main")
        if err := recover(); err != nil {
                fmt.Println(err)
        }
​
        panic("unknown err")
}
​
$ go run main.go
in main
panic: unknown err
​
goroutine 1 [running]:
main.main()
        ...
exit status 2
// 无效的
func main() {
        deferfunc(){
            fmt.Println("in main")
            
            dorecover()
        }()
​
        panic("unknown err")
}
​
func dorecover() {
    if err := recover(); err != nil {
            fmt.Println(err)
    }
}
​
$ go run main.go
in main
panic: unknown err
​
goroutine 1 [running]:
main.main()
        ...
exit status 2
// 有效的
func main() {
        deferfunc(){
            fmt.Println("in main")
            if err := recover(); err != nil {
                    fmt.Println(err)
            }
        }()
        
        panic("unknown err")
}
​
$ go run main.go
in main
unknown err
// 有效的
func main() {
        defer dorecover()
        
        panic("unknown err")
}
​
​
func dorecover() {
    fmt.Println("in main")
    if err := recover(); err != nil {
            fmt.Println(err)
    }
}
​
$ go run main.go
in main
unknown err

panic和defer可以嵌套

func main() {
        defer fmt.Println("in main")
        deferfunc() {
                deferfunc() {
                        panic("panic again and again")
                }()
                panic("panic again")
        }()
​
        panic("panic once")
}
​
$ go run main.go
in main
panic: panic once
        panic: panic again
        panic: panic again and again
​
goroutine 1 [running]:
...
exit status 2

panic 和 recover底层原理

程序崩溃

编译器会将关键字 panic 转换成 runtime.gopanic,该函数的执行过程包含以下几个步骤:

  1. 创建新的 runtime._panic 并添加到所在 Goroutine 的 _panic 链表的最前面;
  2. 在循环中不断从当前 Goroutine 的 defer 中链表获取 runtime. defer 并运行延迟调用函数;
  3. 判断 panic 是否recover
  4. 调用 runtime.fatalpanic 中止整个程序;
func gopanic(e interface{}) {
        gp := getg()
        ...
        
        // 创建新的 runtime._panic 并添加到所在 Goroutine 的 _panic 链表的最前面
        var p _panic
        p.arg = e
        p.link = gp._panic
        gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
​
        // 在循环中不断从当前 Goroutine 的 _defer 中链表获取 runtime._defer 并运行延迟调用函数;
        for {
                d := gp._defer
                if d == nil {
                        break
                }
​
                d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
                
                // 执行延迟函数
                done := true
                if d.openDefer {
                    done = runOpenDeferFrame(d)
                    if done && !d._panic.recovered {
                       addOneOpenDeferFrame(gp, 0, nil)
                    }
                } else {
                    p.argp = unsafe.Pointer(getargp())
                    d.fn()
                }
​
                d._panic = nil
                d.fn = nil
                gp._defer = d.link
​
                freedefer(d)
                
                // 判断 panic 是否recover
                if p.recovered {
                        ...
                }
        }
​
        // 中止整个程序
        fatalpanic(gp._panic)
        *(*int)(nil) = 0
}
崩溃恢复

编译器会将关键字 recover 转换成 runtime.gorecover:

func gorecover(argp uintptr) interface{} {
        gp := getg()
        p := gp._panic
        if p != nil && !p.recovered && argp == uintptr(p.argp) {
                p.recovered = true
                return p.arg
        }
        returnnil
}

该函数的实现很简单,如果当前 Goroutine 没有调用 panic,那么该函数会直接返回 nil,这也是崩溃恢复在非 defer 中调用会失效的原因。在正常情况下,它会修改 runtime._panic 的 recovered 字段,runtime.gorecover 函数中并不包含恢复程序的逻辑,程序的恢复是由 runtime.gopanic 函数负责的:

func gopanic(e interface{}) {
        ...
​
        for {
                // 执行延迟调用函数,可能会设置 p.recovered = true
                ...
​
                pc := d.pc
                sp := unsafe.Pointer(d.sp)
​
                ...
                if p.recovered {
                        gp._panic = p.link
                        for gp._panic != nil && gp._panic.aborted {
                                gp._panic = gp._panic.link
                        }
                        if gp._panic == nil {
                                gp.sig = 0
                        }
                        gp.sigcode0 = uintptr(sp)
                        gp.sigcode1 = pc
                        mcall(recovery)
                        throw("recovery failed")
                }
        }
        ...
}

从 runtime._defer 中取出了程序计数器 pc 和栈指针 sp 并调用 runtime.recovery 函数触发 Goroutine 的调度,调度之前会准备好 sp、pc 以及函数的返回值:

func recovery(gp *g) {
        sp := gp.sigcode0
        pc := gp.sigcode1
​
        gp.sched.sp = sp
        gp.sched.pc = pc
        gp.sched.lr = 0
        gp.sched.ret = 1
        gogo(&gp.sched)
}

小结

  1. 编译器会负责做转换关键字的工作;
    1. 将 panic 和 recover 分别转换成 runtime.gopanic 和 runtime.gorecover;
    2. 将 defer 转换成 runtime.deferproc 函数;
    3. 在调用 defer 的函数末尾调用 runtime.deferreturn 函数;
  2. 在运行过程中遇到 runtime.gopanic 方法时,会从 Goroutine 的链表依次取出 runtime._defer 结构体并执行;
  3. 如果调用延迟执行函数时遇到了 runtime.gorecover 就会将 _panic.recovered 标记成 true 并返回 panic 的参数;
    1. 在这次调用结束之后,runtime.gopanic 会从 runtime._defer 结构体中取出程序计数器 pc 和栈指针 sp 并调用 runtime.recovery 函数进行恢复程序;
    2. runtime.recovery 会根据传入的 pc 和 sp 跳转回 runtime.deferproc;
    3. 编译器自动生成的代码会发现 runtime.deferproc 的返回值不为 0,这时会跳回 runtime.deferreturn 并恢复到正常的执行流程;
  4. 如果没有遇到 runtime.gorecover 就会依次遍历所有的 runtime._defer,并在最后调用 runtime.fatalpanic 中止程序、打印 panic 的参数并返回错误码 2;

for range

range for slice

下面的注释解释了遍历slice的过程:

// The loop we generate:
//   for_temp := range
//   len_temp := len(for_temp)
//   for index_temp = 0; index_temp < len_temp; index_temp++ {
//           value_temp = for_temp[index_temp]
//           index = index_temp
//           value = value_temp
//           original body
//   }

遍历slice前会先获以slice的长度len_temp作为循环次数,循环体中,每次循环会先获取元素值,如果for-range中接收index和value的话,则会对index和value进行一次赋值。

由于循环开始前循环次数就已经确定了,所以循环过程中新添加的元素是没办法遍历到的。

另外,数组与数组指针的遍历过程与slice基本一致,不再赘述。

range for map

下面的注释解释了遍历map的过程:

// The loop we generate:
//   var hiter map_iteration_struct
//   for mapiterinit(type, range, &hiter); hiter.key != nil; mapiternext(&hiter) {
//           index_temp = *hiter.key
//           value_temp = *hiter.val
//           index = index_temp
//           value = value_temp
//           original body
//   }

遍历map时没有指定循环次数,循环体与遍历slice类似。由于map底层实现与slice不同,map底层使用hash表实现,插入数据位置是随机的,所以遍历过程中新插入的数据不能保证遍历到。

range for channel

遍历channel是最特殊的,这是由channel的实现机制决定的:

// The loop we generate:
//   for {
//           index_temp, ok_temp = <-range
//           if !ok_temp {
//                   break
//           }
//           index = index_temp
//           original body
//   }

channel遍历是依次从channel中读取数据,读取前是不知道里面有多少个元素的。如果channel中没有元素,则会阻塞等待,如果channel已被关闭,则会解除阻塞并退出循环。

注:

  • 上述注释中index_temp实际上描述是有误的,应该为value_temp,因为index对于channel是没有意义的。
  • 使用for-range遍历channel时只能获取一个返回值。

参考

draveness.me/golang/docs…

books.studygolang.com/GoExpertPro…