1. 问题的提出
赋值时是值语义还是引用语义对程序的行为有很大影响,如果我们想要某个函数对一个变量的修改对外部不可见,我们希望值语义的赋值,反之,若我们想要某个函数对一个变量的修改是全局可见时,我们希望引用语义的赋值。那么Go中的赋值究竟是值语义还是引用语义?
2. Go中的赋值
在Go中大部分类型的赋值是值类型,包括:
- 基本类型,如byte、int、bool、float32、float64和string等;
- 复合类型,如数组(array)、结构体(struct)和指针(pointer)等。
可以看到,数组也是值语义的赋值,因此Go中的值语义赋值表现得非常彻底。
2.1 基本类型的赋值
这个很好理解,传入函数的变量与外部变量的地址不同,这与C中保持一致。
2.2 复合类型的赋值
对于从C转向Go的小伙伴来说,这一部分需要详细叙述。
2.2.1 数组的赋值
Go中的数组是很存粹的值类型,例如:
var a = [3]int{1, 2, 3}
var b = a
b[1]++
fmt.Println(a, b)
程序的运行结果是:
[1 2 3] [1 3 3]
这表明赋值时发送了完整的数组赋值。
2.2.2 map和slice的赋值
这两个数据类型在C中并不存在,它们的赋值同样是值语义,但是有时又会表现出类似引用语义的行为。例如:
var a = make([]int, 3)
a[0] = 0
a[1] = 1
a[2] = 2
var b = a
b[1]++
fmt.Println(a, b)
程序的结果为
[0 2 2] [0 2 2]
这个表现与数组的赋值表现并不一样,实际上可以把切片类型看成下面的结构体:
type slice struct {
first *T
len int
cap int
}
由于a
,b
有值相同值的内部指针T
,因此二者对任一元素的修改都对对方可见。
map
类型可以看成是字典指针,因此对字典添加、删除、修改对二者可见:
type Map_K_V struct {
// ...
}
type map[K]V struct {
impl *Map_K_V
}
3. 一个复杂的例子
看下面的程序,提前想想地址是否有变化
package main
// Tx
type Tx struct {
a int
ns []string
}
// Graph
type Graph struct {
ii int
nums []int
tos map[string] string
tx Tx
}
func foo(gra Graph) {
println(&gra.ii)
println(&gra.nums[0], &gra.nums[1], &gra.nums[2])
println(&gra.tos)
println(&gra.tx)
println(&gra.tx.a)
println(&gra.tx.ns[0], &gra.tx.ns[1])
}
func main() {
gra := Graph{1, make([]int, 3), make(map[string]string), Tx{1, make([]string, 2)}}
gra.tos["1"] = "1"
println(&gra.ii)
println(&gra.nums[0], &gra.nums[1], &gra.nums[2])
println(&gra.tos)
println(&gra.tx)
println(&gra.tx.a)
println(&gra.tx.ns[0], &gra.tx.ns[1])
foo(gra)
}
该程序的执行结果为:
int
类型变量的地址改变- 切片类型的元素地址不变
map
类型变量本身的地址改变- 结构体变量本身地址改变
- 属于内部结构体的变量可以直接看成是外部结构体的变量,因此地址变化仍然满足部分2的规则:
int
类型变量的地址改变,切片类型的元素地址不变。