Menu

Golang 方法:值接收者与指针接收者详解

方法就是带接收者的函数。学习如何声明方法、什么时候使用值接收者或指针接收者、方法集与接口,以及在非结构体类型上定义方法。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

声明方法

方法就是带接收者的函数:接收者是写在方法名前面的一个额外参数。接收者把方法和某个类型绑定在一起。

接收者的名字通常是取自类型名的一两个字母(Circle 用 c)。Go 不使用 this 或 self,风格指南也不鼓励使用这样的名字。

因为 Circle 有一个 String() string 方法,它满足 fmt.Stringer,所以 fmt.Println 打印的是这个方法的结果,而不是 {2}。

值接收者与指针接收者

值接收者 (c Circle) 拿到的是一份副本。指针接收者 (c *Circle) 拿到的是地址,所以对字段的修改会保留下来。

IncByValue 能编译通过,却没有任何可见的效果,这是一个经典的 bug。如果一个方法要修改它的接收者,就需要指针接收者。

这些调用还体现了 Go 提供的两个便利。在可寻址的变量上调用 c.Inc(),会被改写为 (&c).Inc();在指针上调用 p.Value(),会被改写为 (*p).Value()。你很少需要只为了调用方法而写 & 或 *。

自动取 & 需要一个可寻址的东西。map 元素和复合字面量都不可寻址:

m := map[string]Counter{"a": {}}
m["a"].Inc()    // compile error: cannot call pointer method Inc on Counter
Counter{}.Inc() // same error: a composite literal is not addressable

在 map 中存储指针(map[string]*Counter),或者把值复制出来,修改后再写回去。

选择哪种接收者

以下情况使用指针接收者以下情况使用值接收者
方法会修改接收者类型很小,而且它的方法从不修改它(time.Time、一个坐标点)
结构体很大(复制有开销)类型是 map、func 或 chan(本身已经是引用)
类型中包含 sync.Mutex 或类似的东西你希望这个值像基本类型一样,可以放心复制
该类型的其他方法已经使用指针

Go FAQ 和标准库遵循同一条经验法则:保持一致。如果有任何方法需要指针接收者,就让所有方法都使用指针接收者,这样方法集是可预测的。如果一个包含 sync.Mutex 的结构体通过值接收者被复制,go vet 会把它标记出来(passes lock by value)。

方法集与接口

这个区别对接口影响最大。T 的方法集包含值接收者的方法。*T 的方法集则同时包含值接收者和指针接收者的方法。

如果在那个切片里写 Square{Side: 3} 而不加 &,就无法编译:

cannot use Square{…} (value of struct type Square) as Shape value in array or slice literal: Square does not implement Shape (method Area has pointer receiver)

原因是:接口中可以存放一个不可寻址的 Square 副本,所以 Go 无法取它的地址来调用指针方法。更多内容见接口。

非结构体类型上的方法

在你的包中声明的任何具名类型都可以有方法,不只是结构体。

限制在于:类型必须和方法定义在同一个包中,而且它的底层类型不能是指针或接口。你不能在自己的包中给 int、string 或 time.Time 添加方法。用一个新的具名类型把它包起来,这才是 Go 的做法。给整数类型定义 String() 方法,也是让枚举输出可读文本的办法;见用 iota 实现枚举。

方法值与方法表达式

绑定到某个具体接收者的方法,就是一个函数值:

使用值接收者的方法值在创建时会复制接收者,所以 f 打印的仍然是 Ana。如果是指针接收者,它会保存指针,因而能看到之后的修改。方法值很适合用作回调:http.HandleFunc("/", srv.handleIndex)。

常见错误

  • 会修改接收者的方法却用了值接收者。 能编译通过,却只是悄悄修改了一份副本。
  • 混用两种接收者。 会让人搞不清是 T 还是 *T 满足某个接口。每个类型只选一种。
  • 在 nil 指针上调用指针方法。 这是允许的,方法会以 nil 接收者运行,只有在访问字段时才会 panic。有些类型(比如 nil 安全的链表)会有意利用这一点,但大多数类型并不预期这种情况。

常见问题

Go 语言中方法和函数有什么区别?

方法有一个接收者,写在 func 和方法名之间:func (c Circle) Area() float64。你在一个值上调用它(c.Area()),它属于该类型的方法集,类型也正是靠方法来满足接口的。除此之外,它的行为就像一个以接收者为第一个参数的函数。

Go 中什么时候应该使用指针接收者?

当方法必须修改接收者时,当结构体大到每次调用都复制一遍很浪费时,或者当类型中包含不能被复制的东西(比如 sync.Mutex)时,就使用指针接收者。如果一个类型有任何方法需要指针接收者,通常的建议是为了保持一致,把它的所有方法都改成指针接收者。

为什么方法使用指针接收者后,我的类型就没有实现接口了?

使用指针接收者的方法属于 *T 的方法集,而不属于 T。所以 var s Shape = Square{} 会失败,报错 Square does not implement Shape (method Area has pointer receiver)。改为赋值一个指针:var s Shape = &Square{}。

Go 语言可以给内置类型定义方法吗?

不能直接定义。方法只能声明在同一个包中定义的具名类型上。先定义你自己的类型 type Celsius float64,再给它添加方法。你不能给 int、string 或其他包中的类型添加方法。

Coddy 编程语言插图

和 Coddy 一起学编程

开始