Golang 是谷歌在 09 年开源的一个静态类型、支持 GC、支持函数式的编程语言,由于对多线程编程语法级别的支持,在云计算时代大放异彩。Go 中有很多令人会心一笑的设计,也有很多遭人不时诟病的旮旯,但总体来说, Go 是一门令人眼前一亮的语言,很多设计引人思考、很多取舍令人触动,从这方面来说,我觉得 Go 值得一学。
之后我会开一个 Go 系列的坑,写多写少,随心随缘。
最近在写 Go 代码时需要给某个 struct 定制一个字符串转换方法
func (ms MyStruct) String() string
但是在实现是考虑选用 value methods 还是 pointer methods 方式时纠结了起来。
Go 的语法糖使得这两种方式在调用上是一致的,这让我一时难以抉择孰优孰劣,于是决定深入探究一下其背后原理以便之后能写出更地道(idiomatic)的 Go 代码。
在官方 effective go 文档中,对两者区别其实是有精确描述的:
The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers.
There is a handy exception, though. When the value is addressable, the language takes care of the common case of invoking a pointer method on a value by inserting the address operator automatically.
大意如下:
看了这个解释心里有一句 mmp,不值当讲不当讲。Go 的语法糖初用起来很爽,但是用的越多反而发现会引入很多的语义上的复杂性,给你的心智带来极大负担。比如说 type assertion、embedding、自动解引用、自动插入取地址符、自动插入分号等等,不一一吐槽。只能说都是 trade off,做人不能太贪,不能既要又要是吧。
多言易成妄语,直接上小例子:
package mainimport ( "fmt")type Foo struct { name string}func (f *Foo) PointerMethod() { fmt.Println("pointer method on", f.name)}func (f Foo) ValueMethod() { fmt.Println("value method on", f.name)}func NewFoo() Foo { // 返回一个右值 return Foo{name: "right value struct"}}func main() { f1 := Foo{name: "value struct"} f1.PointerMethod() // 编译器会自动插入取地址符,变为 (&f1).PointerMethod() f1.ValueMethod() f2 := &Foo{name: "pointer struct"} f2.PointerMethod() f2.ValueMethod() // 编译器会自动解引用,变为 (*f2).PointerMethod() NewFoo().ValueMethod() NewFoo().PointerMethod() // Error!!!}
最后一句报错如下:
./pointer_method.go:34:10: cannot call pointer method on NewFoo()./pointer_method.go:34:10: cannot take the address of NewFoo()
看来编译器首先试着给 NewFoo()
返回的右值调用 pointer method,出错;然后试图给其插入取地址符,未果,就只能报错了。
至于左值和右值的区别,大家感兴趣可以自行搜索一下。大致来说,最重要区别就是是否可以被寻址,可以被寻址的是左值,既可以出现在赋值号左边也可以出现在右边;不可以被寻址的即为右值,比如函数返回值、字面值、常量值等等,只能出现在赋值号右边。
对于某个特定场景,两者如何取舍其实和另一个问题等价:就是你在定义函数时如何传参 —— 是传值还是传指针。
比如上述例子:
func (f *Foo) PointerMethod() { fmt.Println("pointer method on ", f.name)}func (f Foo) ValueMethod() { fmt.Println("value method on", f.name)}
可以转换为下面两个函数进行考虑:
func PointerMethod(f *Foo) { fmt.Println("pointer method on ", f.name)}func ValueMethod(f Foo) { fmt.Println("value method on", f.name)}
用 Go 的术语来说,就是将函数的 receiver 看做是 argument。
那么传值还是传指针呢?这几乎是各个语言都会遇到的一个灵魂拷问。当然, Java 第一个表示不服,这里不展开,感兴趣自行 google。
在定义 receiver 为值还是指针时,主要有以下几个考虑点:
那啥时候用 value method 呢?很简单的不可变对象使用 value method 可以减轻 gc 的负担,貌似也就这些好处了。因此请记住:
遇事不决请用 pointer method.
当然,Go 大大们怕你还是有疑问,于是帮你详细列了一些常见的 case,请看这里:https://github.com/golang/go/wiki/CodeReviewComments#receiver-type