【新人系列】Golang 入门(十四):接口

✍ 个人博客:https://blog.csdn.net/Newin2020?type=blog
📝 专栏地址:https://blog.csdn.net/newin2020/category_12898955.html
📣 专栏定位:为 0 基础刚入门 Golang 的小伙伴提供详细的讲解,也欢迎大佬们一起交流~
📚 专栏简介:在这个专栏,我将带着大家从 0 开始入门 Golang 的学习。在这个 Golang 的新人系列专栏下,将会总结 Golang 入门基础的一些知识点,并由浅入深的学习这些知识点,方便大家快速入门学习~
❤️ 如果有收获的话,欢迎点赞 👍 收藏 📁 关注,您的支持就是我创作的最大动力 💪

1. 鸭子类型

鸭子类型,是一种动态编程语言的对象推断策略,核心在于关注对象能如何被使用,而非对象的类型本身。在 Go 语言中,它通过接口的方式得到了完美的支持。
根据使用需求,开发者在使用 “鸭子” 的时候确定其是否具备必要的 “技能”,而无需关心它是否为真正的 “鸭子”。

具体来说,Go 语言中的鸭子类型是通过 Interface 来实现的多态性。与 Python 等动态类型的语言不同,Go 并不要求类型显式声明实现某个接口,只要实现了相关的方法即可。在鸭子类型这种设计风格中,开发者不关心对象是什么类型,只关心对象有没有特定的方法。

总的来说,鸭子类型提供了一种非侵入式的接口,任何类型,只要实现了该接口中的方法,都可以被视为同一类型处理。

2. 接口详解

2.1 快速了解

在 Go 语言中,接口是一种定义了一组方法签名的抽象类型。接口定义了对象的行为规范,而不关心对象的具体实现。接口中的方法只有声明,没有实现。任何类型只要实现了接口中定义的所有方法,就被称为实现了该接口。

//接口的定义
type Duck interface {
    //方法的申请
    Gaga()
    Walk()
    Swimming()
}

type pskDuck struct {
    legs int
}

func (pd *pskDuck) Gaga() {
    fmt.Println("嘎嘎")
}

func (pd *pskDuck) Walk() {
    fmt.Println("this is pskduck walking")
}

func (pd *pskDuck) Swimming() {
    fmt.Println("this is pskduck swiming")
}

func main() {
    var d Duck = &pskDuck {}
    d.Walk()
}

多个接口对应一个结构体

type MyWriter interface {
    Write(string) error
}

type MyCloser interface {
    Close() error
}

type writerCloser struct {}

func (wc *writerCloser) Write(string) error {
    fmt.Println("write string")
    return nil
}

func (wc *writerCloser) Close() error {
    fmt.Println("close")
    return nil
}

func main() {
    var mw MyWriter = &writerCloser {}
    var mc MyCloser = &writerCloser {}
}

接口嵌套

type MyWriter interface {
    Write(string)
}

type MyReader interface {
    Read() string
}

type MyReadWriter interface {
    MyWriter
    MyReader
    ReadWrite()
}

type SreadWriter struct {}

func (s *SreadWriter) Write(s2 string) {
    fmt.Println("write")
}

func (s *SreadWriter) Read() string {
    fmt.Println("read")
    return ""
}

func (s *SreadWriter) ReadWrite() {
    fmt.Println("read write")
}

func main() {
    var mrw MyReadWriter = &SreadWriter {}
    mrw.Read()
}

2.2 空接口类型

空接口的类型可以接受任意类型的数据,它只需要记录这个类型在哪,是什么类型的就足够了。下面这个 _type 就指向接口的动态类型元数据,而 data 指向接口的动态值。

在这里插入图片描述

一个空接口的变量 e 在它被赋值以前,_type 和 data 都为 nil。现在我们有一个 *os.File 类型的变量 f,在上面类型系统小节了解过后,我们知道 *os.File 对应下面这样类型的元数据。

var e interface{}
f, _ := os.Open("test.txt")

在这里插入图片描述

如果把 f 赋值给 e,而又因为 f 本身就是个指针,所以这个 data 就等于 f,且 _type 指向 *os.File 的类型元数据。

var e interface{}
f, _ := os.Open("test.txt")
e = f

在这里插入图片描述

我们已经知道类型元数据这里可以找到 *os.File 的方法元数据数组,里面就有我们常用的 Read 和 Write 这些方法的描述信息。

在这里插入图片描述

这就是空接口类型变量赋值前后的变化,下面来看看非空接口。

2.3 非空接口类型

非空接口就是有方法列表的接口类型,一个变量要想赋值给一个非空接口类型,必须要实现接口类型的所有方法才行。与空接口类型一样,下面这个 data 字段指向接口的动态值,所以接口要求的方法列表以及接口动态类型信息一定要存储在下面这个 itab 结构体里。

type iface struct {
    tab    *itab    // 指向itab类型结构体
    data   unsafe.Pointer
}

type itab struct {
    inter  *interfacetype    // 指向interface类型的元数据
    _type  *_type            // 指向接口的动态类型元数据
    hash   uint32            // 从动态类型元数据中拷贝来的类型哈希值,在快速判断类型是否相等时使用
    _      [4]byte           
    fun    [1]uintptr        // 记录的是这个动态类型实现的那些接口要求的方法地址
}

// 接口类型的元数据
type interfacetype struct {
    typ      _type
    pkgpath  name
    mhdr     []imethod    // 接口要求的方法列表就记录在这里
}

还是来看个例子,我们声明一个 io.ReadWriter 类型的变量 rw,它被赋值以前 data 和 tab 都为 nil。

var rw io.ReadWriter
f, _ := os.Open("test.txt")

在这里插入图片描述

下面我们把一个 *os.File 类型的变量 f 赋值给 rw,此时 rw 的动态值就是 f了。

var rw io.ReadWriter
f, _ := os.Open("test.txt")
rw = f

而 tab 会指向一个 itab 结构体,并且它的接口类型为 io.ReadWriter,动态类型为 *os.File。同时,要注意 itab 这里的 fun,它会从动态类型元数据中拷贝接口要求的那些方法的地址,以便通过 rw 快速定位到方法,而无需再去类型元数据那里查找。

在这里插入图片描述

这就是非空接口的结构,但是关于 itab 还是需要额外注意一点,因为接口类型一旦确定了,动态类型也就确定了,那么 itab 的内容就不会改变了,所以这个 itab 结构体是可复用的。

2.4 缓存机制

实际上 Go 语言会把用到的 itab 结构体缓存起来,并且以接口类型和动态类型的组合为 key,以 itab 结构体指针为 value 构造一个哈希表,用于存储与查询 itab 缓存信息。

在这里插入图片描述

这里的哈希表与 map 底层的哈希表不同,是一种更为简便的设计。需要一个 itab 时,会先去这里查找。而 key 的哈希值是下面这样计算的,用接口类型的类型哈希值与动态类型的类型哈希值进行异或运算。

func itabHashFunc(inter *interfacetypr, typ *_type) uintptr {
    return uintptr(inter.typ.hash ^ typ.hash)
}

如果已经有对应的 itab 指针,就直接拿来使用。

在这里插入图片描述

如果 itab 缓存中没有,则需要创建一个 itab 结构体,然后添加到这个哈希表中。

在这里插入图片描述

明确了空接口和非空接口的数据结构,理解了接口动态值与动态类型在赋值前后的变化,接下来就可以看看类型断言的具体实现了。

3. slice 的常见错误

func mPrint(data ...interface{}) {
    for _, value := range datas {
        fmt.Println(value)
    }
}

func main() {
    //正确
    var data = []interface{} {
        "bobby", 18, 1.80
    }
    
    //下面这种在传入mPrint后会报错
    var data = []string {
        "bobby", "bobby2", "bobby3"
    }
    
    //正确
    var data []interface{}
    for _, value := range data {
        data = append(data, value)
    }
    
    mPrint(data...)
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值