for 和 range

概述

for 循环可以将数据与逻辑分离,使得多份数据可以使用一份相同的逻辑,减少代码量,提高代码的复用性。

经典循环

  1. for Ninit; Left; Right {
  2. NBody
  3. }

循环范围

切片和数组

● for-range 的控制结构最终会被编译器替换为 for 循环。

● 使用 for-range 方式清空切片的过程会被优化,会根据头指针和长度计算出占用的内存,直接清空内存,并且最后修改索引。

● 遍历切片时,在遍历之前循环次数已经根据切片长度确定,因此,在循环体内向切片插入数据不会改变执行次数。

for v1, v2 = range a中,go 语言会额外创建新的变量存储切片中的元素,即v2 的值在每一次迭代之后都会改变。

哈希表

● 首先随机选出一个正常通开始遍历,然后遍历其所有的溢出桶,最后按照索引顺序遍历哈希表中的其他桶。

字符串

● 使用 for-range 类似使用下表访问数组得到的是字节,但是此处遍历字符串类型时,会将字节转换为 rune 类型,还会根据当前 rune 类型会用不同的方式解码。

channel

for-range接收者只有一个。

select

概述

能够让多个 goroutine 同时等待多个 Channel 可读或可写。在多个文件或者 Channel 状态改变之前,select 会一直阻塞当前线程或者 goroutine 。当多个 case 同时被触发时,会随机执行一个。

select 开始执行前,将所有的 case 语句按照先后出现的次序执行一遍,此时位于 case 等号左边的语句不会被执行,如果 case 左侧是一个接受值的表达式,那么在其接受值前才会被表达。

现象

非阻塞收发

通常情况下,select 会阻塞当前 Goroutine ,并等待多个 channel 中的一个达到可收发的状态,但是如果 select 中包含 default 语句,那么该 select 语句执行时有两种情况:

● 当存在可以收发的 channel 语句时,直接处理该 channel 对应的 case

● 不存在可以收发的 channel 时,执行 default 中的语句。

随机执行

● 避免饥饿问题,如果按照顺序依次执行,那么后边的条件可能永远得不到执行。

数据结构

type scase struct {
    c         *hchan                //chan
    elem     unsafe.Pointer        //data element

因为 case 中都与 channel 的接收和发送有关,所以 runtime.scase 结构体中也包含一个 runtime.hchan 类型的字段存储 case 中使用的 channel。

实现原理

常见流程

● 将所有 case 转换成包含 channel 以及类型等信息的 runtime.scase 结构体。

● 调用运行时函数 runtime.selectgo 从多个准备就绪的 channel 中选择一个可执行的 runtime.scase 结构体。

● 通过 for 循环生成一组 if 语句,在语句中判断自己是否为被选中的 case

小结

首先在编译期间对 select 语句进行优化,会根据其中的 case 的不同选择不同的优化方式:

● 空select 转化为 runtime.block 挂起当前 goroutine

● 如果只有一个 case 首先编译器将其转换为 if 格式,然后判断操作的 channel 是否为空,然后执行 case 结构中的内容。

● 如果 select 中有两个 case 且其中一个是 default ,那么会使用 runtime.selectnbrecvruntime.selectnbsend 非阻塞地执行收发操作。

● 默认情况下会通过 runtime.selectgo 获取执行 case 的索引,并通过多条 if 语句执行对应 case 中的代码。

编译器执行完优化之后,go 语言会在运行时执行编译期间展开的 runtime.selectgo 函数,该函数按照:

● 随机生成一个遍历的轮询顺序 pollOrder 并根据 channel 地址生成加锁程序 lockOrder

● 根据 poolOrder 遍历所有 case 查看是否有可以立即返回的 channel

▶ 存在,直接获取 case 对应的索引并返回

▶ 不存在,创建 runtime.sudog 结构体,将当前 goroutine 加入所有关于 channel 的收发队列,并调用 runtime.gopark 挂起当前 goroutine 等待调度器调度。

● 当调度器唤醒当前 goroutine 时,会在此按照 lockOrder 遍历所有 case 从中查找需要被处理的 runtime.sudog 对应的索引。

defer

概述

go 语言的 defer 会在函数返回之前执行传入的函数,通常用于关闭文件描述符,关闭数据库连接,以及解锁资源。

现象

使用时常见的两个问题:

● defer 关键字的调用时机以及多次调用 defer 时执行顺序是如何确定的。

● defer 关键字使用传值方式传递参数时会进行预计算,这会导致结果不符合预期。

作用域

向 defer 关键字传入的函数会在函数返回之前运行。

简单举例:

func main() {
    for i := 0; i < 6; i++ {
        defer fmt.Print(i, "  ")
    }
}
//5  4  3  2  1  0  

func main() {
    {
        defer fmt.Println("defer up")
        fmt.Println("block ends")
    }
    defer fmt.Println("defer down")
    fmt.Println("main ends")
}
//block ends
//main ends
//defer down
//defer up

预计算参数

go 语言中函数都是传值的,虽然 defer 是关键字,但也继承了这个特点。

//t "time"
func main() {
    start := t.Now()
    defer fmt.Println(t.Since(start))
    t.Sleep(t.Second)
}
//0s

func main() {
    start := t.Now()
    defer fmt.Println(t.Since(start))
    t.Sleep(t.Second)
    defer fmt.Println(t.Since(start))
    t.Sleep(t.Second)
    defer fmt.Println(t.Since(start))
    t.Sleep(t.Second)
    defer fmt.Println(t.Since(start))
    t.Sleep(t.Second)
    defer fmt.Println(t.Since(start))
}
//4.0384851s
//3.0303726s
//2.0287069s
//1.0110677s
//0s
defer 关键字会立即复制函数中引用的外部参数,所以 `t.Since(start)` 的结果不是在 `main` 函数退出之前计算的,而是在 defer 关键字调用时计算的。

为解决这个问题,可以向 defer 关键字中传入匿名函数:

func main() {
    start := time.Now()
    defer func() { fmt.Println((time.Since(start))) }()

    time.Sleep(time.Second)
}
//1s

虽然是值传递,但是此处复制的是函数的指针,所以 time.Since(start) 会在 main 函数返回前调用并打印符合预期的结果。

数据结构

源码中的数据结构:

type _defer struct {
    siz             int32 
    started         bool
    heap            bool
    openDefer         bool
    sp                uintptr  // sp at time of defer
    pc                uintptr  // pc at time of defer
    fn                *funcval // can be nil for open-coded defers
    _panic            *_panic  // panic that is running defer
    link             *_defer
    fd               unsafe.Pointer
    varp             uintptr     
    framepc         uintptr
}

_defer 是延时调用链表上的一个元素,所有结构体会通过 link 字段串成一个链表。image.png
siz 参数和结果内存的大小。

sppc 分别代表栈指针和调用方的程序计数器。

fn 是defer 关键字中传入的函数。

_panic 是触发延时调用的结构体,可能为空。

openDefer 表示当前 defer 是否经过开放编码优化。

解答开篇现象

● 后调用的函数会先执行

▶ 后调用的 defer 函数会被追加到 Goroutine_defer 链表的最前面

▶ 运行 runtime._defer 时是从链表前往后执行的

● 会预先计算函数的参数

▶ 如果调用 runtime.deferproc 函数创建新的延时调用,就会立刻复制函数的参数,函数的参数不会等到真正执行时计算。

panic 和 recover

概述

这两个关键字与 defer 有很大的关系:

● panic 之后触发当前 goroutine 的 defer。

● recover 只有在 defer 中才会有效。

● panic 允许在 defer 中嵌套多次调用。

跨协程失败

panic 只会触发当前 goroutine 的延时函数调用。

func main() {
    defer println("in main")

    go func() {
        defer println("in goroutine")
        panic("")
    }()

    time.Sleep(time.Second)
}
//in goroutine
//panic:

//goroutine 5 [running]:
//main.main.func1()
//        F:/js/main.go:10 +0x7b
//created by main.main
//        F:/js/main.go:8 +0x76
//exit status 2

可见 main 函数中的 defer 语句并没有执行,执行的只有当前goroutine 中的 defer。

defer 关键字对应的 runtime.deferproc 会将延时调用函数和调用方所在的 goroutine 关联起来,所以程序发生崩溃时,只会调用当前 goroutine 中的延时调用函数,保障协程之间互不影响。

失效的崩溃恢复

func main() {
    defer println("in main")
    if err := recover(); err != nil {
        fmt.Println("error")
    }
    panic("unknow err")
}
//in main
//panic: unknow err

//goroutine 1 [running]:
//main.main()
//       F:/js/main.go:12 +0x8a
//exit status 2

原因:recover 只有发生在 panic 之后才会生效,然而上述代码中,recover 是在 panic 函数之前的,不满足生效的条件,而且 panic 之后程序崩溃,直接将 recover 写在 panic 之后永远不会被执行,所以只能将 recover 写在 defer 中。

崩溃嵌套

程序多次调用 panic 也不会影响 defer 函数的正常执行,所以使用 defer 进行收尾工作一般来说是安全的。

数据结构

type _panic struct {
    argp      unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
    arg       interface{}    // argument to panic
    link      *_panic        // link to earlier panic
    pc        uintptr        // where to return to in runtime if this panic is bypassed
    sp        unsafe.Pointer // where to return to in runtime if this panic is bypassed
    recovered bool           // whether this panic is over
    aborted   bool           // the panic was aborted
    goexit    bool
}
● `argp` 指向 defer 调用时参数的指针。

arg 调用 panic 是传入的参数。

link 指向了更早调用的 runtime.panic 结构,同时可推断,panic 函数可以被多次调用,他们之间通过 link 可以组成链表。

recovered 表示当前 runtime._panic 是否被 recover 恢复。

aborted 表示当前 panic 是否被强行终止。

pc, sp, goexit 都是为了修复 runtime.Goexit 带来的问题引入的。runtime.Goexit只能结束调用该函数的 goroutine,而不影响其他的 goroutine,但该函数会被 defer 中的 panic 和 recover 取消,引入这三个字段是为了保证函数一定会生效。

程序崩溃

编译器将关键字 panic 转化为runtime.gopanic,该函数执行分为以下几个步骤:

● 创建新的 runtime._panic 并添加到 所在 goroutine 的 _panic 链表的最前面。

● 在循环中不断从当前的 goroutine 的 defer 列表中获取 runtime._defer,并调用 runtime.reflectcall 运行延时调用函数。

● 运用 runtime.fatalpanic 终止整个程序。

崩溃恢复

调用函数 runtime.gorecover 修改字段,将 runtime._panicrecovered 的字段修改为 true 。之后调用函数 runtime.gopanic ,从 runtime._defer 中取出程序计数器 pc 和栈指针 sp,并调用 runtime.recovery 函数触发 goroutine 的调度,该函数在在调度的过程中会将返回值设置成1,此时编译器生成的代码会直接跳转到调用方函数返回之前并执行 runtime.deferreturn

跳转到此函数以后,程序就从 panic 中恢复并执行正常的逻辑,而 runtim.recovere 函数也能从 runtime._panic 结构中取出调用 panic 时传入的 arg 参数并返回给调用方。

make 和 new

概述

make 的作用是初始化内置的数据结构,也就是切片,数组,哈希表,channel。

▶ slice 是一个包含 data,cap,len 的结构体 reflect.SliceHeader

▶ hash 是一个指向 runtime.hmap 的结构体指针。

▶ ch 是一个指向 runtime.hchan 结构体的指针。

new 的作用是根据传入的类型分配一块内存空间,并返回指向这块内存空间的指针。

new

编译器会在中间代码生成阶段通过两个函数处理该关键字,一个将关键字类型转化为 ONEWOBJ 类型的节点,另一个会根据申请空间的大小分两种情况来处理:

● 申请空间为0,返回一个表示空指针的 zerobase 变量。

● 其他情况下会将关键字转换成 runtime.newobject 函数。

/