go基础15-方法:理解“方法”的本质

107 阅读5分钟

Go 程序的执行流本质上就是在函数调用栈中上下流动,从一个函数到另一个函数。

我们将围绕方法的本质、方法 receiver 的类型选择、方法集合,以及如何实现方法的“继承”这几个主题,进行讲解。

认识 Go 方法

我们就以 Go 标准库 net/http 包中 *Server 类型的方法 ListenAndServeTLS 为例,讲解一下 Go 方法的一般形式:

image.png

从上面这张图我们可以看到,和由五个部分组成的函数声明不同,Go 方法的声明有六个组成部分,多的一个就是图中的 receiver 部分。在 receiver 部分声明的参数,Go 称之为 receiver 参数,这个 receiver 参数也是方法与类型之间的纽带,也是方法与函数的最大不同。

Go 中的方法必须是归属于一个类型的,而 receiver 参数的类型就是这个方法归属的类型,或者说这个方法就是这个类型的一个方法。我们以上图中的 ListenAndServeTLS 为例,这里的 receiver 参数 srv 的类型为 *Server,那么我们可以说,这个方法就是 *Server 类型的方法,

注意!这里我说的是 ListenAndServeTLS 是 *Server 类型的方法,而不是 Server 类型的方法。具体的原因,我们在后面课程还会细讲,这里你先有这个认知就好了。

func (t *T或T) MethodName(参数列表) (返回值列表) {
    // 方法体
}

无论 receiver 参数的类型为 *T 还是 T,我们都把一般声明形式中的 T 叫做 receiver 参数 t 的基类型。如果 t 的类型为 T,那么说这个方法是类型 T 的一个方法;如果 t 的类型为 *T,那么就说这个方法是类型 *T 的一个方法。而且,要注意的是,每个方法只能有一个 receiver 参数,Go 不支持在方法的 receiver 部分放置包含多个 receiver 参数的参数列表,或者变长 receiver 参数。

方法接收器(receiver)参数、函数 / 方法参数,以及返回值变量对应的作用域范围,都是函数 / 方法体对应的显式代码块。

这就意味着,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 参数名字要保证唯一外,Go 语言对 receiver 参数的基类型也有约束,那就是 receiver 参数的基类型本身不能为指针类型或接口类型。下面的例子分别演示了基类型为指针类型和接口类型时,Go 编译器报错的情况:

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)
}

最后,Go 对方法声明的位置也是有约束的,Go 要求,方法声明要与 receiver 参数的基类型声明放在同一个包内。基于这个约束,我们还可以得到两个推论。

  • 第一个推论:我们不能为原生类型(诸如 int、float64、map 等)添加方法。比如,下面的代码试图为 Go 原生类型 int 增加新方法 Foo,这样做,Go 编译器会报错:
func (i int) Foo() string { // 编译器报错:cannot define new methods on non-local type int
    return fmt.Sprintf("%d", i) 
}
  • 第二个推论:不能跨越 Go 包为其他包的类型声明新方法。比如,下面的代码试图跨越包边界,为 Go 标准库中的 http.Server 类型添加新方法 Foo,这样做,Go 编译器同样会报错:
import "net/http"

func (s http.Server) Foo() { // 编译器报错:cannot define new methods on non-local type http.Server
}

我们直接还是通过一个例子理解一下。如果 receiver 参数的基类型为 T,那么我们说 receiver 参数绑定在 T 上,我们可以通过 *T 或 T 的变量实例调用该方法:

type T struct{}

func (t T) M(n int) {
}

func main() {
    var t T
    t.M(1) // 通过类型T的变量实例调用方法M

    p := &T{}
    p.M(2) // 通过类型*T的变量实例调用方法M
}

方法的本质是什么?

通过前面的学习,我们知道了 Go 的方法与 Go 中的类型是通过 receiver 联系在一起,我们可以为任何非内置原生类型定义方法,比如下面的类型 T:

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 参数以第一个参数的身份并入到方法的参数列表中。按照这个原理,我们示例中的类型 T 和 *T 的方法,就可以分别等价转换为下面的普通函数:

// 类型T的方法Get的等价函数
func Get(t T) int {  
    return t.a 
}

// 类型*T的方法Set的等价函数
func Set(t *T, a int) int { 
    t.a = a 
    return t.a 
}

Go 语言中的方法的本质就是,一个以方法的 receiver 参数作为第一个参数的普通函数。

此文章为3月Day15学习笔记,内容来源于极客时间《Tony Bai · Go 语言第一课》。