go-for-range修改原数据
基础知识
值类型&引用类型
go数据结构中包含值类型和引用类型两种:
引用类型:
- slice
- map
- channel
- interface
- function
值类型:其他基本数据结构
区别
基本类型:变量直接存储值,内存通常在栈中分配,栈在函数调用完会被释放
引用类型:变量存储的是一个地址,这个地址存储最终的值。内存通常在 堆上分配。通过 GC 回收。
引用类型的实现中,包含了对底层数据结构的引用,比如slice底层是由数组实现的,一个slice实际上包含了对底层数据的引用。
make&new
new(T):开辟内存,置为0值,返回指针。T可以是各种数据结构。
make:只能传入chan、map 以及 slice,返回原值,非指针
for range中循环变量的坑
众所周知,go中使用for range语法得到的循环变量,并不是原始数据,而是原始数据的副本。因此我们常见的说法是,使用for range语法修改原始数据是无效的。举个例子
type person struct {
name string
age int
}
people := []person{
{name: "Alice", age: 20},
{name: "Bob", age: 25},
}
for _, p := range people {
p.age += 10
}
// age还是20, 25
t.Log(people[0], people[1])
(go 1.21开启了for range循环变量语义的修改测试)
for range+引用类型
但是,当我们for loop中的循环变量结构体中包含引用类型时,对这些引用类型的修改,还是会同步到原始数据中的。从实现原理上易得:引用类型即使复制了副本,但是这些副本存储的都是地址(门牌号),最终找到的数据全是同一份,因此修改任意一个都会影响其他的。
func TestForLoop(t *testing.T) {
type person struct {
name string
age int
data map[string]string
}
people := []person{
{name: "Alice", age: 20, data: map[string]string{"a": "1"}},
{name: "Bob", age: 25, data: map[string]string{"a": "1"}},
}
for _, p := range people {
p.age += 10
p.data["a"] = "2"
p.data["b"] = "3"
}
// age还是20, 25,但是data都被改了
t.Log(people[0], people[1])
}
应用
修改pod中的container
比如在kubernetes的相关开发中,有时候需要对container进行修改,先来看pod中和container相关的定义
// PodSpec is a description of a pod.
type PodSpec struct {
Containers []Container
}
type Container struct {
Name string
Resources ResourceRequirements
}
type ResourceRequirements struct{
Limits ResourceList
Requests ResourceList
}
type ResourceList map[ResourceName]resource.Quantity
可以看到Container的Name是基本类型,而Container的ResourceList是map。因此,如果用for range同时去修改Container中的Name和ResourceList,结果对于pod中原容器的影响是不同的。Name并不会改变,而ResourceList这个map是可以被修改的。
结构定义
已知:
- go中函数参数是传值的
- 指针省空间,效率高
我们看k8s的代码中对数据结构的定义,可以看到slice中放结构体或者map中存结构体时,大多都是直接使用[]T及map[string]T,而不是[]*T及map[string]*T。理解了引用类型的原理,就可以解释这一点了:因为没有必要再为门牌号准备门牌号。
参考
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。