Value copy

In GO,everything we assign is a copy.

变量的值到底是什么?

  • 值类型,string,array,struct,是数据本身;
  • 引用类型,slice,map,channel,pointer,function,是数据的引用——但仍是值拷贝,只是这个值中带着地址。

所以本质上二者都是值拷贝。

值拷贝的意义:

  • 值传递意味着数据可以被分配到上,函数返回时,栈上的数据立即销毁,无需 GC;
  • 指针数据分配在堆上,由 GC 管理
  • 要践行通过通信共享内存,而不是共享内存来通信,传递值的副本可以避免数据竞争

遍历一个很大的 slice 时,除了直接使用索引以外,还可以将 slice 变为指针类型,避免拷贝开销。(但是遍历指针类型对 CPU 来说效率低)

循环控制器的求值时机

The provided expression is evaluated only once,before the beginning of the loop.

只在循环开始前求一次值,之后会使用其副本进行迭代。

s := []int{0, 1, 2}
for range s { // 不需要索引和值,只要按照长度循环相应的次数即可。
    s = append(s, 10)
}
  • 先进行拷贝,得到的s_copy作为循环控制器
  • 后续操作,s 在不断变化,但是循环控制器始终不变,一直是开始的那个拷贝副本,所以只会执行三次。

为什么要有一个始终不变的循环控制器呢?

为了保证循环的可预测性,防止意外的无限循环,减少对应的性能开销

举一个无限循环的错误示范:

s := []int{0,1,2}
for i := 0; i < len(s); i++ {
    s = append(s, 10)
}

这会导致无限循环,因为 s 的长度一直在增长。

channels 和 Arrays 也有类似的行为,具体见下方所示:

Channels

同样的,在处理多个 channel 时也会遇到类似问题。

ch1 := make(chan int, 3)
go func() {
    ch1 <- 0
    ch1 <- 1
    ch1 <- 2
    close(ch1)
}()

ch2 := make(chan int, 3)
go func() {
    ch2 <- 10
    ch2 <- 11
    ch2 <- 12
    close(ch2)
}()

ch := ch1
for v := range ch {
    fmt.Println(v)
    ch = ch2
}

这里打印出的 v 一直是 ch1,因为只在开始时确定一次。

一开始就确定了一个循环控制器,这样以后在 range 中修改也不会修改定住的这个控制器。

日常开发中通常会采用 select 语句来处理多个动态数据。

Array

a := [3]int{0,1,2}
for i, v := range a {
    a[2] = 10
    if i == 2 {
        fmt.Println(v)
    }
}

这段代码最终会输出 2 还是 10 呢?答案是 2,循环中的 v 来自于拷贝,而只有a[i]访问的是原始的数组。

与最开始的 slice 类似,都有拷贝作为循环控制器:

  • 一开始求值得到一个拷贝副本值a_copy,接下来的循环都将在a_copy上进行
  • i,v 都来源于a_copy
  • 明确使用变量名a[2]才是使用原数组

pointer

当结构体较大时,建议使用指针存入 map,否则会带来两个严重的后果:

  • 只是操作副本
  • 大的结构体在 map 存取过程中会涉及到数据拷贝

所以使用指针 mapPointer map[string]*LargeStruct,可以直接对结构体内的属性值进行操作mapPointer[id].foo = "bar"

在 Go1.22 之前,以下代码会出现循环变量的复用问题:

for _, customer := range customers {
    s.m[customer.ID] = &customer
}
  • 循环变量的复用:customer只有一个实例,每次只操作这一个实例。
  • 循环赋值
  • 获取被复用的地址

如何解决这个问题:

  • 每次循环创建一个新的局部变量
  • 通过索引获取原始切片的元素地址,而不是循环变量的地址

在 Go1.22 之后,修改了 for 循环的语义,让每次循环背后都做了一次局部变量声明操作:

customer := customers[i]

这样就不会出现循环变量的复用问题了。

在 Java 中 for-each 循环每次都是一个新的变量。

对比 Java

可以看到,首先 Go 中一切皆值拷贝,而 Java,对象变量存的就是引用,拷贝的是引用的值——地址

在传参过程中,Java 传递的是引用的值(地址),这样不会带来拷贝的开销。