七叶笔记 » golang编程 » 搞明白Go的值和指针,别写bug了

搞明白Go的值和指针,别写bug了

最近多次出现同事误用指针导致系统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

下面这张图能够更好的解释上面的输出:

  1. 无论是结构体还是简单变量,都有值和地址
  2. 结构体t内部有两个string变量,因地址是16进制显示的,所以0C040和0C050正好是16字节。至于为什么是16字节,大家可以自己思考一下
  3. 指针save没有存放真正的结构体数据,存放的是一个地址值,该地址为t的起始地址。当然save自身也有地址。
  4. 无论是简单变量还是结构体,只要是指针类型,都可以用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循环的最后一个值。希望大家不要写这种有问题的代码。

上述所有代码可在 查看。

资料

  1. 简述 Golang 查看变量占用字节大小

相关文章