熟悉面向对象编程的小伙伴对类的概念一定不会陌生,在经典面向对象语言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语言的类型系统