认识 Go 方法
Go 的方法也是以 func
关键字修饰的,并且和函数一样,也包含方法名(对应函数名)、参数列表、返回值列表与方法体(对应函数体)。方法中的这几个部分和函数声明中对应的部分,在形式与语义方面都是一致的,比如:
- 方法名字首字母大小写决定该方法是否是导出方法;
- 方法参数列表支持变长参数;
- 方法的返回值列表也支持具名返回值等。
Go 方法的声明由6部分组成:
func
关键字- receiver 是方法与类型之间的纽带,也是方法与函数的最大不同
- 方法名
- 参数列表
- 返回值列表
- 方法体
Tips: Go 方法必须是归属于一个类型的,而 receiver 参数的类型就是这个方法归属的类型,或者说这个方法就是这个类型的一个方法。
声明形式
func (t *T或T) MethodName(参数列表) (返回值列表) {
// 方法体
}
- 无论 receiver 参数的类型为
*T
还是T
,我们一般都把声明形式中的T
叫做 receiver 参数t
的基类型 - 如果
t
的类型为T
,那么说这个方法是类型T
的一个方法 - 如果
t
的类型为*T
,那么就说这个方法是类型*T
的一个方法 - 每个方法只能有一个 receiver 参数,Go 不支持在方法的 receiver 部分放置包含多个 receiver 参数的参数列表,或者变长 receiver 参数
receiver 参数的作用域
-
方法接收器(receiver)参数、函数 / 方法参数,以及返回值变量对应的作用域范围,都是函数 / 方法体对应的显式代码块 传送门
package main import "fmt" type Person struct { Name string Age int } func (p Person) SayHello() { fmt.Printf("Hello, my name is %s. I am %d years old.\n", p.Name, p.Age) } func main() { p := Person{Name: "Forest", Age: 24} p.SayHello() // 调用方法 // 函数体开始 funcExample := func() { // 函数参数作用域范围开始 message := "Hello, World!" fmt.Println(message) // 函数参数作用域范围结束 } // 函数体结束 funcExample() // 调用函数 // 返回值变量作用域范围开始 age := getAge() fmt.Println("Age:", age) // 返回值变量作用域范围结束 } // 函数开始 func getAge() int { // 函数体开始 return 30 // 函数体结束 } // 函数结束
-
receiver 部分的参数名不能与方法参数列表中的形参名,以及具名返回值中的变量名存在冲突,必须在这个方法的作用域中具有唯一性;否则Go 编译器报错。
type T struct{} func (t T) M(t string) { // 编译器报错:duplicate argument t (重复声明参数t) ... ... }
-
如果在方法体中,没有用到 receiver 参数,可以省略 receiver 的参数名。
type T struct{} func (T) M(t string) { ... ... }
-
receiver 参数的基类型本身不能为指针类型或接口类型;否则报错。
type MyInt *int func (r MyInt) String() string { // r的基类型为MyInt,编译器报错:invalid receiver type MyInt (MyInt is a pointer type) return fmt.Sprintf("%d", *(*int)(r)) } type MyReader io.Reader func (r MyReader) Read(p []byte) (int, error) { // r的基类型为MyReader,编译器报错:invalid receiver type MyReader (MyReader is an interface type) return r.Read(p) }
方法声明与receiver的要求
方法声明要与 receiver 参数的基类型声明放在同一个包内;不能为原生类型(诸如 int、float64、map 等)添加方法;不能跨越 Go 包为其他包的类型声明新方法。Go 语言中的方法的本质就是:一个以方法的 receiver 参数作为第一个参数的普通函数。
receiver 参数类型对 Go 方法的影响
func (t T) M1() <=> F1(t T)
当 receiver 参数的类型为 T
时,方法 M1
使用类型为 T
的 receiver 参数时,传递的是 T
类型实例的副本。在方法体中对副本的任何修改操作都不会影响到原始的 T
类型实例。
func (t *T) M2() <=> F2(t *T)
当 receiver 参数的类型为 *T
时,选择以 *T
作为 receiver 参数类型时,M2
方法等价于 F2(t *T)
。传递给 F2
函数的是 T
类型实例的地址,所以对参数 t
的任何修改都会反映到原始的 T
类型实例上。
选择 receiver 参数类型的原则
如果 Go 方法要把对 receiver 参数代表的类型实例的修改,反映到原类型实例上,那么我们应该选择 *T
作为 receiver 参数的类型,是不是只能通过 *T
类型变量调用该方法,而不能通过 T
类型变量调用? 传送门
package main
import "fmt"
type Person struct {
Name string
Age int
}
// 使用 *Person 作为 receiver 参数的方法
func (p *Person) UpdateAge(newAge int) {
p.Age = newAge
}
func main() {
p := Person{Name: "Forest", Age: 24}
// 使用 *Person 类型变量调用 UpdateAge 方法
p.UpdateAge(30)
fmt.Println("Age after update:", p.Age) // 输出: Age after update: 30
// 使用 Person 类型变量调用 UpdateAge 方法会导致编译错误
//p.UpdateAge(30) // 编译错误: cannot use p (type Person) as type *Person in function argument
}
一般情况下,我们通常会为 receiver 参数选择 T
类型,因为这样可以缩窄外部修改类型实例内部状态的“接触面”,也就是尽量少暴露可以修改类型内部状态的方法。
考虑到 Go 方法调用时,receiver 参数是以值拷贝的形式传入方法中的。那么,如果 receiver 参数类型的 size 较大,以值拷贝形式传入就会导致较大的性能开销,这时我们选择 *T
作为 receiver 类型可能更好些。 传送门
package main
import "fmt"
type BigData struct {
// 假设这里有很多很大的数据字段
Data [1000000]int
}
// 使用 *BigData 作为 receiver 参数的方法
func (bd *BigData) ProcessData() {
// 对大数据进行处理
fmt.Println("process data")
}
func main() {
bd := &BigData{} // 创建 BigData 类型的指针
// 使用 *BigData 类型的变量调用 ProcessData 方法
bd.ProcessData()
}
如果 T
类型需要实现某个接口,那我们就要使用 T
作为 receiver 参数的类型,来满足接口类型方法集合中的所有方法。
方法集合
如果某类型 T
的方法集合与某接口类型的方法集合相同,或者类型 T
的方法集合是接口类型 I
方法集合的超集,那么我们就说这个类型 T
实现了接口 I
。或者说,方法集合这个概念在 Go 语言中的主要用途,就是用来判断某个类型是否实现了某个接口。 传送门
package main
import "fmt"
// 定义一个接口
type Speaker interface {
Speak()
}
// 定义一个类型
type Person struct {
Name string
}
// 为类型 Person 添加一个方法
func (p Person) Speak() {
fmt.Println("Hello, my name is", p.Name)
}
func main() {
p := Person{Name: "Forest"}
// 检查类型 Person 是否实现了接口 Speaker
var s Speaker = p
s.Speak() // 调用 Speak 方法,输出: Hello, my name is Forest
}
在上面的示例中,我们定义了一个接口 Speaker
,其中只有一个方法 Speak
。然后我们定义了一个类型 Person
,并为其添加了一个方法 Speak
。接下来,我们将类型 Person
的实例 p
赋值给接口 Speaker
类型的变量 s
。这是合法的,因为类型 Person
的方法集合与接口 Speaker
的方法集合相同,即 Person
类型实现了接口 Speaker
。
最后,我们通过调用接口变量 s
的 Speak
方法来验证类型 Person
是否实现了接口 Speaker
。由于 p
是 Person
类型,而 Person
类型实现了 Speak
方法,因此调用 s.Speak()
将触发 Person
类型的 Speak
方法,输出 "Hello, my name is Forest"。
结构体类型的方法集合,包含嵌入的接口类型的方法集合。
结构体的方法集合不允许存在交集,但是接口的方法集合可以。因为接口只是方法定义,存在交集的方法的定义还是相同的;而结构体的方法涉及到实现,存在交集时编译器无法决定要用哪个实现。
package main
import "fmt"
type A interface {
Method1()
}
type B interface {
Method1()
}
type C struct{}
func (c C) Method1() {
fmt.Println("Method1 from C")
}
func main() {
var a A
var b B
var c C
a = c
b = c
a.Method1() // Method1 from C
b.Method1() // Method1 from C
}
Go 就会优先使用结构体自己实现的方法。如果没有实现,那么 Go 就会查找结构体中的嵌入字段的方法集合中,是否包含了这个方法。如果多个嵌入字段的方法集合中都包含这个方法,那么我们就说方法集合存在交集。
package main
import "fmt"
type A struct{}
func (a A) Method1() {
fmt.Println("Method1 from A")
}
type B struct{}
func (b B) Method2() {
fmt.Println("Method2 from B")
}
type C struct {
A
B
}
func main() {
c := C{}
c.Method1() // Method1 from A
c.Method2() // Method2 from B
}