Go 语言中的方法

49 阅读7分钟

认识 Go 方法

Go 的方法也是以 func 关键字修饰的,并且和函数一样,也包含方法名(对应函数名)、参数列表返回值列表方法体(对应函数体)。方法中的这几个部分和函数声明中对应的部分,在形式与语义方面都是一致的,比如:

  • 方法名字首字母大小写决定该方法是否是导出方法
  • 方法参数列表支持变长参数
  • 方法的返回值列表也支持具名返回值等。
img

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

最后,我们通过调用接口变量 sSpeak 方法来验证类型 Person 是否实现了接口 Speaker。由于 pPerson 类型,而 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
}