golang的for-range中容易犯错的指针使用,顺便一提Ruby中的对象及引用

写在前面

使用Golang写代码已有一段时间,自认为达到了一个“还可以”的水平:工作中的业务需求能快速地完成,同事遇到了问题也能参与讨论一起解决。

不过最近还是有一处代码竟没有一次性通过,是可忍孰不可忍,接下来就借此机会聊一聊Golang中的副本与指针的概念。

适应人群

入门——初级√——中级——高级;本文适应初级及以上。

先看一段代码

如果大家不看最后的答案,可以分析一下,下面这段代码的输出是什么。

package main
import "fmt"

type Angle struct {
	Name string
	Age  int
}

func main() {
	m := make(map[string]*Angle)
	angs := []Angle{
		{Name: "ka", Age: 18},
		{Name: "kan", Age: 18},
		{Name: "kang", Age: 18},
	}
	for _, ang := range angs {
		m[ang.Name] = &ang
	}
	for k, v := range m {
		fmt.Println("key=>"+k, "name=>"+v.Name)
	}
}

Golang中的副本与指针

作为一名“优秀“的Ruby程序猿,在写Golang时习惯上难免会带有一些Ruby的影子;尤其当平时的工作包含一部分Ruby代码的编写时,这种编程语言自由切换的酸爽感觉,真简直了。

为了便于记忆,这里先把Ruby与Golang的不同列一下。

Ruby中的引用(或指针)

def decode(json)
  data = ::JSON.parse(json, quirks_mode: true)

  if ActiveSupport.parse_json_times
    convert_dates_from(data)
  else
    data
  end
end

先来看一下Ruby中的一个函数(出处见这里)。在rails的源码中,有一段对JSON进行解码的函数,它的意思是把json进行解析以后,返回data这个对象。(Ruby中JSON的parse函数参考这里)

初看很自然对不对?那么考虑一下,这里为什么直接返回了data,而不是返回data的指针呢

在Ruby中,一切皆对象,默认的返回既是对象的引用(或指针)。因此在Ruby的JSON的decode函数中,返回data即意味着返回了data的指针或者引用。(需要说明,在Ruby中,有一类特殊的类型,它们的值放在指针里面,比如数字)

Golang中的副本与指针

Golang中相应的decode函数,源码见这里,它主要实现的功能是把JSON编码的值读出来,然后把JSON的转变成Go的值(一般为struct,既结构)

// Decode reads the next JSON-encoded value from its
// input and stores it in the value pointed to by v.
//
// See the documentation for Unmarshal for details about
// the conversion of JSON into a Go value.
func (dec *Decoder) Decode(v interface{}) error {
  //省略很多内容
}

注意上面的源码可以发现,这里的dec *Decoder使用了指针的形式;假如函数的声明是func (dec Decoder) Decode(v interface{}) error会怎么样呢?

其实答案很简单,如果使用(dec Decoder)的方式,Decode函数将不起任何作用。因为这种情况下在调用dec.Decode(v)时,会在内存中生成一个dec的副本,其方法作用的值也会保存在这个副本中;函数一旦结束,这个副本所在的内存被GC(垃圾回收器)回收掉,原来的dec根本感觉不到任何的变化,因此这里应该使用指针的方式。

当然,当使用指针的方式,dec.Decode(v)方法调用时,dec的指针也会被复制一份,但是由于两个指针指向同一份数据,因此函数会作用到dec的本体上面

代码的答案

通过上面的内容,可以很容易理解,”先看一段代码“小节的代码的答案,如下所示:

#
# 下面为golang代码的输出
key=>ka name=>kang
key=>kan name=>kang
key=>kang name=>kang

如果还是有所疑惑,那么考虑下面的代码片段:

// 很容易看出来这里的i是一个被重复利用的值
// 即在整个for循环中,只存在一个i变量;
// 同样的道理,这里只存在一个ang变量,每次
// range时,新的Angle副本会填充整个ang变量
// 变成一个新的值。
// 把ang的指针赋值给m[ang.Name],也就意味
// 着所有的m中存在的是指向同一块内存的同一个
// 指针。
for i, ang := range angs {
  fmt.Println(&i)
  m[ang.Name] = &ang
}

小结

本文简单就一段代码引发的bug进行了简单的分析,从Ruby中一切皆对象,一切赋值为引用(或指针)赋值,到Golang中的副本与指针,阐明了代码中bug存在的本质。

最后不免感慨,编程语言的自由切换,需要对每种语言模型具有深刻的理解。平日里多留心细节,不忘编程初心,才是一个快乐的程序猿。

(程序猿的编程初心是什么:升值加薪,走上人生高峰,赢取某白富美, 😆 )