Go的方法集详解

https://mp.weixin.qq.com/s/sWDrVL0EsHzCDNDpGtpT-A
1



什么是方法集



在go语言中,每个类型都有与之关联的方法,把这个类型的所有方法称为类型的方法集。如下:



type Student struct {
age int8
name string
}



func (s Student) showName() {
fmt.Println(s.name)
}



func (s * Student) setName(newName string) {
s.name = newName
}
类型Student方法集包含了showName()方法。
类型*Student方法集包含了showName()方法和setName()方法。
为什么呢?因为:
类型 T 方法集,包含全部 receiver T 方法。



类型 *T 方法集,包含全部 receiver T + *T 方法。

方法集和方法接受者的关系



在上面的案例中,类型Student的方法集并不包含了setName()方法,那么是不是Student类型变量,就不能调用setName()方法呢?即下面调用,是否会报错呢?



s := Student{}
s.setName(“dq”)
其实,上面的调用是ok的。为什么呢?我们来回顾一下go语言的方法定义。
参数 receiver 可任意命名,如方法中,不使用参数,可以省略参数名。
参数 receiver 类型可以是 T 或 *T,但类型T不能为接口或指针类型。
不支持方法重载。
实例value或pointer可以调用全部的方法,编译器会自动转换。
如下:



type Student struct {
age int8
name string
}
type StudentPoint *Student



func (Student) sayHello() { //省略receiver 的参数参数名字
fmt.Println(“hello world”)
}



func (s Student) showName() {
fmt.Println(s.name)
}



func (s * Student) setName(newName string) {
s.name = newName
}



func (s StudentPoint) showName2(){ // Error:接受者(receiver)为指针类型
fmt.Println(s.name)
}



s := Student{}
s.setName(“dq”) //go会自动转为 (&s).setName(“dq”)



var s2 = &s
s2.showName() //o会自动转为 (s2).showName()
所以,当类型调用自己申明的方法的时候,不需要考虑方法集。方法接受者是值类型(T),还是指针类型(
T),影响T类型的实体变量的方法集。



3



方法集和接口



接口的定义



接口是一个或多个方法签名的集合。任何类型的方法集中只要拥有该接口“对应的全部方法”声名。就表示它 “实现” 了该接口,无须在该类型上显式声明实现了哪个接口。
对应的全部方法:是指有相同名称、参数列表 (不包括参数名) 以及返回值。
接口只有方法的声明,没有实现。
接口可以匿名嵌入到其他接口,或是嵌入结构体中。
接口命名习惯以 er 结尾。
type Personer interface { //定义一个接口
showName()
}



type Student struct {
Personer //嵌入接口
}
接口执行机制



接口对象,是由接口表(interface table)和数据指针组成。
接口表存储元数据信息,包括接口类型、动态类型,以及实现接口的方法指针。
数据指针持有的是目标对象的只读复制品,复制完整对象或指针。
package main



import (
“fmt”
“reflect”
)



type User struct {
id int
name string
}



type Student struct {
id int
name string
}



func main() {
u := User{1, “Tom”}
var i interface{} = u // 由于interface{}不包含任何方法,所以任何类型,都实现了interface{}接口
fmt.Println(reflect.TypeOf(i)) //main.User



i = Student{}
fmt.Println(reflect.TypeOf(i)) //main.Student
}
以实体类型和以指针类型实现接口的区别



若以实体类型(T)实现接口,不管是T类型的值,还是T类型的指针,都实现了该接口。
若以指针类型(*T)实现接口,只有T类型的指针,才实现了该接口。
type Animal interface { //接口
say()
}



type Dog struct {
name string
}



type Cat struct {
name string
}



func (_self Dog) say() {
fmt.Println(“I am”, _self.name)
}



func (_self *Cat) say() {
fmt.Println(“I am”, _self.name)
}



func main() {
var d1 Animal= Dog{name:”wangCai1”}
d1.say() //因为Dog 的value类型实现了Animal接口



var d2 Animal= &Dog{name:”wangCai2”}
d2.say() //因为dDog 的指针类型实现了Animal接口



var c1 Animal= Cat{name:”miaoMiao1”}
c1.say() //因为Cat 的value类型没有实现了Animal接口,所以报错



var c2 Animal= &Cat{name:”miaoMiao2”}
c2.say() //因为Cat 的指针类型实现了Animal接口
}
类型必须实现接口的所有方法,才能表示它 “实现” 了该接口,如下:



type Animal interface {
say()
doSome()
}



type Dog struct {
name string
}
func (_self Dog) say() {
fmt.Println(“I am”, _self.name)
}
func (_self *Dog) doSome() {
fmt.Println(“I will do something”)
}



func main() {
// 报错,因为Dog的value类型实现了Animal接口的say方法没有实现doSome方法
var d1 Animal= Dog{name:”wangCai1”}
d1.say()



//因为dDog 的指针类型实现了Animal接口集的所有方法
var d2 Animal= &Dog{name:”wangCai2”}
d2.say()
}



4



方法集和嵌入



什么是嵌入



go语言中,所谓的嵌入,即把一个类型作为另外一个类型的匿名字段。如下:



type Person struct {
age int8
name string
}



type Student struct {
Person //嵌人 Persion类型
}
go语言通过嵌入组合,来实现继承的行为。于是,我们就可以通过Student类型的实例,访问Persion类型的变量和方法。如下:



var s = Student{}
s.name = “dq”
值类型(T)嵌入和指针类型(*T)嵌入的区别



type Student1 struct {
Person //值类型的嵌入
}
type Student2 struct {
*Person //指针类型的嵌入
}
要理解这个区别,就有知道go语言中类型的默认值。如下:
数值类型(如int8、int16、uint等),默认值为0;
布尔类型,默认值为false;
字符串类型,默认值为””
指针、通道、切片、字典等,默认值为nil
复合类型的默认值,为所包含类型的默认值。
所以:
type Person struct {
age int8
name string
}



func (s Person) showName() {
fmt.Println(s.name)
}



func (s *Person) setName(newName string) {
s.name = newName
}



type Student1 struct {
Person //Student1包含了Person,那么Student1对应的value和pointer包含Person
}



type Student2 struct {
*Person
}



// 内嵌类型 Persion默认值为 Person{age:0, name:””}
s1 := Student1{}
s1.setName(“student1_01”) // ok
s1.showName()



// 内嵌类型 *Persion默认值为 nil
s2 := &Student2{}
s2.setName(“student1_02”) //Error,由于目前内嵌类型的值为nil,会触发报错
s2.showName()



// 给嵌入类型一个复制,就ok了
s3 := &Student2{Person:&Person{age:3, name:”s3”}}
//s3 := &Student2{&Person{age:3, name:”s3”}} 和上一行等价
s3.showName()
在上面的案例中变量s2中嵌入类型默认值为nil,故会报错:panic: runtime error: invalid memory address or nil pointer dereference
所以,针对指针嵌入类型,在使用前,需要赋值。
嵌入和方法集的关系



类型 S 包含匿名字段 T,则 S和S 方法集包含 T 方法。
类型 S 包含匿名字段 *T,则 S和 *S 方法集包含 T + *T 方法。
不管嵌入的是T还是
T,*S方法集,包含 T + *T 方法。
下面,通过案例来解析,如下,思考Student1的指针和实例类型,以及Student2的指针和实例类型,是否实现了Human接口呢?
type Human interface { //定义接口
showName()
setName(string)
}



type Person struct {
age int8
name string
}
func (s Person) showName() { // Person类型的实例和指针,都实现了Human的showName方法
fmt.Println(s.name)
}
func (s *Person) setName(newName string) { // 只有Person类型的指针,才实现了Human的setNanme方法
s.name = newName
}



type Student1 struct { // 以值类型,嵌入
Person
}



type Student2 struct { // 以指针类型,嵌入
Person
}
解析:
应用上面规则1,由于Student1通过实体类型(T)方式,嵌入Person,所以 Stuednt1的实例类型,仅仅包含了接受者为Person的方法,即不包含setName()方法,所以Student1的实例类型,没有实现Human接口,不能赋值给Human接口;应用上面规则3, Stuednt1的指针类型,包含了接受者为Person和接受者为
Person的方法,即Stuednt1的指针类型,实现了Human接口。
应用上面规则2,由于Student2通过指针类型(*T)方式,嵌入Person,所以Student2的指针类型和实例类型,都实现了Human接口。
所以:
// Error 应用上面的关系判断第1条规则,因为Student1实例类型的方法集中,仅仅包含Person的实例方法集,即仅仅包含showName()方法,所以Student1的实例类型,没有实现Human接口
var s1 Human = Student1{} //报错:Student1 does not implement Human (setName method has pointer receiver)
s1.setName(“student1_01”)
s1.showName()



var s2 Human = &Student1{} //ok 应用第1条和弟3条规则
s2.setName(“student1_02”)
s2.showName()



var s3 Human = Student2{&Person{}} //ok ,应用第2条规则
s3.setName(“student2_01”)
s3.showName()



var s4 Human = &Student2{&Person{}} //ok ,应用第2条规则
s4.setName(“student2_02”)
s4.showName()


Category golang