需要预先了解:


1. 方法

**方法(method)**是带接收者的函数。接收者写在 func 和函数名之间,把方法和某个类型绑定起来。方法看起来只是多了一个参数,但"值接收者还是指针接收者"这个选择会直接影响方法能否修改对象,以及该类型能否满足某个接口。


1.1 定义

type Counter struct {
	n int
}

func (c Counter) Get() int {
	return c.n
}

(c Counter) 就是接收者。调用时用点号:

c := Counter{n: 5}
fmt.Println(c.Get()) // 5

方法名不能与字段名冲突。


1.2 值接收者 vs 指针接收者

值接收者拿到的是调用者的副本,修改副本不影响原对象;指针接收者拿到的是地址,可以修改原对象:

func (c Counter) IncrVal()  { c.n++ } // 改副本
func (c *Counter) IncrPtr() { c.n++ } // 改原对象
c := Counter{}
c.IncrVal()
fmt.Println("值接收者调用后:", c.Get()) // 0

c.IncrPtr()
fmt.Println("指针接收者调用后:", c.Get()) // 1

选择原则有三条:

  1. 需要修改接收者时用指针接收者;
  2. 接收者较大(字段多、含数组)时用指针接收者,避免每次调用都复制;
  3. 同一个类型的方法集要保持一致——只要有一个方法用了指针接收者,其余方法也应该用指针接收者。混用会让"这个类型到底该传值还是传指针"变得难以判断,也会影响接口实现(下一节)。

1.3 调用时的自动取址与解引用

调用方法时,编译器会自动帮你取地址或解引用:

p := &Counter{n: 10}
p.IncrVal() // 自动解引用,等价于 (*p).IncrVal()

v := Counter{n: 20}
v.IncrPtr() // 自动取址,等价于 (&v).IncrPtr()

但这两种自动转换都有前提:值必须可寻址。map 的元素、结构体字面量都不可寻址,因此无法调用指针接收者方法:

m := map[string]Counter{"a": {}}
m["a"].Incr() // 编译错误
Counter{}.Incr() // 编译错误
cannot call pointer method Incr on Counter

原因是 map 元素在扩容时会被搬移,地址随时会变;字面量则根本没有地址。解决办法是把值取出来改完再写回去,或者让 map 的 value 类型就是 *Counter。


1.4 方法集

**方法集(method set)**是"某个类型上可调用的方法集合"。它和接收者类型有关:

类型 方法集
T 所有值接收者方法
*T 所有值接收者方法 + 所有指针接收者方法

也就是说,*T 的方法集包含 T 的方法集,反过来不成立。这个规则决定了接口的实现关系:

type V struct{}

func (v V) ValMethod()  {}
func (v *V) PtrMethod() {}

type ValIface interface {
	ValMethod()
}

type PtrIface interface {
	ValMethod()
	PtrMethod()
}
var v V
var p *V

var a ValIface = v  // 可以,V 有 ValMethod
var b ValIface = p  // 可以,*V 的方法集包含 V 的
var c PtrIface = p  // 可以,*V 有 PtrMethod

var d PtrIface = v  // 编译错误
cannot use v (variable of struct type V) as PtrIface value in variable declaration:
V does not implement PtrIface (method PtrMethod has pointer receiver)

最后一行是 Go 里最常见的接口实现错误之一:用值类型实现接口时,接口的方法集里不能有指针接收者方法。接口本身的用法见 接口,底层的 itab 与方法表见 接口与 channel 的底层实现。


1.5 嵌入的方法提升

匿名嵌入的内层类型的方法会被提升到外层:

type Base struct {
	Name string
}

func (b Base) Describe() string { return "Base(" + b.Name + ")" }
func (b Base) OnlyBase() string { return "只有 Base 有" }

type Derived struct {
	Base
}

外层可以定义同名方法遮蔽内层:

func (d Derived) Describe() string {
	return "Derived(" + d.Base.Describe() + ")" // 显式调用内层
}
d := Derived{Base{Name: "内层"}}
fmt.Println("外层方法:", d.Describe())   // Derived(Base(内层))
fmt.Println("内层方法:", d.Base.Describe()) // Base(内层)
fmt.Println("未被遮蔽的方法:", d.OnlyBase()) // 只有 Base 有

注意 Derived 的 Describe 并不是"重写"——Base 的 Describe 依然存在,只是通过 d.Describe() 访问时优先找到外层的。想调用内层版本必须写 d.Base.Describe()。

如果外层想通过"内层方法"看到外层的行为,需要在调用时把外层传进去,或者用接口。嵌入不会带来动态派发。


1.6 method value 与 method expression

方法本质就是函数,只是第一个参数是接收者。Go 提供了两种把它"变成函数"的写法。

method value:绑定接收者,得到一个无参(或只剩其他参数)的函数:

c := Counter{n: 5}
f := c.Incr // 等价于 func() { (&c).Incr() }
f()
fmt.Println(c.Get()) // 6

注意 c.Incr 在创建时就确定了接收者,之后的调用都作用在同一个对象上。

method expression:T.Name 或 (*T).Name 形式,接收者变成第一个参数:

g := (*Counter).Incr
g(&c)

h := Counter.Get
fmt.Println(h(c)) // 6

method expression 常用于把方法当作回调传给高阶函数。


1.7 可以为哪些类型定义方法

规则只有一条:方法必须定义在同一个包内的命名类型上。

可以为任何自定义类型定义方法,不限于结构体:

type MyInt int

func (m MyInt) Double() MyInt { return m * 2 }

type IntSlice []int

func (s IntSlice) Sum() int {
	total := 0
	for _, v := range s {
		total += v
	}
	return total
}
fmt.Println(MyInt(21).Double())        // 42
fmt.Println(IntSlice{1, 2, 3}.Sum())   // 6

但不能给其他包的类型定义方法:

func (d time.Duration) String2() string { return "x" }
cannot define new methods on non-local type time.Duration

也不能给基础类型本身定义方法(func (i int) Double() int 同样报错),必须先 type MyInt int 定义一个新类型。需要"给 time.Duration 加方法"时,就用 type MyDuration time.Duration 包一层。


1.8 练习

  1. 定义 type Temperature float64,给它加 Celsius 和 Fahrenheit 两个方法,验证两种接收者的行为差异。
  2. 定义 Base 与 Derived,让 Base 有一个指针接收者方法,观察 Derived 的指针与值分别能否调用它,并用方法集规则解释。
  3. 用 method value 把一组方法存进 []func(),依次调用,观察它们作用在哪个对象上。