类型方法

熟悉面向对象编程的小伙伴对类的概念一定不会陌生,在经典面向对象语言C#中,类的属性、字段、方法一般都会被定义在一个文件里。然而在Golang中却没有类的概念,相比与C#,Golang更像C。在Golang中类型都通过结构体(struct)来实现,众所周知在C里面,结构体在定义时是没有方法的,Golang也一样,但是Golang依然可以为结构体编写方法,在调用时就像面向对象语言一样用’对象.方法()'来调用。

package main

import "fmt"

type user struct {
	name string
	age  int
}

func (usr user) sayHi() {
	fmt.Printf("user %s age is %d say hi\n", usr.name, usr.age)
}

以上示例我们创建了一个user类型,并为它编写了一个sayHi方法,所以我们在程序里可以这样去调用。

	var usr user = user{"Tom", 20}
	usr.sayHi()

由此可见,在Golang中类型的定义和类型方法的定义是分开的,以至于可以在不同的文件里定义类型的方法,做到引用了特定的文件才能使用特定的方法,有点类似C#的扩展方法。
类型方法的定义规则

func (型参名 绑定的类型) 方法名(型参名 类型) 返回值 {
}

类型方法集的可访问性

有C语言基础的小伙伴应该都知道,指针类型和结构体类型是两码事。即*user和user 是两个不同的类型,一个表示指向user结构体的指针(只存放地址),一个表示结构体变量。
在Golang中有这样一套方法集访问规则

类型方法参数接收类型
T(t T)
*T(t T) and (t *T)

即 T 类型只能传向参数类型是 T 的方法,*T类型则可以传向参数类型是T或者 *T的方法。这样可能不太好理解,也可以换一个角度。

方法参数接收类型可传类型
(t T)T and *T
(t *T)*T

如果方法参数是T类型,那么可以传T或者*T。如果参数类型是 *T则只能传 *T。

既然如此,如果我们这样定义类型方法

package main

import "fmt"

type user struct {
	name string
	age  int
}

// 指针类型
func (usr *user) sayHello() {
	fmt.Printf("user %s age is %d say hello\n", usr.name, usr.age)
}

// 结构体类型
func (usr user) sayHi() {
	fmt.Printf("user %s age is %d say hi\n", usr.name, usr.age)
}

现在有一个user类型的变量,它能够访问sayHello方法吗?或者有一个user*类型的变量,它能够访问sayHi方法吗?

	var usr user = user{"Tom", 20}
	// 这里能调用是因为编译器自动帮我们取了地址
	usr.sayHello() //(&usr).sayHello()
	usr.sayHi()
	
	var ptUsr = &usr
	ptUsr.sayHello()
	ptUsr.sayHi()

答案是,无论*user还是user都能无障碍访问对应的方法。
不是说 *user 类型的参数只能传 *user类型吗?为什么没取地址的变量usr也能访问sayHello方法?原因是编译器在编译的时候为我们加上了取地址操作。

接口

Golang中的接口实现比较特殊,和传统面向对象语言有较大的差异。Golang中的接口定义格式如下

type 接口名 interface{
Method1(arg1 arg)
Method2(arg2 arg)
}

接口定义的格式很好理解,几乎和C#、Java一样,但是实现就大不一样了。
首先,类型在实现接口时不需要显式的继承接口,即定义类型时不需要指定这个类型实现哪些接口,只要定义了对应的类型方法就算是实现了这个接口。

type user struct {
	name string
	age  int
}

type person interface {
	talk()
}

func (usr user) talk() {
	fmt.Printf("%s talk \n", usr.name)
}

因为user类型有takl()方法,所以user类型实现了person接口,作为老C#码农这里忍不住要吐槽一下,这样子虽然看上去很灵活,但是也太恶心人了,一个类型根本不知道实现了哪些接口,而且如果接口方法的定义分在了不同的文件里,引用多一个文件和少一个文件类型实现的接口有可能不一样。

接口的实现

下面我们看这段代码

type user struct {
	name string
	age  int
}

type person interface {
	talk()
	eat()
}

func (usr user) talk() {
	fmt.Printf("%s talk \n", usr.name)
}

func (usr *user) eat() {
	fmt.Printf("%s eat \n", usr.name)
}

请注意person接口实现时的类型

func main() {
	var usr user = user{"Tom", 20}
	var p person = usr	// 这里编译错误
	p.talk()
	p.eat()
}

这里将user类型的变量赋值给person接口将会编译错误,因为user类型并没有实现person接口,而*user 才实现了person接口。将代码改一下,对usr变量取地址就能编译成功了。

var p person = &usr

正如上文说到,user类型和*user 类型是两个类型,类型方法之所以能相互无障碍调用是因为编译会自动帮我们取地址。但是在以上场景的时候编译器就做不到帮我们取地址了。
代码中为什么只有 *user 类型实现了person接口?首先,eat方法是用 *user作为参数的,这个当然算实现了eat方法,然后,talk方法是用 user作为参数的,根据上面的表格,当参数类型是T时,传T或者 *T 都是可以接受的,所以 *user 也实现了eat方法。
反观user类型,eat方法中指定是 *user 类型,根据上面的表格,当参数是 *T 时,只能传 *T 所以user类型没有能访问到eat方法,所以它没有实现person接口。至于为什么user类型的变量能使用eat方法呢?这个在上面已经讲过了,是因为编译器自动帮我们取了地址。

参考资料

Go语言实战(William Kennedy, Brian Ketelsen, Erik St. Martin) 第五章 Go语言的类型系统