乐趣区

关于go:15-GolangGo语言快速入门结构体与接口

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

退出移动版