声明方法
方法就是带接收者的函数:接收者是写在方法名前面的一个额外参数。接收者把方法和某个类型绑定在一起。
接收者的名字通常是取自类型名的一两个字母(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 或其他包中的类型添加方法。