概念
方法声明
方法声明的基本格式为:
func (receiver ReceiverType) MethodName(params...) (results...) {
// body
}
例如:
package main
type person struct{}
func (p person) sayHello() {
println("hello")
}
func main() {
p := person{}
p.sayHello()
}
与函数类似,Go 中的方法也使用 func 关键字,并包含方法名、参数列表、返回值列表与方法体。但方法在方法名之前多了一个 receiver(接收者) 参数,它将方法与某个类型绑定起来,这是方法与函数的核心区别。
每个方法必须隶属于一个特定类型,因此 receiver 有且只能有一个。receiver 的作用域与形参、命名返回值相同,都是整个方法体代码块;因此 receiver 的命名不能与形参或(在同一作用域内的)变量/命名返回值冲突。若方法体不使用 receiver 变量名,可以省略名称:
type T struct{}
func (T) M(t string) {
// ...
}
Go 语言还要求:
-
receiver 的类型必须是定义在当前包内的非接口命名类型,且不能是指针类型(即 receiver 可以是
T 或*T,但T的底层类型不能是指针)。 -
因此:
- 不能为内置类型(如
int、string)直接添加方法;但可以通过“自定义命名类型”间接添加方法,例如type MyInt int。 - 不能跨包为其他包定义的类型添加方法(只能在定义该类型的包内声明其方法)。
- 不能为内置类型(如
方法的本质
方法通过 receiver 与类型关联。可以为任何满足上述约束的命名类型定义方法,例如:
type T struct {
a int
}
func (t T) Get() int {
return t.a
}
func (t *T) Set(a int) int {
t.a = a
return t.a
}
从实现上看,Go 可以将“方法”理解为“带有额外第一个参数(receiver)的函数”。例如上面的两个方法可类比为:
func Get(t T) int {
return t.a
}
func Set(t *T, a int) int {
t.a = a
return t.a
}
这种等价关系也体现在 方法表达式(method expression) 与 方法值(method value) 上:
- 方法表达式:
T.Get 或(*T).Set,得到一个“显式 receiver 参数”的函数值。 - 方法值:
t.Get 或pt.Set,会把 receiver 绑定(闭包化),得到不再显式需要 receiver 参数的函数值。
示例(修正原文中 (*T).Get 与实际方法集不匹配的问题):
package main
import "fmt"
type T struct{ a int }
func (t T) Get() int { return t.a }
func (t *T) Set(a int) { t.a = a }
func main() {
fmt.Printf("%T\n", T.Get) // func(main.T) int
fmt.Printf("%T\n", (*T).Set) // func(*main.T, int)
t := T{a: 1}
mv := t.Get
fmt.Printf("%T\n", mv) // func() int
}
由于方法本质上仍是函数,因此函数的错误处理方式(如返回 error)、defer、panic/recover 等机制同样适用。
receiver 参数类型(T vs *T)
Go 的参数传递是值传递。当 receiver 是 T 时,方法内部拿到的是一个副本;当 receiver 是 *T 时,方法内部通过指针修改原对象。
package main
import "fmt"
type T struct{ a int }
func (t T) Set2() {
t.a = 2
fmt.Println("method inside:", t.a) // 2
}
func (t *T) Set3() {
t.a = 3
fmt.Println("method inside:", t.a) // 3
}
func main() {
t := T{1}
fmt.Println(t.a) // 1
t.Set2()
fmt.Println(t.a) // 1(未修改原对象)
t.Set3() // 编译器自动取址:等价于 (&t).Set3()
fmt.Println(t.a) // 3
}
选择原则
-
需要在方法内修改 receiver 所代表的对象状态时,选
*T。 -
不需要修改对象、且对象较小、拷贝代价可接受时,可选
T。 -
下列场景通常应选
*T:- 类型较大,拷贝成本高。
- 类型包含不应被复制的成员(如包含
sync.Mutex等);这类类型“复制后语义错误”,虽然未必总是编译错误,但应避免值接收者导致的复制。
-
与接口实现相关:
- 若接口的方法由值接收者方法即可满足,那么
T 与*T都实现该接口。 - 若接口包含任何指针接收者方法(即方法只在
*T 方法集中),则只有*T 能实现该接口,T不行。 - 因此并不是“为了实现接口就应选
T”,而是要根据期望让T 也实现接口还是只让*T实现接口来决定。
- 若接口的方法由值接收者方法即可满足,那么
方法集合(Method Set)
必要性
Go 需要一套可判定规则来决定:某个类型(及其指针)有哪些可调用方法、是否实现某个接口。尤其当值接收者与指针接收者方法并存时,T 与 *T 的可用方法不同;方法集合保证了编译期即可判定调用/接口赋值是否合法。
定义
对非接口类型 T:
-
T 的方法集合:所有接收者为T的方法。 -
*T 的方法集合:所有接收者为T 或*T的方法。
对接口类型 I:
-
I的方法集合:接口中声明的全部方法(含通过嵌入引入的方法)。
接口实现规则:
- 若类型
X 的方法集合是接口I 方法集合的超集(包含接口要求的所有方法),则X 实现I。
示例:
package main
import "fmt"
type I interface{ M() }
type T struct{}
func (T) M() {} // 属于 T 的方法集,也属于 *T 的方法集
func (*T) P() {} // 只属于 *T 的方法集
func takesI(x I) { fmt.Printf("%T implements I\n", x) }
func main() {
var t T
var pt *T = &t
takesI(t) // OK
takesI(pt) // OK
var _ interface{ P() } = pt // OK
// var _ interface{ P() } = t // 编译错误:T 的方法集不含 P
}
类型嵌入(组合)模拟“继承式复用”
Go 不支持传统类继承,但可用 组合 与 类型嵌入(embedding) 获得类似的“字段/方法提升(promote)”效果。
接口类型嵌入
新接口会把被嵌入接口的方法集合并入自身方法集合:
type E interface {
M1()
M2()
}
type I interface {
E
M3()
}
此时 I 的方法集合为 M1/M2/M3。
结构体类型嵌入
在结构体中把某个类型名(或其指针类型名、或接口类型名)直接写成字段,即为嵌入字段;嵌入字段的方法也会被提升,使得外部可直接通过外层类型调用这些方法(本质上是选择器的语法糖与委派)。
嵌入字段的约束:
- 嵌入字段本身可以是命名类型
T 或 *T(允许指针类型作为嵌入字段),也可以是接口类型;限制在于嵌入字段必须是“类型名”形式,不能是*struct{...}这类未命名类型。 - 在同一个结构体中,嵌入字段的字段名(对
T 来说是T,对*T 来说也是T)必须唯一,因此不能同时嵌入T 与*T,也不能重复嵌入同名类型。
概念
方法声明
方法声明的基本格式为:
func (receiver) 方法名(参数列表) (返回值列表) {
方法体
}
例如:
package main
type person struct{}
func (p person) sayHello() {
println("hello")
}
func main() {
p := person{}
p.sayHello()
}
与函数类似,Go 中的方法也使用 func 关键字,并包含方法名、参数列表、返回值列表与方法体;这些部分的语义与函数一致。不同之处在于:方法在方法名之前多了一个 receiver(接收者)部分,receiver 参数是方法与类型之间的纽带,用于把方法“挂”到某个类型上,也是方法区别于函数的核心。
每个方法必须归属于一个特定类型,因此每个方法有且仅有一个 receiver 参数。receiver 参数与形参、具名返回值一样,作用域都在方法体对应的显式代码块内,因此其命名不能与同一作用域内的其他标识符冲突。若方法体内不使用 receiver,可以省略 receiver 的参数名:
type T struct{}
func (T) M(s string) {
// ...
}
此外,receiver 的类型必须是定义在当前包内的非接口类型 T 或 *T(其中 T 不能是指针类型)。因此可以得到:
- 不能为非本包定义的类型声明方法(不能跨包给别人的类型加方法)。
- 不能直接为预声明类型(如
int、string)声明方法;但可以为它们在本包定义的“新类型”声明方法,例如type MyInt int。
方法的本质
Go 中的方法与类型通过 receiver 联系在一起。可以为任意本包定义的非接口类型(包括结构体、别名出来的新类型等)定义方法:
type T struct{ a int }
func (t T) Get() int { return t.a }
func (t *T) Set(a int) {
t.a = a
}
从实现角度看,方法可以理解为“把 receiver 作为第一个参数的函数”。例如上面的两个方法可视作等价函数:
func Get(t T) int { return t.a }
func Set(t *T, a int) {
t.a = a
}
Go 还支持方法表达式(method expression)与方法值(method value):
- 方法表达式:
T.Get /(*T).Set,得到一个“显式 receiver 参数”的函数值。 - 方法值:
v.Get /(&v).Set,会把 receiver 绑定到该值上,得到一个不再需要 receiver 参数的函数值。
示例:
package main
import "fmt"
type T struct{ a int }
func (t T) Get() int { return t.a }
func (t *T) Set(a int) { t.a = a }
func main() {
// 方法表达式:receiver 显式作为第一个参数
f1 := T.Get
fmt.Printf("%T\n", f1) // func(main.T) int
f2 := (*T).Set
fmt.Printf("%T\n", f2) // func(*main.T, int)
// 方法值:receiver 被绑定
t := T{a: 1}
mv := t.Get
fmt.Printf("%T\n", mv) // func() int
}
由于方法在本质上是函数调用形式的一种,defer、panic/recover、错误处理等机制在方法中同样适用。
receiver 参数类型
声明方法时,receiver 可以选择 T(值接收者)或 *T(指针接收者)。Go 参数按值传递:值接收者方法得到的是 receiver 的一份副本;指针接收者方法可通过指针修改原对象。
package main
import "fmt"
type T struct{ a int }
func (t T) Set2() {
t.a = 2
fmt.Println("method inside:", t.a)
}
func (t *T) Set3() {
t.a = 3
fmt.Println("method inside:", t.a)
}
func main() {
t := T{1}
fmt.Println(t.a) // 1
t.Set2()
fmt.Println(t.a) // 1
t.Set3() // 允许:编译器自动取址
fmt.Println(t.a) // 3
}
可以看到:指针接收者会影响调用方的实例;值接收者只修改副本。
receiver 参数类型的常用原则
-
需要修改接收者状态,或避免拷贝开销,通常选择
*T。 -
不需要修改接收者、且类型较小、语义上更像“值”,可选择
T。 -
当类型包含不可安全复制或不应复制的字段(例如
sync.Mutex、sync.Once、包含这些字段的结构体等),应使用*T,并避免发生复制。 -
关于接口实现:
-
T 的方法集只包含接收者为T的方法; -
*T 的方法集包含接收者为T 和*T 的方法;
因此若接口方法由指针接收者实现(func (*T) M()),则只有*T 才实现该接口;若接口方法由值接收者实现(func (T) M()),则T 与*T都实现该接口。
-
方法集合
必要性
当同时存在值接收者与指针接收者方法时,T 与 *T 可用的方法并不相同。Go 需要一套可判定的编译期规则来决定:某个类型(及其指针)到底有哪些方法、是否实现某个接口,从而保证类型安全与一致语义。
package main
import "fmt"
type I interface{ M() }
type T struct{}
func (T) M() {} // 属于 T 的方法集
func (*T) P() {} // 只属于 *T 的方法集
func takesI(x I) { fmt.Printf("%T implements I\n", x) }
func main() {
var t T
var pt *T = &t
takesI(t) // OK:T 的方法集里有 M
takesI(pt) // OK:*T 的方法集包含 (T)M
// var _ interface{ P() } = t // 编译错误:T 的方法集不含 P
var _ interface{ P() } = pt // OK
}
定义(常用结论)
-
T 的方法集:所有接收者为T的方法。 -
*T 的方法集:所有接收者为T 或*T的方法。
若某类型的方法集包含某接口的全部方法,则该类型实现该接口。
类型嵌入模拟“实现继承”
Go 不支持传统类继承,通常用组合(composition)实现代码复用与行为复用;其中类型嵌入(embedding)提供了类似“提升方法”的效果。
接口类型嵌入
新接口会把被嵌入接口的方法集合并入自身方法集合:
type E interface {
M1()
M2()
}
type I interface {
E
M3()
}
此时 I 需要同时拥有 M1/M2/M3。
结构体类型嵌入
在结构体中直接写入某个类型名、*T 或接口类型名作为字段,即为嵌入字段。嵌入字段的可导出方法会被“提升”(promote),从而可以通过外层类型直接调用(必要时会隐式取址/解引用)。
type Base struct{}
func (Base) F() {}
type Derived struct {
Base // 嵌入字段
}
调用 d.F() 本质上等价于 d.Base.F()(在可行的前提下由编译器完成选择与转换),体现的是委派而非继承。
结构体嵌入字段的约束要点:
- 嵌入字段必须写成类型名
T、*T 或接口类型名I(不能写成**T等)。 - 同一结构体内,嵌入字段的字段名必须唯一:
T 的字段名为T,*T 的字段名也为T(因此二者不能在同一结构体中同时作为嵌入字段出现)。