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存在的本质。
最后不免感慨,编程语言的自由切换,需要对每种语言模型具有深刻的理解。平日里多留心细节,不忘编程初心,才是一个快乐的程序猿。
(程序猿的编程初心是什么:升值加薪,走上人生高峰,赢取某白富美, 😆 )