go源码阅读 slice

slice

对于slice来说,其实源码部分并不多,主要看一下make slicegrowsliceslicecopycopy

编译阶段

make slice语句跟map一样,在compiletypecheck阶段,将MAKE节点偷偷换成OMAKESLICE节点

关于oppend的代码实在/usr/local/go/src/cmd/compile/internal/gc/ssa.gofunc (s *state) append(n *Node, inplace bool),这个是编译时代码

关于slice的代码在/usr/local/go/src/runtime/slice.go ,这部分是运行时代码

type slice struct
type slice struct {  
   array unsafe.Pointer
   len   int
   cap   int
}

array就是slice底层对应数组的内存地址,所有结构体的数据都是连续排列在这的,

len当前的slice数据大小

cap当前的slice数据容量

makelist
func makeslice(et *_type, len, cap int) unsafe.Pointer {  
   mem, overflow := math.MulUintptr(et.size, uintptr(cap))
   if overflow || mem > maxAlloc || len < 0 || len > cap {
      // NOTE: Produce a 'len out of range' error instead of a
      // 'cap out of range' error when someone does make([]T, bignumber).
      // 'cap out of range' is true too, but since the cap is only being
      // supplied implicitly, saying len is clearer.
      // See golang.org/issue/4085.
      mem, overflow := math.MulUintptr(et.size, uintptr(len))
      if overflow || mem > maxAlloc || len < 0 {
         panicmakeslicelen()
      }
      panicmakeslicecap()
   }

   return mallocgc(mem, et, true)
}

这里面其实lencap都是默认传入进来的,实际make slice只返回分配的数组内存地址,而lencap还是结构体自己去记录的

growslice
func growslice(et *_type, old slice, cap int) slice {  
    // 这个 cap是即将要分配cap大小,例如
    //     s0 := make([]int, 2, 1)
    // s0 = append(s0, 5, 6, 7, 8, 9)
    // 那么这个时候生成调用代码growslice的参数cap就是 2+5=7
    // 使用gdb 在 append那一行断点再step,就能看到数据
    if cap < old.cap {
        panic(errorString("growslice: cap out of range"))
    }

    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            newcap = doublecap
        } else {
            // Check 0 < newcap to detect overflow
            // and prevent an infinite loop.
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4
            }
            // Set newcap to the requested cap when
            // the newcap calculation overflowed.
            if newcap <= 0 {
                newcap = cap
            }
        }
    }
    ... 

    return slice{p, old.len, newcap}
}

上面主要是增长方式,记得三种情况就行:

  1. 如果发生append之后的cap是超过原来的一倍,那么按超过的最大cap作为新的cap;否则进行以下逻辑
  2. 如果原来的old cap小于1024(注意这里不是内存,而是容量),那么扩容 1 倍(即原来的2倍);否则
  3. 扩容1.25倍(即原来的2.25倍)
slicecopy
func slicecopy(to, fm slice, width uintptr) int {  
   ...

   size := uintptr(n) * width
   if size == 1 { // common case worth about 2x to do here
      // TODO: is this still worth it with new memmove impl?
      *(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
   } else {
      memmove(to.array, fm.array, size)
   }
   return n
}

这里面的memmove跟机器硬件指令有关(优化),都是使用汇编完成的;

有意思的是如果需要copy的数据是 1 字节,是直接赋值这个字节

append
func (s *state) append(n *Node, inplace bool)  

这个里面inplace代表了两种使用append的方式

If inplace is false, process as expression "append(s, e1, e2, e3)"  
If inplace is true, process as statement "s = append(s, e1, e2, e3)"  

不过inplace==false在我现在的版本go version go1.12.5 linux/amd64会编译错误

编译器希望我们处理一下append的返回值

函数参数
func f(s1 []int) {  
    // s1[0] = 200
    s1 = append(s1, 5, 6, 7, 8, 9)

    fmt.Printf("f s1=%v\n", s1)
}

func main() {  
    s0 := make([]int, 1, 2)
    s0[0] = 10

    f(s0)

    fmt.Printf("main s0=%v\n", s0)
}
// output
f s1=[10 5 6 7 8 9]  
main s0=[10]  

可以理解为,f(s0 []int)其实调用是长这样的f(array *int, len int, cap int),如果在函数f修改s1切片中的某个值(例如上面的注释)是可以变动mains0;

但是类似append这种(如果cap不够导致触发growslice例外),可能会导致返回新的array指针,就形成了局部变量,不会影响main中的s0

数组

函数内部的数组(函数外部会保存在数据段),类似只有切片结构的array字段,只有一个指针而已(指向一块分配好的内存),所以不能改变大小

// main.go
  1 package main
  2 
  3 import (
  4     "fmt"
  5 )
  6 
  7 func main() {
  8     array := [3]int{1, 2, 3}
  9     slice := array[:2]
 10     fmt.Printf("main s0=%v %v\n", array, slice)
 11 }

go build -gcflags="all=-N -l" -o main main.go  
gdb main  
断点下在 第 10 行,
(gdb) info locals
&array = 0xc000016540
slice = {array = 0xc000016540, len = 2, cap = 3}  

参考

slice 和 array