Go 中数组、字符串和切片在底层原始数据有着相同的内存结构,在上层因为语法限制有不同的行为表现。

Go 语言数组是一种值类型,虽然数组的元素可以被修改,但是数组本身的赋值和函数传参都是以整体复制的方式处理的。

Go 语言字符串底层数据也是对应的字节数组,但是字符串的只读属性禁止了在程序中对底层字节数组的元素的修改。字符串赋值只是复制了数据地址和对应的长度,而不会导致底层数据的复制。

切片的行为更为灵活,切片的结构和字符串结构类似,但是解除了只读限制。切片的底层数据虽然也是对应数据类型的数组,但是每个切片还有独立的长度和容量信息,切片赋值和函数传参数时也是将切片头信息部分按传值方式处理。因为切片头含有底层数据的指针,所以它的赋值也不会导致底层数据的复制。

其实 Go 语言的赋值和函数传参规则很简单,除了闭包函数以引用的方式对外部变量访问之外,其它赋值和函数传参数都是以传值的方式处理。

字符串

type StringHeader struct {
    Data uintptr // 指向的底层字节数组
    Len  int // 字符串字节长度
}

Untitled

切片

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

切片高效操作的要点是要降低内存分配的次数,尽量保证 append 操作不会超出 cap  的容量,降低触发内存分配的次数和每次分配内存大小

函数

func Inc() (v int) {
    defer func(){ v++ } ()
    return 42
}

其中 defer 语句延迟执行了一个匿名函数,因为这个匿名函数捕获了外部函数的局部变量 v,这种函数我们一般叫闭包。闭包对捕获的外部变量并不是传值方式访问,而是以引用的方式访问。

闭包的这种引用方式访问外部变量的行为可能会导致一些隐含的问题:

func main() {
    for i := 0; i < 3; i++ {
        defer func(){ println(i) } ()
    }
}
// Output:
// 3
// 3
// 3

因为是闭包,在 for 迭代语句中,每个 defer 语句延迟执行的函数引用的都是同一个 i 迭代变量,在循环结束后这个变量的值为 3,因此最终输出的都是 3。

修复的思路是在每轮迭代中为每个 defer 函数生成独有的变量