20.1 方法的定义

在 Go 语言中,结构体就像是类的一种简化形式,那么面向对象程序员可能会问:类的方法在哪里呢?在 Go 语言中有一个概念,它和方法有着同样的名字,并且大体上意思相近。

Go 语言中方法和函数在形式上很像,它是作用在接收器(receiver)上的一个函数,接收器是某种类型的变量。因此方法是一种特殊类型的函数,方法只是比函数多了一个接收器(receiver),当然在接口中定义的函数我们也称为方法(因为最终还是要通过绑定到类型来实现)。

正是因为有了接收器,方法才可以作用于接收器的类型(变量)上,类似于面向对象中类的方法可以作用于类属性上。

定义方法的一般格式如下:

func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }

方法名之前,func 关键字之后的括号中指定接收器 receiver。

type A struct {
	Face int
}
func (a A) f() {
	fmt.Println("hi ", a.Face)
}

上面代码中,我们定义了结构体 A ,注意f()就是 A 的方法,(a A)表示接收器。a 是 A的实例,f()是它的方法名,方法调用遵循传统的 object.name 即选择器符号:a.f()。

接收器(receiver)

  • 接收器类型除了不能是指针类型或接口类型外,可以是其他任何类型,不仅仅是结构体类型,也可以是函数类型,还可以是 int、bool、string 等等为基础的自定义类型。
    • package main
      import (
      	"fmt"
      )
      type Human struct {
      	name   string // 姓名
      	Gender string // 性别
      	Age    int    // 年龄
      	string        // 匿名字段
      }
      func (h Human) print() { // 值方法
      	fmt.Println("Human:", h)
      }
      type MyInt int
      func (m MyInt) print() { // 值方法
      	fmt.Println("MyInt:", m)
      }
      func main() {
      	//使用new方式
      	hu := new(Human)
      	hu.name = "Titan"
      	hu.Gender = "男"
      	hu.Age = 14
      	hu.string = "Student"
      	hu.print()
      	// 指针变量
      	mi := new(MyInt)
      	mi.print()
      	// 使用结构体字面量赋值
      	hum := Human{"Hawking", "男", 14, "Monitor"}
      	hum.print()
      	// 值变量
      	myi := MyInt(99)
      	myi.print()
      }
      程序输出:
      Human: {Titan 男 14 Student}
      MyInt: 0
      Human: {Hawking 男 14 Monitor}
      MyInt: 99
      
  • 接收器不能是一个接口类型,因为接口是一个抽象定义,但是方法却是具体实现;如果这样做会引发一个编译错误:invalid receiver type…。
    • package main
      import (
      	"fmt"
      )
      type printer interface {
      	print()
      }
      func (p printer) print() { //  invalid receiver type printer (printer is an interface type)
      	fmt.Println("printer:", p)
      }
      func main() {}
      
  • 接收器不能是一个指针类型,但是它可以是任何其他允许类型的指针。
    • package main
      import (
      	"fmt"
      )
      type MyInt int
      type Q *MyInt
      func (q Q) print() { // invalid receiver type Q (Q is a pointer type)
      	fmt.Println("Q:", q)
      }
      func main() {}
      

接收器不能是指针类型,但可以是类型的指针,有点绕口。下面我们看个例子:

package main
import (
	"fmt"
)
type MyInt int
func (mi *MyInt) print() { // 指针接收器,指针方法
	fmt.Println("MyInt:", *mi)
}
func (mi MyInt) echo() { // 值接收器,值方法
	fmt.Println("MyInt:", mi)
}
func main() {
	i := MyInt(9)
	i.print()
}

如果有类型T,方法的接收器为(t T)时我们称为值接收器,该方法称为值方法;方法的接收器为(t *T)时我们称为指针接收器,该方法称为指针方法。

类型 T(或 *T)上的所有方法的集合叫做类型 T(或 *T)的方法集。

关于接收器的命名

社区约定的接收器命名是类型的一个或两个字母的缩写(像 c 或者 cl 对于 Client)。不要使用泛指的名字像是 me,this 或者 self,也不要使用过度描述的名字,简短即可。

方法表达式与方法值

Go语言中,方法调用的方式如下:如有类型X的变量x,m()是其方法,则方法有效调用方式是x.m(),如果x是指针变量,则x.m()实际上是(&x).m()的简写。所以我们看到指针方法的调用写成x.m(),这其实是一种语法糖。

这里我们了解下Go语言的选择器(selector),如:

x.f

上面代码表示如果x不是包名,则表示是x(或* x)的f(字段或方法)。标识符f(字段或方法)称为选择器(selector),选择器不能是空白标识符。选择器表达式的类型是f的类型。

选择器f可以表示类型T的字段或方法,或者指嵌入字段T的字段或方法f。遍历到f的嵌入字段的层数被称为其在T中的深度。在T中声明的字段或方法f的深度为零。在T中的嵌入字段A中声明的字段或方法f的深度是A中的f的深度加1。

Go语言中,我们认为方法的显式接收器(explicit receiver)x是方法x.m()的等效函数X.m()的第一个参数,所以x.m()和X.m(x)是等价的,下面我们看看具体例子:

package main
import (
	"fmt"
)
type T struct {
	a int
}
func (tv T) Mv(a int) int {
	fmt.Printf("Mv的值是: %d\n", a)
	return a
} // 值方法
func (tp *T) Mp(f float32) float32 {
	fmt.Printf("Mp: %f\n", f)
	return f
} // 指针方法
func main() {
	var t T
	// 下面几种调用方法是等价的
	t.Mv(1)    // 一般调用
	T.Mv(t, 1) // 显式接收器t可以当做为函数的第一个参数
	f0 := t.Mv // 通过选择器(selector)t.Mv将方法值赋值给一个变量 f0
	f0(2)
	T.Mv(t, 3)
	(T).Mv(t, 4)
	f1 := T.Mv // 利用方法表达式(Method Expression) T.Mv 取到函数值
	f1(t, 5)
	f2 := (T).Mv // 利用方法表达式(Method Expression) T.Mv 取到函数值
	f2(t, 6)
}

t.Mv(1)和T.Mv(t, 1)效果是一致的,这里显式接收器t可以当做为等效函数T.Mv()的第一个参数。而在Go语言中,我们可以利用选择器,将方法值(Method Value)取到,并可以将其赋值给其它变量。使用 t.Mv,就可以得到 Mv 方法的方法值,而且这个方法值绑定到了显式接收器(实参)t。

f0 := t.Mv // 通过选择器将方法值t.Mv赋值给一个变量 f0

除了使用选择器取到方法值外,还可以使用方法表达式(Method Expression) 取到函数值(Function Value)。方法表达式(Method Expression)产生的是一个函数值(Function Value)而不是方法值(Method Value)。

f1 := T.Mv // 利用方法表达式(Method Expression) T.Mv 取到函数值
f1(t, 5)
f2 := (T).Mv // 利用方法表达式(Method Expression) T.Mv 取到函数值
f2(t, 6)

这个函数值的第一个参数必须是一个接收器:

f1(t, 5)
f2(t, 6)

上面有关选择器,方法表达式,函数值,方法值等概念可以帮助我们更好理解方法,掌握他们可以更好地使用好方法。

Go语言中不允许方法重载,因为方法是函数,所以对于一个类型只能有唯一一个特定名称的方法。但是如果基于接收器类型,我们可以通过一种变通的方法,达到这个目的:具有同样名字的方法可以在 2 个或多个不同的接收器类型上存在,比如在同一个包里这么做是允许的:

type MyInt1 int
type MyInt2 int
func (a *MyInt1) Add(b int) int { return 0 }
func (a *MyInt2) Add(b int) int { return 0 }

自定义类型方法与匿名嵌入

Go语言中类型加上它的方法集等价于面向对象中的类。但在 Go 语言中,类型的代码和绑定在它上面的方法集的代码可以不放置在同一个文件中,它们可以保存在同一个包下的其他源文件中。下面是在非结构体类型上定义方法的例子:

type MyInt int
func (m MyInt) print() { // 值方法
	fmt.Println("MyInt:", m)
}

注意:类型和作用在它上面定义的方法必须在同一个包里定义,所以基础类型int、float 等上不能直接定义。

类型在其他的,或是非本地的包里定义,在它上面定义方法都会发生错误。

package main
import (
	"fmt"
)
func (i int) print() { // cannot define new methods on non-local type int
	fmt.Println("Int:", i)
}
func main() {
}
程序编译不通过,错误如下:
cannot define new methods on non-local type int

虽然我们不能直接为非同一包下的类型直接定义方法,但我们可以以这个类型(比如:int 或 float)为基础来自定义新类型,然后再为新类型定义方法。

package main
import (
	"fmt"
)
type MyInt int
func (m MyInt) print() { // 值方法
	fmt.Println("MyInt:", m)
}
func main() {
	myi := MyInt(99)
	myi.print()
}
程序输出:
MyInt: 99

MyInt类型由int 为基础自定义的,MyInt定义了一个方法 print()

下面我们再以这个代码为例看看在类型别名下的方法情况,类型别名情况下方法是保留的,但自定义的新类型方法是需要重新定义的,原方法不保留。

如果我们采用类型别名下面程序可正常运行,Go 1.9及以上版本编译通过:

package main
import (
	"fmt"
)
type MyInt int
type NewInt = MyInt
func (m MyInt) print() { // 值方法
	fmt.Println("MyInt:", m)
}
func main() {
	myi := MyInt(99)
	myi.print()
	Ni := NewInt(myi)
	Ni.print()
}
程序输出:
MyInt: 99
MyInt: 99

但上面代码我们稍微修改,把type NewInt = MyInt 改为type NewInt MyInt 。一个符号“=”去掉使得NewInt 变为新类型,会报程序错误:

Ni.print undefined (type NewInt has no field or method print)

因为Ni 属于新的自定义类型 NewInt, 它没有定义print()方法,需要另外定义这个方法。

我们也可以像下面这样将定义好的类型作为匿名类型嵌入在一个新的结构体中。当然新方法只在这个自定义类型上有效。

package main
import (
	"fmt"
)
type Human struct {
	name   string // 姓名
	Gender string // 性别
	Age    int    // 年龄
	string        // 匿名字段
}
type Student struct {
	Human     // 匿名字段
	Room  int // 教室
	int       // 匿名字段
}
func (h Human) String() { // 值方法
	fmt.Println("Human")
}
func (s Student) String() { // 值方法
	fmt.Println("Student")
}
func (s Student) Print() { // 值方法
	fmt.Println("Print")
}
func main() {
	stud := Student{Room: 102, Human: Human{"Hawking", "男", 14, "Monitor"}}
	stud.String()
	stud.Human.String()
}
程序输出:
Student
Human