Golang 笔记(一):值方法和指针方法(Value Methods vs Pointer Methods)

简介: Golang 笔记(一):值方法和指针方法(Value Methods vs Pointer Methods)

小引

最近在写 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.

大意如下:

  1. 值方法(value methods)可以通过指针和值调用,但是指针方法(pointer methods)只能通过指针来调用。
  2. 但有一个例外,如果某个值是可寻址的(addressable,或者说左值),那么编译器会在值调用指针方法时自动插入取地址符,使得在此情形下看起来像指针方法也可以通过值来调用。

看了这个解释心里有一句 mmp,不值当讲不当讲。Go 的语法糖初用起来很爽,但是用的越多反而发现会引入很多的语义上的复杂性,给你的心智带来极大负担。比如说 type assertion、embedding、自动解引用、自动插入取地址符、自动插入分号等等,不一一吐槽。只能说都是 trade off,做人不能太贪,不能既要又要是吧。

多言易成妄语,直接上小例子:

package main
import (
  "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 为值还是指针时,主要有以下几个考虑点:

  1. 方法是否需要修改 receiver 本身。如果需要,那 receiver 必然要是指针了。
  2. 效率问题。如果 receiver 是值,那在方法调用时一定会产生 struct 拷贝,而大对象拷贝代价很大哦。
  3. 一致性。对于同一个 struct 的方法,value method 和 pointer method 混杂用肯定是不优雅的啦。

那啥时候用 value method 呢?很简单的不可变对象使用 value method 可以减轻 gc 的负担,貌似也就这些好处了。因此请记住:

遇事不决请用 pointer method.

当然,Go 大大们怕你还是有疑问,于是帮你详细列了一些常见的 case,请看这里:https://github.com/golang/go/wiki/CodeReviewComments#receiver-type

参考

  1. effective go:https://golang.org/doc/effective_go.html#pointers_vs_values
  2. golang faq:https://golang.org/doc/faq#methods_on_values_or_pointers
  3. golang code review comments: https://github.com/golang/go/wiki/CodeReviewComments#receiver-type
  4. stackoverflow: https://stackoverflow.com/questions/27775376/value-receiver-vs-pointer-receiver


相关文章
|
9月前
|
存储 C++
【指针笔试题的笔记】
【指针笔试题的笔记】
27 0
|
2月前
|
SQL 前端开发 Go
编程笔记 GOLANG基础 001 为什么要学习Go语言
编程笔记 GOLANG基础 001 为什么要学习Go语言
|
2月前
|
C++
指针(笔记2)一
本文介绍了C++中`const`关键字修饰指针的两种情况:当`const`位于星号(*)左侧时,它限制指针所指向的内容不可修改,但指针自身可变;当`const`位于星号(*)右侧时,它限制指针变量不可改变,但可通过该指针修改其指向的内容。此外,文章还讨论了指针的基本运算,包括指针加减整数(用于遍历数组),指针减指针(计算两者间元素个数)以及指针的关系运算(在循环中控制指针移动)。
23 1
|
2月前
|
人工智能 C++
指针习题笔记(较难,可用于思维锻炼)
指针习题笔记(较难,可用于思维锻炼)
21 4
|
2月前
|
程序员 编译器 C语言
指针(笔记2)二
这篇内容主要讲解了指针和野指针的概念以及如何避免野指针的问题。野指针是指针未初始化、越界访问或指向已释放内存的情况。避免野指针的方法包括初始化指针、避免指针越界和在不再使用时将指针设为NULL。此外,文章提到了`assert`断言在调试中的作用,它可以帮助检测程序运行时的错误条件。最后,讨论了函数调用中的传值调用和传址调用,指出传址调用允许函数直接修改实参变量的值。
21 0
|
2月前
|
存储 编译器
指针(笔记1) 二
这篇内容介绍了指针的相关概念,包括解引用操作符、指针变量的大小、指针的解引用、指针加整数以及void*指针的使用。解引用操作符允许通过指针访问和修改变量,指针变量的大小取决于地址总线的宽度(32位系统为4字节,64位系统为8字节)。指针加整数时,不同类型的指针会按相应类型大小移动。void*指针可以存储任何类型的数据地址,但不能直接解引用或进行指针运算,通常用于函数参数以实现泛型编程。最后强调了指针变量应指向相同类型的变量,并在类型不匹配时进行强制转换。
24 0
|
2月前
|
存储 C语言 Perl
指针(笔记1)一
本文介绍了计算机内存和地址的基本概念。内存由存储单元组成,每个单元有唯一地址,内存地址是无符号整数。在32位系统中,地址空间为4GB。内存被划分为字节单元,每个变量在内存中占据特定长度的空间,例如字符占1字节,整型占4字节。指针是存储变量地址的变量,通过取地址操作符(&)获取变量的地址。指针变量需要先赋值才能使用,并且只能指向定义时的变量类型。
20 0
|
2月前
|
Go 开发者
Golang深入浅出之-Go语言方法与接收者:面向对象编程初探
【4月更文挑战第22天】Go语言无类和继承,但通过方法与接收者实现OOP。方法是带有接收者的特殊函数,接收者决定方法可作用于哪些类型。值接收者不会改变原始值,指针接收者则会。每个类型有相关方法集,满足接口所有方法即实现该接口。理解并正确使用这些概念能避免常见问题,写出高效代码。Go的OOP机制虽不同于传统,但具有灵活性和实用性。
28 1
|
2月前
|
Java Go
Golang深入浅出之-Go语言指针面试必知:理解与使用指针
【4月更文挑战第21天】Go语言中的指针允许直接操作内存,常用于高效数据共享和传递。本文介绍了指针的基础知识,如声明、初始化和解引用,以及作为函数参数使用。此外,讨论了`new()`与`make()`的区别和内存逃逸分析。在结构体上下文中,指针用于减少复制开销和直接修改对象。理解指针与内存管理、结构体的关系及常见易错点,对于面试和编写高性能Go代码至关重要。
30 2
|
2月前
|
C语言
C语言(指针详解)重点笔记:指针易错点,都是精华
C语言(指针详解)重点笔记:指针易错点,都是精华
41 0