目 录CONTENT

文章目录
go

【Go】方法

hxuanyu
2026-02-04 / 0 评论 / 1 点赞 / 24 阅读 / 0 字

概念

方法声明

方法声明的基本格式为:

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(因此二者不能在同一结构体中同时作为嵌入字段出现)。
1
博主关闭了所有页面的评论