最近多次出现同事误用指针导致系统bug,所以这次聊一下Go的值与指针,大家尽量避免写出错误代码。
错误示例
让我们先看一个错误代码示例:
//错误示例
func ErrorShow() {
fmt.Println("------------------错误示例")
var a, b string = "a", "b"
ai := ImageItem{
Key: a,
Name: a,
FileType: a,
}
bi := ImageItem{
Key: b,
Name: b,
FileType: b,
}
//news
newS := make(map[int]*string)
newL := []ImageItem{ai, bi}
for k, item := range newL {
newS[k] = &item.Key
}
f, _ := json.Marshal(newS)
fmt.Println(string(f))
}
上面这段代码会输出{“0″:”a”,”1″:”b”}吗?
我们看一下代码执行后的结果:
➜ myproject go run main.go
——————错误示例
{“0″:”b”,”1″:”b”}
系统输出了两个b,是不是和想象的有点不一样,让我们看一下原因。
值与指针
在讲原因前,我们先了解一下Go的值与指针。
对于Go里的任何变量,都存在内存中,而内存都有地址。所以对于任何变量,都可以从地址(指针)和值两个维度来看。
func ValueAndPoint() {
fmt.Println("------------------简单变量")
var hello string = "hello"
fmt.Println("值为", hello)
fmt.Printf("字符串字节长度为%d\n", unsafe.Sizeof(hello))
fmt.Println("地址为", &hello)
fmt.Printf("地址为%p\n", &hello)
//结构体
t := Test{
Hello: "hello",
World: "world",
}
fmt.Println("------------------结构体")
fmt.Println("值为", t)
fmt.Printf("结构体字节长度为%d\n", unsafe.Sizeof(t))
fmt.Println("地址为", &t)
fmt.Printf("地址为%p\n", &t)
fmt.Printf("地址为%p\n", &t.Hello)
fmt.Printf("地址为%p\n", &t.World)
//地址存放
fmt.Println("------------------地址存放")
var save *Test = &t
fmt.Printf("值为%p\n", &*save)
fmt.Printf("指针字节长度为%d\n", unsafe.Sizeof(save))
fmt.Printf("地址为%p\n", &save)
}
输出为:
➜ myproject go run main.go
——————简单变量
值为 hello
字符串字节长度为16
地址为 0xc000072210
地址为0xc000072210
——————结构体
值为 {hello world}
结构体字节长度为32
地址为 &{hello world}
地址为0xc00000c040
地址为0xc00000c040
地址为0xc00000c050
——————地址存放
值为0xc00000c040
指针字节长度为8
地址为0xc00000e020
下面这张图能够更好的解释上面的输出:
- 无论是结构体还是简单变量,都有值和地址
- 结构体t内部有两个string变量,因地址是16进制显示的,所以0C040和0C050正好是16字节。至于为什么是16字节,大家可以自己思考一下
- 指针save没有存放真正的结构体数据,存放的是一个地址值,该地址为t的起始地址。当然save自身也有地址。
- 无论是简单变量还是结构体,只要是指针类型,都可以用fmt.Printf(“地址为%p\n”, &t)打印地址值
分析
了解完值和指针的知识,我们在错误示例上增加一些输出,用于分析:
//错误示例
func ErrorShow() {
fmt.Println("------------------错误示例")
var a, b string = "a", "b"
ai := ImageItem{
Key: a,
Name: a,
FileType: a,
}
bi := ImageItem{
Key: b,
Name: b,
FileType: b,
}
//news
newS := make(map[int]*string)
newL := []ImageItem{ai, bi}
for k, item := range newL {
fmt.Println("------------------第", k+1, "次循环")
fmt.Printf("item的地址为 %p\n", &item)
fmt.Println("item的值为", item)
fmt.Printf("新key的地址为%p,老key的地址为%p\n", &item.Key, &newL[k].Key)
newS[k] = &item.Key
fmt.Printf("newS[k]的地址为: %p\n", newS[k])
fmt.Printf("newS[k]的值为: %s\n", *newS[k])
}
f, _ := json.Marshal(newS)
fmt.Println(string(f))
}
输出为:
➜ myproject go run main.go
——————错误示例
——————第 1 次循环
item的地址为 0xc000072180
item的值为 {a a a}
新key的地址为0xc000072180,老key的地址为0xc000020120
newS[k]的地址为: 0xc000072180
newS[k]的值为: a
——————第 2 次循环
item的地址为 0xc000072180
item的值为 {b b b}
新key的地址为0xc000072180,老key的地址为0xc000020150
newS[k]的地址为: 0xc000072180
newS[k]的值为: b
{“0″:”b”,”1″:”b”}
可以看出,在每次循环的时候,item并没有重新生成,使用的是同一块内存位置,而newS里存放的是item中key所在地址。所以最后一次循环,item值为bi的数据,newS里的地址又指向同一个位置,自然都变成了b。
检验
正确
那下面这个例子输出结果是什么呢?
func NewCase() {
var a, b string = "a", "b"
ai := ImageItem{
Key: a,
Name: a,
FileType: a,
}
bi := ImageItem{
Key: b,
Name: b,
FileType: b,
}
l := make(map[int]*ImageItem)
l[0] = &ai
l[1] = &bi
s := make([]*ImageItem, 2)
//指针赋值,没影响
for k, item := range l {
fmt.Println("------------------第", k+1, "次循环")
fmt.Printf("原数据的指针地址为: %p\n", l[k])
fmt.Printf("原数据的数据为: %v\n", l[k])
fmt.Println("item的地址为", &item)
fmt.Printf("item的值为: %p\n", item)
s[k] = item
fmt.Println("s[k]的地址为", &s[k])
fmt.Printf("s[k]的值为: %p\n", s[k])
}
f, _ := json.Marshal(s)
fmt.Println(string(f))
}
输出为:
➜ myproject go run main.go
——————第 1 次循环
原数据的指针地址为: 0xc00009e030
原数据的数据为: &{a a a}
item的地址为 0xc000094008
item的值为: 0xc00009e030
s[k]的地址为 0xc000098070
s[k]的值为: 0xc00009e030
——————第 2 次循环
原数据的指针地址为: 0xc00009e060
原数据的数据为: &{b b b}
item的地址为 0xc000094008
item的值为: 0xc00009e060
s[k]的地址为 0xc000098078
s[k]的值为: 0xc00009e060
[{“key”:”a”,”name”:”a”,”fileType”:”a”},{“key”:”b”,”name”:”b”,”fileType”:”b”}]
这种就没有问题。这是因为item存的值本身就是ai和bi的地址,给s赋值的时候,传递的也是ai和bi的地址,所以数据是正确的。当然,如果改动item或s里的值,也会影响到ai或bi。
错误
在实际工作中,大家可能经常碰到的是这种case:
func NormalErrorCase() {
var a, b string = "a", "b"
ai := ImageItem{
Key: a,
Name: a,
FileType: a,
}
bi := ImageItem{
Key: b,
Name: b,
FileType: b,
}
l := make(map[int]ImageItem)
l[0] = ai
l[1] = bi
s := make([]*ImageItem, 2)
//指针赋值,没影响
for k, item := range l {
s[k] = &item
}
f, _ := json.Marshal(s)
fmt.Println(string(f))
}
输出:
➜ myproject go run main.go
——————错误
[{“key”:”b”,”name”:”b”,”fileType”:”b”},{“key”:”b”,”name”:”b”,”fileType”:”b”}]
具体原因大家可以自己画图做分析。
总结
上面这些例子之所以使用错误,究其原因是因为在for循环中,item使用的是同一块内存,如果记录的是该item的地址,那么所有的值会变成for循环的最后一个值。希望大家不要写这种有问题的代码。
上述所有代码可在 查看。
资料
- 简述 Golang 查看变量占用字节大小