Go语言反对面向对象编程,然而又和传统的面向对象语言如C++,Java等略有不同:Go语言没有类class的概念,只有构造体strcut,其能够领有属性,能够领有办法,咱们能够通过构造体实现面向对象编程。Go语言也有接口interface的概念,其定义一组办法汇合,构造体只有实现接口的所有办法,就认为其实现了该接口,构造体类型变量就能赋值给接口类型变量,这相当于面向对象中的多态。另外,Go语言也能够有继承的概念,不过是通过构造体的"组合"实现的。
构造体
Go语言基于构造体实现面向对象编程,与类class的概念比拟相似,构造体能够领有属性,也能够领有办法;咱们通过点号拜访构造体任意属性或者办法。个别定义形式如下所示:
package mainimport "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变量呢?其实是编译过程中做了一些解决,申明的构造体办法,以及构造体办法的调用,都和目前看到的不太一样。底层编译生成的函数如下:
//输出参数类型为StudentStudent.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 mainimport "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+0type."".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 mainimport "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 //办法2type."".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()+0type."".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 mainimport "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,"".Animaltype.*"".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+0type."".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 := 111fmt.Println(a)//结构eface变量eface.type = type.inteface.data = runtime.convT64(a)fmt.Println(eface)
说到这里还有一个比拟有意思的景象,因为任何类型都能转化为interface{},nil转化之后还等于nil吗?刚开始写Go语言,老是搞不清楚,明明最后值是nil,作为interface{}类型传递到函数之后,再判断居然不等于nil了!当初晓得了,空接口interface{}对应的变量用eface示意,必定是不会等于nil的。
package mainimport "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 mainimport "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了。
//类型断言,转化为构造体Humanhuman := v.(Human) //伪代码:if eface.type != type."".Human { runtime.panicdottypeE()}human = *eface.data//类型断言,转化为构造体Humanhuman, 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语言反对面向对象编程。接口的定义以及应用,接口继承,接口的定义等,须要咱们重点了解。