Skip to content

面向对象


面向对象有三大特点:封装、继承和多态

1.方法

给内置类型定义方法是不被允许的

接收者

接口类型是无效的方法接收者

在之前的 学习笔记(二) 中有提到过方法,其格式如下:

go
func (接收者) func_name(参数) 返回值 {
    //操作
}

学习了函数调用栈后,我们要知道 —— 接收者在函数调用中,其实是作为函数的参数来传递的。举个例子:

go
package main

import "fmt"

type A struct {
    name string
}

func (a A) getname() string { //值传递,不修改原本变量
    return a.name
}

func (pa *A) setname() { //指针传递,修改原本变量
    pa.name = "Hi ! " + pa.name
}

func main() {
    a := A{name: "Golang"}
    pa := &a

  //用方法变量来调用函数

    fmt.Println(a.getname()) //查看汇编代码,发现调用形式是:A.getname(a)
    a.setname()              //语法糖,编译期间会为转为 (&a).setname(),即(*A).getname(pa)

    fmt.Println(pa.getname()) //语法糖,编译期间会为转为 (*pa).setname(),即A.getname(a)
    pa.setname()              //调用形式是:(*A).getname(pa)

    //用方法表达式来调用函数,不建议这种方法

    //一、struct值只能调用 值接收者 的方法

    fmt.Println(A.getname(a)) //与fmt.Println(A.getname(*pa))等价
    A.setname(a)              //会报错

    //二、struct指针能调用 值接收者 和 指针接收者 的方法

    (*A).setname(&a)              //与(*A).getname(pa)等价
    fmt.Println((*A).getname(&a)) //经过编译器处理,最后还是调用了 A.getname(a)

}

注意:上述语法糖是在编译期间发挥作用的,所以像 A{name: "hello"}.setname() 这种不能在编译期间拿到地址的字面量,就没办法通过语法糖转换,会报错。

(*A).setname(&A{name: "hello"}) 用方法表达式的方式调用就能正常执行。

下面列几个图,更直观的展现不同接收者的方法调用:

img

img

方法表达式

上面一直在提方法表达式,方法表达式到底是个什么东西?

答:方法表达式就是 Function Value。

img

方法变量

上图的 a.GetName 就是一个方法变量。

方法变量实际上就是个闭包,是捕获了方法接收者的 Function Value。

img

既然知道了方法变量是闭包,那就需要注意:闭包的捕获列表可能只进行值拷贝,也可能造成变量逃逸。这些在之前的 函数进阶 中有讨论过,这里就不在赘述。

2.接口

在了解接口前,不得不先理解 Go 语言的类型系统:

类型系统

在 Go 语言中,内置类型和自定义类型都有对应的类型描述信息,称为它的 “类型元数据”,每种类型元数据都是全局唯一的。这些类型元数共同构成了 Go 语言的类型系统。

作为类型元数据头部信息的 Type 的数据结构

go
源码文件:src/internal/abi/type.go    line:20

type Type struct {
    Size_       uintptr  //类型大小
    PtrBytes    uintptr  //含有所有指针类型前缀大小
    Hash        uint32   //类型hash值;避免在哈希表中计算
    TFlag       TFlag   //二外类型信息标志
    Align_      uint8   //该类型变量对齐方式
    FieldAlign_ uint8   //该类型结构字段对齐方式
    Kind_       uint8    //类型编号
    
    Equal func(unsafe.Pointer, unsafe.Pointer) bool  //用于比较该类型对象的函数

    GCData    *byte    //gc数据
    Str       NameOff   //类型名字的偏移
    PtrToThis TypeOff   //指向该类型的指针类型,可以为零
}

这只是作为头部信息,在 Type 之后还要存储各种类型额外需要描述的信息:

img

在之前的版本中,Type 结构体是写在 src/runtime/type.go 中的,且结构体名称为 _type;

现在则是写在 src/internal/abi/type.go 中,在 runtime/type.go 中,以 type _type = abi.Type 别名使用

如果是自定义类型,后面还会有一个 UncommonType 结构体:

go
源码文件:src/internal/abi/type.go    line:197

type UncommonType struct {
    PkgPath NameOff // 记录类型所在的包路径
    Mcount  uint16  // 记录该类型的方法数
    Xcount  uint16  // 记录该类型导出的方法数
    Moff    uint32  // 记录这些方法元数据组成的数组相对于 UncommonType 结构体的偏移 offset from this uncommontype to [mcount]Method
    _       uint32  // unused
}
go
源码文件:src/internal/abi/type.go    line:186

type Method struct {
    Name NameOff // name of method
    Mtyp TypeOff // method type (without receiver)
    Ifn  TextOff // fn used in interface call (one-word receiver)
    Tfn  TextOff // fn used for normal method call
}

方法元数据的数据结构

img

接口(非空)

在 Go 语言中,接口是一种类型

接口的定义

接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。

如果用户定义的类型实现了某个接口类型声明的一组方法,那么这个用户定义的类型的值就可以赋给这个接口类型的值。

定义格式如下:

go
type 接口类型名 interface{
        方法名1( 参数列表1 ) 返回值列表1
        方法名2( 参数列表2 ) 返回值列表2

    }

接口的实现

go
package main

import "fmt"

type storage struct {
    disk string
}

type io_operate interface {
    read() *storage
    write(str string) *storage
}

func (A *storage) read() *storage {
    fmt.Println(A.disk)
    return A
}

func (A *storage) write(str string) *storage {    //storage 实现了 io_operate 定义的方法,称其实现了该接口
    A.disk = str
    return A
}

func main() {

    var x io_operate //接口类型变量能够存储所有实现了该接口的实例
    a := storage{disk: "A"}
    x = &a //因为是指针接收者实现方法,所以要取地址

    x.write("X")
    a.read() //X
    x.read() //X

    a.write("B")
    x.read() //B
    a.read() //B

}

可以看到,通过接口类型变量调用方法 和 通过结构体实例调用方法,两者的结果都是一样的。那为什么要多此一举使用接口呢?

接口的作用

如果是像上方那种小规模编程,使用接口确实意义不大,接口是编程规模化之后才起上作用的。接口就是为了抽象,当别人看到一个接口类型时,不知道它具体是做什么的,但可以知道实现它的方法能做什么。

例如,说话 这个抽象概念,当你遇到一个外国人,你听不懂他在说什么,但你知道他在说话。这就好似接口的作用。

接口的数据结构

go
源码文件:src/runtime/runtime2.go    line:205

type iface struct {
    tab  *itab        //指向接口动态类型元数据 和 接口要求的方法列表
    data unsafe.Pointer  //指向接口的动态值
}

img

一个 itab 表现为接口的一个实现,是可复用的,所以 Go 语言会将其缓存起来,通过哈希表来存储和查询 itab 信息:

img

空接口

空接口是非常特殊的接口,所有类型都实现了该接口,所以空接口类型变量可以存放所有值。Go 语言专门为其定义了一个结构体 eface:

go
源码文件:src/runtime/runtime2.go    line:210

type eface struct {
    _type *_type       //指向接口的动态类型元数据
    data  unsafe.Pointer  //指向接口的动态值
}

3.多态

Go 中的多态性是在接口的帮助下实现的。

多态的定义

多态是同一个操作,作用于不同对象,会有不同的过程和结果。简单来说,就是用相同的接口表示不同的实现。

Go 中的多态

举个例子,就很好理解了:

go
type Shaper interface {
    Area() float64
}

type Square struct {
    side float64
}

func (s Square) Area() float64 {
    return s.side * s.side
}

type Circle struct {
    radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.radius * c.radius
}

func ComputeArea(shaper Shaper) float64 {
    return shaper.Area()
}

func main() {
    s := Square{5}
    c := Circle{4}
    fmt.Println(ComputeArea(s)) // 输出 25
    fmt.Println(ComputeArea(c)) // 输出 50.26548245743669
}

4.反射

反射的概念

反射是一种检查interface变量的底层类型(type)和值(value)的机制。

反射三大定律

官方提供了三条定律来说明反射:

一、反射可以将interface类型变量转换成反射对象

提供了两个方法 reflect.TypeOfreflect.ValueOf 来获取到一个变量的反射类型和反射值:

go
package main

import (
    "fmt"
    "reflect"
)

func main() {

    var a = 1
    typea := reflect.TypeOf(a)
    valuea := reflect.ValueOf(a)
    fmt.Println(typea)     //int
    fmt.Println(valuea)    //1

}

二、反射可以将反射对象还原成interface对象

通过 reflect.Value.Interface 来获取到反射对象的 interface 对象

go
package main

import (
    "fmt"
    "reflect"
)

func main() {

    var a = 1
    valuea := reflect.ValueOf(a)

    b := valuea.Interface().(int)    //转成interface对象,再通过类型断言获取int类型
    fmt.Println(b)    //1

}

三、反射对象可修改,value值必须是可设置的

通过 reflect.Value.CanSet 来判断一个反射对象是否是可设置的。

若可设置,可通过 reflect.Value.Set 来修改反射对象的值。(不可设置,强制修改会 panic)

常用到 reflect.Value.Elem 来获取指针指向的值。

go
package main

import (
    "fmt"
    "reflect"
)

func main() {

    var x float64 = 3.4
    a := reflect.ValueOf(x)
    b := reflect.ValueOf(&x)

    fmt.Println(a.CanSet())        // false
    fmt.Println(b.CanSet())        // false
    fmt.Println(b.Elem().CanSet()) // true

b.Elem().SetFloat(3.14) //如果b的Kind不是Float32或Float64,或者CanSet()为false,会panic

fmt.Println(x) //3.14

}

能够看到,上面只有 b.Elem().CanSet() 为 true,这是 值拷贝 和 指针拷贝 的区别,想要修改对象,这两个是绕不开的。这两者的区别前几篇学习笔记中已经遇到很多了,无非是值拷贝修改副本,指针拷贝修改原变量,不再赘述。

反射的原理

这一部分非常复杂和绕,笔者的功力有限,不知如何写的清楚明白,这里推荐观看 幼麟实验室的视频 ,里面讲解的比较形象通透。

如有转载或 CV 的请标注本站原文地址