超级苦工
阅读 232
《快学 Go 语言》第 9 课 —— 接口

接口是一个对象的对外能力的展现,我们使用一个对象时,往往不需要知道一个对象的内部复杂实现,通过它暴露出来的接口,就知道了这个对象具备哪些能力以及如何使用这个能力。

我们常说「佛有千面」,不同的人看到的佛并不一样。一个复杂的复合对象常常也可以是一个多面手,它具备多种能力,在形式上实现了多种接口。「弱水三千,只取一瓢」,使用时我们根据不同的场合来挑选满足需要的接口能力来使用这个对象即可。

Go 语言的接口类型非常特别,它的作用和 Java 语言的接口一样,但是在形式上有很大的差别。Java 语言需要在类的定义上显式实现了某些接口,才可以说这个类具备了接口定义的能力。但是 Go 语言的接口是隐式的,只要结构体上定义的方法在形式上(名称、参数和返回值)和接口定义的一样,那么这个结构体就自动实现了这个接口,我们就可以使用这个接口变量来指向这个结构体对象。下面我们看个例子

package main

import "fmt"

// 可以闻
type Smellable interface {
  smell()
}

// 可以吃
type Eatable interface {
  eat()
}

// 苹果既可能闻又能吃
type Apple struct {}

func (a Apple) smell() {
  fmt.Println("apple can smell")
}

func (a Apple) eat() {
  fmt.Println("apple can eat")
}

// 花只可以闻
type Flower struct {}

func (f Flower) smell() {
  fmt.Println("flower can smell")
}

func main() {
  var s1 Smellable
  var s2 Eatable
  var apple = Apple{}
  var flower = Flower{}
  s1 = apple
  s1.smell()
  s1 = flower
  s1.smell()
  s2 = apple
  s2.eat()
}

--------------------
apple can smell
flower can smell
apple can eat

上面的代码定义了两种接口,Apple 结构体同时实现了这两个接口,而 Flower 结构体只实现了 Smellable 接口。我们并没有使用类似于 Java 语言的 implements 关键字,结构体和接口就自动产生了关联。

空接口

如果一个接口里面没有定义任何方法,那么它就是空接口,任意结构体都隐式地实现了空接口。

Go 语言为了避免用户重复定义很多空接口,它自己内置了一个,这个空接口的名字特别奇怪,叫 interface{} ,初学者会非常不习惯。之所以这个类型名带上了大括号,那是在告诉用户括号里什么也没有。我始终认为这种名字很古怪,它让代码看起来有点丑陋。

空接口里面没有方法,所以它也不具有任何能力,其作用相当于 Java 的 Object 类型,可以容纳任意对象,它是一个万能容器。比如一个字典的 key 是字符串,但是希望 value 可以容纳任意类型的对象,类似于 Java 语言的 Map 类型,这时候就可以使用空接口类型 interface{}。

package main

import "fmt"

func main() {
    // 连续两个大括号,是不是看起来很别扭
    var user = map[string]interface{}{
        "age": 30,
        "address": "Beijing Tongzhou",
        "married": true,
    }
    fmt.Println(user)
    // 类型转换语法来了
    var age = user["age"].(int)
    var address = user["address"].(string)
    var married = user["married"].(bool)
    fmt.Println(age, address, married)
}

-------------
map[age:30 address:Beijing Tongzhou married:true]
30 Beijing Tongzhou true

代码中 user 字典变量的类型是 map[string]interface{},从这个字典中直接读取得到的 value 类型是 interface{},需要通过类型转换才能得到期望的变量。

接口变量的本质

在使用接口时,我们要将接口看成一个特殊的容器,这个容器只能容纳一个对象,只有实现了这个接口类型的对象才可以放进去。

接口变量作为变量来说它也是需要占据内存空间的,通过翻阅 Go 语言的源码可以发现,接口变量也是由结构体来定义的,这个结构体包含两个指针字段,一个字段指向被容纳的对象内存,另一个字段指向一个特殊的结构体 itab,这个特殊的结构体包含了接口的类型信息和被容纳对象的数据类型信息。

// interface structure
type iface struct {
  tab *itab  // 类型指针
  data unsafe.Pointer  // 数据指针
}

type itab struct {
  inter *interfacetype // 接口类型信息
  _type *_type // 数据类型信息
  ...
}

既然接口变量只包含两个指针字段,那么它的内存占用应该是 2 个机器字,下面我们来编写代码验证一下

package main

import "fmt"
import "unsafe"

func main() {
    var s interface{}
    fmt.Println(unsafe.Sizeof(s))
    var arr = [10]int {1,2,3,4,5,6,7,8,9,10}
    fmt.Println(unsafe.Sizeof(arr))
    s = arr
    fmt.Println(unsafe.Sizeof(s))
}

----------
16
80
16

数组的内存占用是 10 个机器字,但是这丝毫不会影响到接口变量的内存占用。

用接口来模拟多态

前面我们说到,接口是一种特殊的容器,它可以容纳多种不同的对象,只要这些对象都同样实现了接口定义的方法。如果我们将容纳的对象替换成另一个对象,那不就可以完成上一节我们没有完成的多态功能了么?好,顺着这个思路,下面我们就来模拟一下多态

package main

import "fmt"

type Fruitable interface {
    eat()
}

type Fruit struct {
    Name string  // 属性变量
    Fruitable  // 匿名内嵌接口变量
}

func (f Fruit) want() {
    fmt.Printf("I like ")
    f.eat() // 外结构体会自动继承匿名内嵌变量的方法
}

type Apple struct {}

func (a Apple) eat() {
    fmt.Println("eating apple")
}

type Banana struct {}

func (b Banana) eat() {
    fmt.Println("eating banana")
}

func main() {
    var f1 = Fruit{"Apple", Apple{}}
    var f2 = Fruit{"Banana", Banana{}}
    f1.want()
    f2.want()
}

---------
I like eating apple
I like eating banana

使用这种方式模拟多态本质上是通过组合属性变量(Name)和接口变量(Fruitable)来做到的,属性变量是对象的数据,而接口变量是对象的功能,将它们组合到一块就形成了一个完整的多态性的结构体。

接口的组合继承

接口的定义也支持组合继承,比如我们可以将两个接口定义合并为一个接口如下

type Smellable interface {
  smell()
}

type Eatable interface {
  eat()
}

type Fruitable interface {
  Smellable
  Eatable
}

这时 Fruitable 接口就自动包含了 smell() 和 eat() 两个方法,它和下面的定义是等价的。

type Fruitable interface {
  smell()
  eat()
}

接口变量的赋值

变量赋值本质上是一次内存浅拷贝,切片的赋值是拷贝了切片头,字符串的赋值是拷贝了字符串的头部,而数组的赋值呢是直接拷贝整个数组。接口变量的赋值会不会不一样呢?接下来我们做一个实验

package main

import "fmt"

type Rect struct {
    Width int
    Height int
}

func main() {
    var a interface {}
    var r = Rect{50, 50}
    a = r

    var rx = a.(Rect)
    r.Width = 100
    r.Height = 100
    fmt.Println(rx)
}

------
{50 50}

从上面的输出结果中可以推断出结构体的内存发生了复制,这个复制可能是因为赋值(a = r)也可能是因为类型转换(rx = a.(Rect)),也可能是两者都进行了内存复制。那能不能判断出究竟在接口变量赋值时有没有发生内存复制呢?不好意思,就目前来说我们学到的知识点还办不到。到后面的高级阶段我们将会使用 unsafe 包来洞悉其中的更多细节。不过我可以提前告诉你们答案是什么,那就是两者都会发生数据内存的复制 —— 浅拷贝。

指向指针的接口变量

如果将上面的例子改成指针,将接口变量指向结构体指针,那结果就不一样了

package main

import "fmt"

type Rect struct {
    Width int
    Height int
}

func main() {
    var a interface {}
    var r = Rect{50, 50}
    a = &r // 指向了结构体指针

    var rx = a.(*Rect) // 转换成指针类型
    r.Width = 100
    r.Height = 100
    fmt.Println(rx)
}

-------
&{100 100}

从输出结果中可以看出指针变量 rx 指向的内存和变量 r 的内存是同一份。因为在类型转换的过程中只发生了指针变量的内存复制,而指针变量指向的内存是共享的。

下一节我们开讲 Go 语言的错误处理

阅读《快学 Go 语言》更多章节,用微信扫一扫上面的二维码关注公众号「码洞」

关注下面的标签,发现更多相似文章
评论
相关推荐
国内知名Wchat团队荣誉出品顶级IM通讯聊天系统

国内知名Wchat团队荣誉出品顶级IM通讯聊天系统 团队言语在先: 想低价购买者勿扰(团队是在国内首屈一指的通信公司离职后组建,低价购买者/代码代码贩子者/同行勿扰/基础版本低于30w勿扰) 。想购买...

test

<div style="margin-top:20px"> <el-tabs v-model="editableTabsValue" tab-...

测试标题

chenyuncaic...

我的测试文章

这里我的标题 非 这里是被信息,我希望得到备注 我的第二个标题...

f'g'h'g'h'f'g'h'g'f'h

发布告白夫妇...

nestjs+vue+ts打造一个酷炫的星空聊天室

简介😛 闲暇时间想做一个聊天室来巩固前端技能,于是在2020年6月24号就开始了阿童木聊天室的开发之旅。😈 项目采用全 typescript 开发,这是为了以后的功能迭代打基础。当然,我本身也是很...

好几个放假

东方红格当然多层次发鬼地方...

《快学 Go 语言》第 16 课 —— 包管理 GOPATH 和 Vendor

来源: 知乎 原文: 《快学 Go 语言》第 16 课 —— 包管理 GOPATH 和 Vendor 到目前位置我们一直在编写单文件代码,只有一个 main.go 文件。本节我们要开始朝完整的项目结构...

dddd

...

《快学 Go 语言》第 15 课 —— 反射

来源: 知乎 原文: 《快学 Go 语言》第 15 课 —— 反射 反射是 Go 语言学习的一个难点,但也是非常重要的一个知识点。反射是洞悉 Go 语言类型系统设计的法宝,Go 语言的 ORM 库离不...

hello

*《快学 Go 语言》第 14 课 —— 魔术变性指针来源: 知乎 原文: 《快学 Go 语言》第 14 课 —— 魔术变性指针 本节我们要学习一些 Go 语言的魔法功能,通过内置的 unsafe 包...

《快学 Go 语言》第 14 课 —— 魔术变性指针

来源: 知乎 原文: 《快学 Go 语言》第 14 课 —— 魔术变性指针 本节我们要学习一些 Go 语言的魔法功能,通过内置的 unsafe 包提供的功能,直接操纵指定内存地址的内存。有了 unsa...

《快学 Go 语言》第 13 课 —— 并发与安全

来源: 知乎 原文: 《快学 Go 语言》第 13 课 —— 并发与安全 上一节我们提到并发编程不同的协程共享数据的方式除了通道之外还有就是共享变量。虽然 Go 语言官方推荐使用通道的方式来共享数据,...

《快学 Go 语言》第 12 课 —— 通道

来源: 知乎 原文: 《快学 Go 语言》第 12 课 —— 通道 不同的并行协程之间交流的方式有两种,一种是通过共享变量,另一种是通过队列。Go 语言鼓励使用队列的形式来交流,它单独为协程之间的队列...

《快学 Go 语言》第 11 课 —— 千军万马跑协程

来源: 知乎 原文: 《快学 Go 语言》第 11 课 —— 千军万马跑协程 协程和通道是 Go 语言作为并发编程语言最为重要的特色之一,初学者可以完全将协程理解为线程,但是用起来比线程更加简单,占用...

测试发布

测试文章发布...

《快学 Go 语言》第 10 课 —— 错误与异常

来源: 知乎 原文: 《快学 Go 语言》第 10 课 —— 错误与异常 Go 语言的异常处理语法绝对是独树一帜,在我见过的诸多高级语言中,Go 语言的错误处理形式就是一朵奇葩。一方面它鼓励你使用 C...

《快学 Go 语言》第 9 课 —— 接口

来源: 知乎 原文: 《快学 Go 语言》第 9 课 —— 接口 接口是一个对象的对外能力的展现,我们使用一个对象时,往往不需要知道一个对象的内部复杂实现,通过它暴露出来的接口,就知道了这个对象具备哪...

《快学 Go 语言》第 8 课 —— 结构体

来源: 知乎 原文: 《快学 Go 语言》第 8 课 —— 结构体 本节我们要开讲 Go 语言在数据结构上最重要的概念 —— 结构体。如果说 Go 语言的基础类型是原子,那么结构体就是分子。分子是原子...

《快学 Go 语言》第 7 课 —— 字符串

来源: 知乎 原文: 《快学 Go 语言》第 7 课 —— 字符串 字符串通常有两种设计,一种是「字符」串,一种是「字节」串。「字符」串中的每个字都是定长的,而「字节」串中每个字是不定长的。Go 语言...