方法
需要预先了解:
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.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 练习
- 定义
type Temperature float64,给它加Celsius和Fahrenheit两个方法,验证两种接收者的行为差异。 - 定义
Base与Derived,让Base有一个指针接收者方法,观察Derived的指针与值分别能否调用它,并用方法集规则解释。 - 用 method value 把一组方法存进
[]func(),依次调用,观察它们作用在哪个对象上。



