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语言反对面向对象编程。接口的定义以及应用,接口继承,接口的定义等,须要咱们重点了解。