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


相关文章
|
7月前
|
存储 Java C++
C++ 引用和指针:内存地址、创建方法及应用解析
C++中的引用是现有变量的别名,创建时需用`&`运算符,如`string &meal = food;`。指针存储变量的内存地址,使用`*`创建,如`string* ptr = &food;`。引用必须初始化且不可为空,而指针可初始化为空。引用在函数参数传递和提高效率时有用,指针适用于动态内存分配和复杂数据结构操作。选择使用取决于具体需求。
97 9
|
3月前
|
Go
Golang的math包常用方法
这篇文章介绍了Golang的math包中的常量和常用方法,并通过示例代码展示了如何使用这些常量和方法。
179 87
Golang的math包常用方法
|
2月前
指针(Pointer)的深度理解(2)
指针(Pointer)的深度理解(2)
34 1
|
3月前
|
C++
C++(十八)Smart Pointer 智能指针简介
智能指针是C++中用于管理动态分配内存的一种机制,通过自动释放不再使用的内存来防止内存泄漏。`auto_ptr`是早期的一种实现,但已被`shared_ptr`和`weak_ptr`取代。这些智能指针基于RAII(Resource Acquisition Is Initialization)原则,即资源获取即初始化。RAII确保对象在其生命周期结束时自动释放资源。通过重载`*`和`->`运算符,可以方便地访问和操作智能指针所指向的对象。
|
4月前
|
Kubernetes Go API
构建 Golang 应用程序的实用方法
构建 Golang 应用程序的实用方法
|
3月前
|
Go
Golang语言高级数据类型之指针篇
这篇文章详细讲解了Golang语言中的指针概念、指针地址和类型、定义指针变量、指针的细节操作、指针传值,以及内置函数new和make的用法和它们之间的区别。
27 0
|
4月前
|
存储 大数据 测试技术
掌握 GoLang 中的指针:高效代码的提示和技巧
掌握 GoLang 中的指针:高效代码的提示和技巧
java.lang.NullPointerExceptionMybatisPlus出现,测试,java.lang.NullPointe,空指针异常,public方法少写了一个字段,没加注解
java.lang.NullPointerExceptionMybatisPlus出现,测试,java.lang.NullPointe,空指针异常,public方法少写了一个字段,没加注解
|
6月前
|
C++ 存储 Java
C++ 引用和指针:内存地址、创建方法及应用解析
'markdown'C++ 中的引用是现有变量的别名,用 `&` 创建。例如:`string &meal = food;`。指针通过 `&` 获取变量内存地址,用 `*` 创建。指针变量存储地址,如 `string *ptr = &food;`。引用不可为空且不可变,指针可为空且可变,适用于动态内存和复杂数据结构。两者在函数参数传递和效率提升方面各有优势。 ```
|
7月前
|
C++
指针(笔记2)一
本文介绍了C++中`const`关键字修饰指针的两种情况:当`const`位于星号(*)左侧时,它限制指针所指向的内容不可修改,但指针自身可变;当`const`位于星号(*)右侧时,它限制指针变量不可改变,但可通过该指针修改其指向的内容。此外,文章还讨论了指针的基本运算,包括指针加减整数(用于遍历数组),指针减指针(计算两者间元素个数)以及指针的关系运算(在循环中控制指针移动)。
42 1