第五章笔记总结

Kesa...大约 8 分钟golang

1. for and range

1.1 for

for 循环被编译器视作OFOR节点,由四个部分组成:

  1. Ninit:循环初始化
  2. Left:循环执行条件
  3. Right:循环体结束执行语句
  4. NBody:循环体
for Ninit; Left; Right {
    NBody
}
golang-for-loop-ssa
golang-for-loop-ssa

1.2 for-range

编译期会将for-rangeORANGE节点换成OFOR也就是普通for

Golang-For-Range-Loop
Golang-For-Range-Loop

数组和切片

使用for-range遍历数组和切片,会拷贝原始切片,若在循环中修改切片的长度,不会改变循环次数。

遍历数组和切片有四种情况:

  1. 遍历数组和切片清空元素
  2. 使用 for range a {} 遍历数组和切片
  3. 使用 for i := range a {} 遍历数组和切片
  4. 使用 for i, elem := range a {} 遍历数组和切片
遍历并清空数组和切片
// 原代码
for i := range a {
	a[i] = zero
}

// 优化后
if len(a) != 0 {
	hp = &a[0]
	hn = len(a)*sizeof(elem(a))
	memclrNoHeapPointers(hp, hn)
	i = len(a) - 1
}

代码会被优化成使用runtime.memclrNoHeapPointersopen in new windowruntime.memclrHasPointersopen in new window直接清除内存区域的数据。

for range a {}
// 原代码
for range a {
    ...
}

// 优化之后
ha := a
hv1 := 0
hn := len(ha)
v1 := hv1
for ; hv1 < hn; hv1++ {
    ...
}

遍历时,拷贝原始切片的变量,遍历次数是新变量的长度。

for i := range a {}
// 原代码
for i := range a {
    ...
}

// 优化之后
ha := a
hv1 := 0
hn := len(ha)
v1 := hv1
for ; hv1 < hn; hv1++ {
    v1 = hv1
    ...
}
for i, elem := range a {}
// 原代码
for i, elem := range a {
    ...
}

// 优化之后
ha := a
hv1 := 0
hn := len(ha)
v1 := hv1
v2 := nil
for ; hv1 < hn; hv1++ {
    tmp := ha[hv1]
    v1, v2 = hv1, tmp
    ...
}

遍历时的elem变量的地址不变,每次循环都会重新赋值。

哈希表

for-range遍历哈希表有三种不同形式:

  1. for range m {}
  2. for k := range m{}
  3. for k, v := range m{}
golang-range-map
golang-range-map

遍历哈希表时,桶的选择是随机的

func mapiterinit(t *maptype, h *hmap, it *hiter) {
	it.t = t
	it.h = h
	it.B = h.B
	it.buckets = h.buckets

	r := uintptr(fastrand())
	it.startBucket = r & bucketMask(h.B)
	it.offset = uint8(r >> h.B & (bucketCnt - 1))
	it.bucket = it.startBucket
	mapiternext(it)
}

使用了runtime.fastrandopen in new window来随机选取桶的索引。

遍历哈希表的顺序:

  1. 先遍历正常桶及其溢出桶
  2. 然后遍历其他位置的桶
golang-range-map-and-buckets
golang-range-map-and-buckets

字符串

for-range遍历字符串有三种形式:

  1. for range s{}
  2. for i := range s{}
  3. for i, c := range s{}

当使用for i, c := range s{}时:

ha := s
for hv1 := 0; hv1 < len(ha); {
    hv1t := hv1
    hv2 := rune(ha[hv1])
    if hv2 < utf8.RuneSelf {
        hv1++
    } else {
        hv2, hv1 = decoderune(ha, hv1)
    }
    v1, v2 = hv1t, hv2
}

c的是字符串的一个字符,类型为rune

channel

for-range遍历channel有两种形式:

  1. for range ch{}
  2. for v := range ch{}

使用for v := range ch{}会转换成:

ha := a
hv1, hb := <-ha
for ; hb != false; hv1, hb = <-ha {
    v1 := hv1
    hv1 = nil
    ...
}

for-range会循环至通道被关闭。

2. select

select用于监听多个channel是否可用:

  1. 若无可用通道,则阻塞当前goroutine
  2. 若有多个,则随机选择一个分支执行
  3. 若存在default分支,则为非阻塞结构,无可用通道将直接执行default分支
Golang-Select-Channels
Golang-Select-Channels

2.1 数据结构

select无数据结构表示,但case分支可用runtime.scaseopen in new window表示:

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

2.2 实现原理

select语句会在编译期间转换成OSELECT节点,每个OSELECT会持有一组OCASE节点,若OCASE执行条件为空则表示为default

golang-oselect-and-ocases
golang-oselect-and-ocases

编译期

编译期根据select中的case数目不同,会有四种优化情况:

  1. select 不存在任何的 case 此时当前goroutine,将直接阻塞,永久休眠

  2. select 只存在一个 case 转换成if语句:

    // 改写前
    select {
    case v, ok <-ch: // case ch <- v
        ...    
    }
    
    // 改写后
    if ch == nil {
        block()
    }
    v, ok := <-ch // case ch <- v
    ...
    
    • 首先判断操作的 Channel 是不是空的
    • 然后执行 case 结构中的内容
  3. select 存在两个 case,其中一个 casedefault 此时为非阻塞结构,若channel不可用,直接执行default

  4. select 存在多个 case 默认情况下会通过 runtime.selectgoopen in new window 获取执行 case 的索引,并通过多个 if 语句执行对应 case 中的代码

运行时

运行时执行编译期间展开的 runtime.selectgoopen in new window 函数,该函数会按照以下的流程执行:

  1. 随机生成一个遍历的轮询顺序 pollOrder 并根据 Channel 地址生成锁定顺序 lockOrder
  2. 根据 pollOrder 遍历所有的 case 查看是否有可以立刻处理的 Channel:
    1. 如果存在,直接获取 case 对应的索引并返回
    2. 如果不存在,创建 runtime.sudogopen in new window 结构体,将当前 Goroutine 加入到所有相关 Channel 的收发队列,并调用 runtime.goparkopen in new window 挂起当前 Goroutine 等待调度器的唤醒
  3. 当调度器唤醒当前 Goroutine 时,会再次按照 lockOrder 遍历所有的 case,从中查找需要被处理的 runtime.sudogopen in new window 对应的索引

3. defer

defer使用时有三个关键点:

  1. defer的调用时机:当前函数返回
  2. defer的调用顺序:后进先出,后定义的defer先执行
  3. defer的参数:defer的参数会在定义预先进行拷贝,而不是在调用时处理

3.1 数据结构

runtime._deferopen in new window

type _defer struct {
	siz       int32
	started   bool
	openDefer bool
	sp        uintptr
	pc        uintptr
	fn        *funcval
	_panic    *_panic
	link      *_defer
    ...
}

defer函数以链表的形式组织在一起。

3.2 执行机制

defer的执行机制有三种:

4. panic and recover

panicrecover的作用

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

panicrecover的执行有三个要点:

  1. panic 只会触发当前 goroutine 的 defer
  2. recover 只能在 defer 中生效
  3. panic 可以在 defer 中嵌套

4.1 数据结构

runtime._panicopen in new window

type _panic struct {
	argp      unsafe.Pointer
	arg       interface{}
	link      *_panic
	recovered bool
	aborted   bool
	pc        uintptr
	sp        unsafe.Pointer
	goexit    bool
}

panic可以被连续调用,多个panic之间组成链表

4.2 执行流程

panicrecover的流程如下:

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

5. make and new

make 和 new 的作用:

  1. make 的作用是初始化内置的数据结构切片、哈希表和 Channel
  2. new 的作用是根据传入的类型分配一片内存空间返回指向这片内存空间的指针
golang-make-and-new
golang-make-and-new

5.1 make

编译期会将make转换成不同的节点:

golang-make-typecheck
golang-make-typecheck

后序将会调用不同的初始化函数执行。

5.2 new

编译器会在中间代码生成阶段通过以下两个函数处理该关键字:

  1. cmd/compile/internal/gc.callnewopen in new window 会将关键字转换成 ONEWOBJ 类型的节点

  2. cmd/compile/internal/gc.state.expr

    会根据申请空间的大小分两种情况处理:

    1. 如果申请的空间为 0,就会返回一个表示空指针zerobase 变量;
    2. 在遇到其他情况时会将关键字转换成 runtime.newobjectopen in new window 函数,会获取传入类型占用空间的大小,调用 runtime.mallocgcopen in new window 在堆上申请一片内存空间并返回指向这片内存空间的指针

Reference

  1. https://draveness.me/golangopen in new window
上次编辑于:
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.2