共计 9490 个字符,预计需要花费 24 分钟才能阅读完成。
Go 语言反对面向对象编程,然而又和传统的面向对象语言如 C ++,Java 等略有不同:Go 语言没有类 class 的概念,只有构造体 strcut,其能够领有属性,能够领有办法,咱们能够通过构造体实现面向对象编程。Go 语言也有接口 interface 的概念,其定义一组办法汇合,构造体只有实现接口的所有办法,就认为其实现了该接口,构造体类型变量就能赋值给接口类型变量,这相当于面向对象中的多态。另外,Go 语言也能够有继承的概念,不过是通过构造体的 ” 组合 ” 实现的。
构造体
Go 语言基于构造体实现面向对象编程,与类 class 的概念比拟相似,构造体能够领有属性,也能够领有办法;咱们通过点号拜访构造体任意属性或者办法。个别定义形式如下所示:
package main
import "fmt"
//type 关键字用于定义类型;Student 构造体领有两个属性 / 字段
type Student struct {
Name string
Score int
}
// 构造体办法,办法中能够应用构造体变量;func (s Student) Study() {s.Score += 10}
// 构造体指针办法,办法中能够应用构造体指针变量
func (s *Student) Study1() {s.Score += 10}
func main() {
stu := Student{
Name: "张三",
Score: 60,
}
stu1 := &stu
fmt.Println(stu.Score) //60
//stu 与 stu1 变量,别离执行 Study 与 Study1 办法
stu.Study()
fmt.Println(stu.Score) //60
stu.Study1()
fmt.Println(stu.Score) //70
stu1.Study()
fmt.Println(stu1.Score) //70
stu1.Study1()
fmt.Println(stu1.Score) //80
}
留神办法 Study 与办法 Study1 的申明,Study 归属构造体类型变量,Study1 归属构造体指针类型变量;两个办法中都批改了 Score 属性。main 办法中相应的定义了构造体变量 stu,构造体指针变量 stu1;别离执行 Study & Study1 办法,变量 stu 与 stu1 的 Score 属性会发生变化吗?执行后果如上所示,在解释之前读者能够思考下为什么是这样的后果。另外,办法 Study 属于构造体类型,为什么 stu1 变量能够调用呢?而办法 Study1 属于构造体指针类型,stu 也能够调用。
在答复下面问题之前,咱们先思考下,Study/Study1 办法中为什么能间接应用 stu/sut1 变量呢?其实是编译过程中做了一些解决,申明的构造体办法,以及构造体办法的调用,都和目前看到的不太一样。底层编译生成的函数如下:
// 输出参数类型为 Student
Student.Study
// 输出类型为 *Student,函数定义:(*Student).Study {
//Ax 寄存器第一个参数,就是 *Student 指针;拷贝构造体数据
MOVQ (AX), DX
MOVQ 8(AX), BX
MOVQ 16(AX), CX
// 传递构造体参数
// 最终还是调用 Student.Study 函数
CALL Student.Study
}
// 输出参数类型为 *Student
(*Student).Study1
能够看到,Study 办法底层编译生成了两个函数;而 Study1 只编译生成一个函数。编译生成的函数,第一个参数都是构造体变量,或者构造体指针变量,这下明确了,原来是通过第一个参数传递过来的。而 4 种调用形式编译过程也做了一些批改:
//stu.Study 办法调用,拷贝 stu 变量作为输出参数
CALL Student.Study(SB)
//stu.Study1,stu 变量地址作为输出参数
CALL (*Student).Study1(SB)
//stu1.Study,stu1 是指针,拷贝指针指向的构造体作为输出参数
CALL Student.Study(SB)
//stu1.Study1,stu1 指针变量作为输出参数
CALL (*Student).Study1(SB)
再强调一次 Go 语言是按值传递参数的。联合下面的形容咱们阐明下 4 种调用形式下 Score 属性最终后果:1)stu.Study,stu 变量作为输出参数,按值传递,传递的是数据正本,所以 Score 不会扭转;2)stu.Study1,以 stu 变量地址作为输出参数,传递的是地址,函数内的数据批改,stu 变量必定会同步批改;3)stu1.Study,stu1 变量尽管是指针,然而调用 Student.Study 函数时,依然传递的是 stu1 指向构造体的数据正本,所以 Score 不会扭转;4)stu1.Study1,以 stu1 指针变量作为输出参数,函数内的数据批改,stu1 指向的数据必定会同步批改。
最初再思考一个问题,构造体变量占多少字节内存呢?这就看构造体的属性定义了,构造体占用的内存大小等于所有字段占用内存大小之和,当然还要思考内存对齐。比方构造体 Student,蕴含一个字符串 16 字节(字符串长度 8 字节 + 字符串指针 8 字节),蕴含一个整型 8 字节,所以 Student 类型变量须要 24 字节内存。而拜访 Student 类型变量的属性,其实只须要简略的变量首地址加属性偏移量就行了。那构造体的办法呢?只存储属性不须要存储办法吗?当然是不须要了,因为构造体办法的调用,在编译阶段就确定了具体的函数。
构造体 - 继承
面向对象有一个很重要的概念叫继承,子类能够继承父类的某些属性或者办法,Go 语言构造体也反对继承;不过语法与传统面向对象语言有些不同,更像是通过组合来实现的继承。如上面程序所示:
package main
import "fmt"
type Human struct {
Name string
Age int
}
func (h Human)Say() {say := fmt.Sprintf("I am %s, my age is %d", h.Name, h.Age)
fmt.Println(say)
}
type Student struct {
Human
Score int
}
func (s Student)Study() {say := fmt.Sprintf("I am %s, my age is %d, my score is %d", s.Name, s.Age, s.Score)
fmt.Println(say)
}
func main() {
var stu Student
stu.Name = "zhangsan"
stu.Age = 18
stu.Score = 90
stu.Say()
stu.Study()}
构造体 Student 蕴含构造体 Human,能够看到 stu 变量类型为构造体 Student,然而咱们能够间接操作属性 Name/Age,以及办法 Say,而这些都是构造体 Human 的属性和办法。那么,Go 语言是如何保护这类继承关系呢?再进一步,咱们操作构造体属性或者办法时,Go 语言如何判断该构造体是否蕴含这些属性以及办法呢?
其实,Go 语言所有类型,都有其对应的类型定义,能够在文件 runtime/type.go 查看。如构造体类型 structtype,structfield 定义了构造体属性,method 定义了构造体办法;如指针类型 ptrtype;如函数类型 functype 等。咱们通过 ”type xxx struct” 形式定义的构造体,其所有信息都在 structtype;通过 ”go tool compile” 也能够看到咱们自定义的所有类型。
type."".Student SRODATA
rel 96+8 t=1 type..namedata.Human.+0 // 属性 1
rel 104+8 t=1 type."".Human+0
rel 120+8 t=1 type..namedata.Score.+0 // 属性 2
rel 128+8 t=1 type.int+0
rel 144+4 t=5 type..namedata.Say.+0 // 办法 1
rel 148+4 t=26 type.func()+0
rel 152+4 t=26 "".(*Student).Say+0
rel 156+4 t=26 "".Student.Say+0
rel 160+4 t=5 type..namedata.Study.+0 // 办法 2
rel 164+4 t=26 type.func()+0
rel 168+4 t=26 "".(*Student).Study+0
rel 172+4 t=26 "".Student.Study+0
type."".Human SRODATA
rel 96+8 t=1 type..namedata.Name.+0 // 属性 1
rel 104+8 t=1 type.string+0
rel 120+8 t=1 type..namedata.Age.+0 // 属性 2
rel 128+8 t=1 type.int+0
rel 144+4 t=5 type..namedata.Say.+0 // 办法 1
rel 148+4 t=26 type.func()+0
rel 152+4 t=26 "".(*Human).Say+0
rel 156+4 t=26 "".Human.Say+0
能够看到,自定义类型属于 SRODATA,只读。临时不须要一行一行去了解,咱们先简略看看能不能获取一些有用信息。type.””.Student 类型定义,蕴含了属性 type..namedata.Human(类型 type.””.Human),以及属性 type..namedata.Score(类型 type.int);蕴含办法 ””.Student.Say,以及办法 ””.Student.Study。基于这些信息,也就相当于构造体 Student 领有了属性 Name/Age,以及办法 Say。
最初,构造体类型 structtype 定义如下:
type structtype struct {
typ _type // 公共 type 类型,所有类型首先蕴含该公共字段
fields []structfield // 属性
// 构造体前面还跟有办法定义 method
}
type _type struct {
size uintptr // 该类型占多少字节内存
hash uint32
kind uint8 // 类型,如 kindStruct,kindString,kindSlice 等
// 等等
}
接口
Go 语言也有接口 interface 的概念,其定义一组办法汇合,构造体并不需要申明实现某借口,其只有实现接口的所有办法,就认为其实现了该接口,构造体类型变量就能赋值给接口类型变量。依据这些形容咱们能够晓得,只有当构造体类型变量赋值给接口类型变量时,Go 语言才会校验构造体是否实现了该接口,在这之前是不会校验也齐全没有必要校验的。
Go 语言接口应用形式通常如下:
package main
import "fmt"
type Animal interface {Eat()
Move()}
type Human struct {
Name string
Age int
}
func (h Human)Eat() {say := fmt.Sprintf("I am %s, I can eat", h.Name)
fmt.Println(say)
}
func (h Human)Move() {say := fmt.Sprintf("I am %s, I can move", h.Name)
fmt.Println(say)
}
func main() {
var animal Animal
animal = Human{Name: "zhangsan", Age: 20}
animal.Eat()
animal.Move()}
变量 animal 的类型为接口 Animal,咱们将构造体 Human 类型赋值给变量 animal,而构造体 Human 实现了办法 Eat/Move;办法调用 animal.Eat 以及 animal.Move,其实执行的是构造体 Human 的办法。再扩大一下,变量 animal 类型是 Animal 接口,其赋值的是什么构造体,最终拜访的就是什么构造体的办法,这是不是能够了解为面向对象常说的多态呢?
变量 animal 在内存是如何保护存储呢?变量 animal 占多大字节内存呢?通过变量 animal,又是如何找到其对应其对应构造体类型的属性呢?以及办法呢?貌似变量 animal 会比较复杂,须要存储构造体 Human 的所有属性,还须要存储所有办法的地址。的确是这样,接口类型变量的定义在 runtime/runtime2.go 文件:
type iface struct {
tab *itab
data unsafe.Pointer // 指向构造体变量,为了获取构造体变量的属性
}
type itab struct {
inter *interfacetype //interfacetype 即接口类型定义,其蕴含接口申明的所有办法;_type *_type // 构造体类型定义
fun [1]uintptr // 柔性数组,长度是可变的,存储了所有办法地址(从构造体类型中拷贝过去的)}
itab 也相当于自定义类型(构造体赋值给接口,主动生成的),其定义当然也能够通过 ”go tool compile” 查看:
// 构造体 (指针) 类型变量赋值给接口类型变量,主动创立对应 itab 类型
go.itab."".Human,"".Animal SRODATA
rel 0+8 t=1 type."".Animal+0 //interfacetype
rel 8+8 t=1 type."".Human+0 // 构造体 type 定义
rel 24+8 t=-32767 "".(*Human).Eat+0 // 办法 1
rel 32+8 t=-32767 "".(*Human).Move+0 // 办法 2
type."".Animal SRODATA
rel 96+4 t=5 type..namedata.Eat.+0 // 办法 1
rel 100+4 t=5 type.func()+0
rel 104+4 t=5 type..namedata.Move.+0 // 办法 2
rel 108+4 t=5 type.func()+0
type."".Human SRODATA
rel 96+8 t=1 type..namedata.Name.+0 // 属性 1
rel 104+8 t=1 type.string+0
rel 120+8 t=1 type..namedata.Age.+0 // 属性 2
rel 128+8 t=1 type.int+0
rel 144+4 t=5 type..namedata.Eat.+0 // 办法 1
rel 148+4 t=26 type.func()+0
rel 152+4 t=26 "".(*Human).Eat+0
rel 156+4 t=26 "".Human.Eat+0
rel 160+4 t=5 type..namedata.Move.+0 // 办法 2
rel 164+4 t=26 type.func()+0
rel 168+4 t=26 "".(*Human).Move+0
rel 172+4 t=26 "".Human.Move+0
另外留神,animal = Human{}形式赋值时,会将原始构造体变量拷贝一份正本,iface.data 指向的是该正本数据;animal = &Human{}形式赋值时,iface.data 指向的是原始构造体变量。联合上述这些类型的定义,咱们能够画出接口变量,构造体变量,接口类型,构造体类型等关系示意图:
最初,不晓得读者有没有遇到过这样的谬误:
package main
import "fmt"
type Animal interface {Eat()
Move()}
type Human struct {
}
func (h *Human)Eat() {fmt.Println("Eat")
}
func (h Human)Move() {fmt.Println("Move")
}
func main() {
var animal1 Animal
animal1 = &Human{}
animal1.Move()
animal1.Eat()
// 这样却能调用
h := Human{}
h.Eat()
h.Move()
// 这样却语法错误
/**
var animal Animal
animal = Human{}
animal.Move()
animal.Eat()
//cannot use Human{…} (value of type Human) as type Animal in assignment:
//Human does not implement Animal (Eat method has pointer receiver)
*/
}
初学 Go 语言可能会比拟蛊惑,办法接受者能够是构造体或者构造体指针,接口变量能够赋值为构造体或者构造体指针。然而当遇到下面程序:animal 赋值为构造体变量,Eat 办法接收者为构造体指针,居然编译谬误,提醒构造体 Human 没有实现接口 Animal 的办法,并且阐明 Eat 办法接受者为构造体指针。而 animal1 变量赋值为构造体指针,却既能调用 Eat 办法,也能调用 Move 办法。为什么呢?
其实咱们在定义了构造体 Human 后,Go 语言不止定义了 type.””.Human 一种类型,还定义了构造体指针类型,咱们通过通过 ”go tool compile” 看一下:
// 构造体 (指针) 类型变量赋值给接口类型变量,主动创立对应 itab 类型
go.itab.*"".Human,"".Animal
type.*"".Human SRODATA
rel 72+4 t=5 type..namedata.Eat.+0 // 办法 1
rel 76+4 t=26 type.func()+0
rel 80+4 t=26 "".(*Human).Eat+0
rel 84+4 t=26 "".(*Human).Eat+0
rel 88+4 t=5 type..namedata.Move.+0 // 办法 2
rel 92+4 t=26 type.func()+0
rel 96+4 t=26 "".(*Human).Move+0
rel 100+4 t=26 "".(*Human).Move+0
type."".Human SRODATA
rel 96+4 t=5 type..namedata.Move.+0 // 办法 1
rel 100+4 t=26 type.func()+0
rel 104+4 t=26 "".(*Human).Move+0
rel 108+4 t=26 "".Human.Move+0
这下明确了,构造体 Human 类型只有 Move 办法,而构造体 Human 指针类型有 Eat 以及 Move 办法;所以在向接口 Animal 类型赋值时,构造体变量无奈编译通过。然而咱们又发现,构造体变量 h,却能够调用 Eat 以及 Move 办法,不是说构造体 Human 类型只有 Move 办法吗?其实这是编译阶段做了解决,将变量 h 的地址(也就是构造体 Human 指针类型)作为参数传递给 Eat 办法了。
这一点要特地留神,办法接收者不论是构造体还是构造体指针,通过构造体变量或者构造体指针变量调用,都是没有问题的。然而,一旦赋值给接口类型变量,编译时会做类型查看,发现构造体类型没有实现某些办法,可是会导致语法错误的。
再扩大思考一下为什么要这么设计呢?构造体变量赋值给接口类型变量,不是一样能够获取到该构造体地址呢?不同样能够调用 Eat 办法。为什么不设计成这样呢?起因其实下面曾经解释过了,animal = Human{}形式赋值时,会将原始构造体变量拷贝一份正本,iface.data 指向的是该正本数据,这时候获取到的地址,还是原始构造体变量的地址吗?
空接口
Go 语言将接口分为两种:带办法的接口,个别比较复杂,用 iface 示意;不带办法的接口也就是空接口,个别当咱们不晓得变量类型时,会申明变量类型为空接口(interface{}),其余类型能够转化为空接口类型。将某一类型变量转化为空接口时,仍然须要保护原始变量类型,以及数据,Go 语言用 eface 示意空接口变量,定义如下:
type eface struct {
_type *_type // 变量的理论类型
data unsafe.Pointer // 数据指针
}
咱们常常应用 fmt.Println 函数向控制台输入变量,其输出参数类型为空接口,在调用该函数时,肯定会触发类型转化,将原始变量转化为 eface 变量:
a := 111
fmt.Println(a)
// 结构 eface 变量
eface.type = type.int
eface.data = runtime.convT64(a)
fmt.Println(eface)
说到这里还有一个比拟有意思的景象,因为任何类型都能转化为 interface{},nil 转化之后还等于 nil 吗?刚开始写 Go 语言,老是搞不清楚,明明最后值是 nil,作为 interface{}类型传递到函数之后,再判断居然不等于 nil 了!当初晓得了,空接口 interface{}对应的变量用 eface 示意,必定是不会等于 nil 的。
package main
import "fmt"
func main() {var a map[string]int = nil
fmt.Println(a == nil) //true
test(a)
}
func test(v interface{}) {fmt.Println(v == nil) //false
}
最初,任意类型转化为 interface{}之后,还能转化回来吗?当然是能够的,Go 语言能够应用类型断言将接口转化为其余类型,应用形式如下:
package main
import "fmt"
type Human struct {Name string}
func main() {h := Human{Name: "zhangsan"}
var v interface{} = h // 构造体类型转化为 interface{}
human := v.(Human) // 类型断言,转化为构造体 Human
fmt.Println(human.Name)
}
是不是很简略?然而应用类型断言的时候肯定要留神,如果类型不匹配,可是会呈现 panic 异样的!其实 v.(Human)能够返回两个值,第一个转化的类型变量,第二个 bool 值代表是否是该类型,这时候就不会有 panic 了。
// 类型断言,转化为构造体 Human
human := v.(Human)
// 伪代码:if eface.type != type."".Human {runtime.panicdottypeE()
}
human = *eface.data
// 类型断言,转化为构造体 Human
human, ok := v.(Human)
if eface.type == type."".Human {
ok = true
human = *eface.data
}
对于 interface{}类型变量,其实咱们也能够很不便获取到其类型,这样就能依据不同类型执行不同业务逻辑了。如将变量转化为字符串函数能够通过如下形式:
func ToStringE(i interface{}) (string, error) {switch s := i.(type) {
case string:
return s, nil
case bool:
return strconv.FormatBool(s), nil
case float64:
return strconv.FormatFloat(s, 'f', -1, 64), nil
// 等等
}
总结
构造体以及接口是 Go 语言十分重要的两个概念;与传统面向对象语言的类 class 以及接口十分相似;正因为构造体与接口的存在,咱们才说 Go 语言反对面向对象编程。接口的定义以及应用,接口继承,接口的定义等,须要咱们重点了解。