Go开发的各种坑 - for-range的数据副本
2023-05-27 本文已影响0人
红薯爱帅
1. 概述
本文介绍for-range
的一个坑,由于其他语言很少遇到,C++没有range操作,Python没有取地址操作,唯独在golang中均支持,所以容易入坑。
另外,顺带着介绍一下变量赋值
操作,作为拓展阅读吧。
2. for-range的数据副本
通过for-range
可以遍历array
、slice
、map
和channel
,预声明的迭代变量,是唯一地址
的数据副本,既不是指向被迭代对象的每一项,也不随着每个loop即时申请新内存。
所以,在使用for-range遍历可迭代对象时,切不可对迭代变量取地址,因为取到的地址是不变的。
验证代码如下,其中:
- case0和case1是等价的,是错误示例
- case2和case3均是正确示例
package main
import "log"
type someStruct struct {
name string
age int
}
func doNotGetPointer() {
var persons = []someStruct{
{"foo", 1},
{"bar", 2},
}
// case0
var data0 []any
for _, item := range persons {
data0 = append(data0, &item) // 由于item是副本变量,所以地址不变
}
// case1
var data1 []any
var item1 someStruct
for i := 0; i < len(persons); i++ {
item1 = persons[i]
data1 = append(data1, &item1)
}
// case2
var data2 []any
var item2 *someStruct
for i := 0; i < len(persons); i++ {
item2 = &persons[i]
data2 = append(data2, item2)
}
// case3
var data3 []any
for i := 0; i < len(persons); i++ {
var item3 someStruct
item3 = persons[i]
data3 = append(data3, &item3)
}
log.Printf("data0 %+v", data0)
log.Printf("data1 %+v", data1)
log.Printf("data2 %+v", data2)
log.Printf("data3 %+v", data3)
}
func main() {
doNotGetPointer()
}
- 运行结果
% go run test_for1.go
2023/05/27 23:36:27 data0 [0x1400000c030 0x1400000c030]
2023/05/27 23:36:27 data1 [0x1400000c048 0x1400000c048]
2023/05/27 23:36:27 data2 [0x14000074180 0x14000074198]
2023/05/27 23:36:27 data3 [0x1400000c060 0x1400000c078]
3. 变量赋值
在不同的编程语言中,数据类型大体可分为基本数据类型
和引用数据类型
。
对于引用数据类型
,常常也是容器类型
,其值往往是可以动态改变
,且赋值给新变量时,只是赋地址,对应的value是一份。
因此,也有了浅copy
和深copy
之说。
3.1. Python
Python中,数据类型分为不可变对象
和可变对象
。
- 不可变对象,例如int, float, str, bool, tuple
- 可变对象,dict, list, set
注意,可变,是其指向的内存中的值可变。
a = [dict(name=f"name-{i}") for i in range(2)]
print("a", a)
b = [i for i in a]
print("b", b)
b[1]["name"] = "xxx"
print("a", a)
print("b", b)
$ python test.py
a [{'name': 'name-0'}, {'name': 'name-1'}]
b [{'name': 'name-0'}, {'name': 'name-1'}]
a [{'name': 'name-0'}, {'name': 'xxx'}] # ===> 修改了b[1]["name"],也对应修改了a[1]的value
b [{'name': 'name-0'}, {'name': 'xxx'}]
3.2. Golang
Go中,对不同数据类型,内存的数据结构不同,有的是值传递
,有的是地址传递
。
- string、slice、map这三种类型的实现机制
类似指针
。直接传递,就是地址传递,而不是值传递。(string
比较特殊,静态字符串默认是在栈空间
上,且只读
) - 其他类型,例如struct等,需要取地址后传递指针。否则,直接传递,就是值传递。
package main
import (
"fmt"
"log"
)
func changeName(names []string) {
for i, _ := range names {
names[i] = fmt.Sprintf("name-%d", i)
}
}
func testSlice() {
var persons = []string{"foo", "bar"}
log.Printf("%+v", persons)
changeName(persons)
log.Printf("%+v", persons)
}
func main() {
testSlice()
}
% go run test_for1.go
2023/05/28 00:05:07 [foo bar]
2023/05/28 00:05:07 [name-0 name-1]
需要注意的是,对于slice而言,直接传递,虽然可以改变其value,但是最好不要增加item。
因为一旦触发其扩容,则slice的起始地址会发生改变。
如果要改变slice内元素个数,需要传递slice指针,例如names *[]string
。