GO基础学习(4)接口

写在开头

非原创,知识搬运工,本节介绍了GO语言接口,接口的类型,接口方法接收者的差异,接口和nil的对比,接口的隐式转换
demo代码地址

往期回顾

  1. 基本数据类型
  2. slice/map/array
  3. 结构体

带着问题去阅读

  1. GO的接口是隐式的,还是和java一样显式的
  2. 接口方法的值接收者和指针接收者有啥区别
  3. 接口的实现一定要用指针吗?为什么
  4. 接口如何和nil做比较
  5. 接口和接口之间如何比较
  6. 空接口是和JS的Object一样的任意类型吗
  7. 接口底层是怎么实现的

1.隐式接口/鸭子类型

前文结构体中大致提到了一下接口,我们说空接口interface{}是类似js的Object,任何结构体都实现了该接口。
那有了结构体和实例不就行了,为什么还需要接口呢?接口的本质就是引入一个新的中间层,调用方可以通过接口与具体实现分离,解耦,即上层模块不再依赖下层模块的具体实现,只需要约定一个接口即可,隐藏底层实现,减少关注点。

比如sql语句查询,我们不关心底层数据库的实现,只关心利用接口返回的结果

GO中的接口都是隐式的,只要结构体实现了接口的所有方法,那么就隐式地实现了该接口,这是不是听起来很像我们学习面向对象常听到的一句话

If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.
某个东西长得像鸭子,像鸭子一样游泳,能嘎嘎叫,那它就可以被看做鸭子

GO通过接口的方式完美支持鸭子类型(Duck Typing)(实际上是编译器在其中做了隐匿的转换工作)

2.值接收者和指针接收者

一段很简单的demo

type Languge interface {
    sayHello()
    code()
}
type Go struct{}

func (g Go) sayHello() {
    fmt.Println("hello world go")
}

func (g *Go) code() {
    fmt.Println("code go")
}

func sayHello(l Languge) {
    l.sayHello()
    l.code()
}
func TestInterface(t *testing.T) {
    g := new(Go)
    sayHello(g)
}

接口/结构体方法和函数的区别就是方法有一个接收者,接收者可以是值接收者,也可以是指针接收者(code)
区别:

  • 结构体章中我们也聊过只有指针接收者可以改变结构体内部成员(实际上也是编译器完成实现的)
  • 实现了值接收者的方法,相当于自动实现了指针接收者的方法,但是实现了指针接收者的方法,不会自动实现对应值接收者类型的方法

第二个区别听起来比较乱,用上面的例子体验一下

func TestDiff(t *testing.T) {
    //var g Languge = &Go{} 这个能通过
    var g Languge = Go{} //这个不能通过
    g.code()
    g.sayHello()
}

这是一段一眼错误的代码,因为没用实例的指针(*GO)赋值给接口类型,那为什么不用指针就会报错呢?

 cannot use Go{} (value of type Go) as Languge value in variable declaration: Go does not implement Languge (method code has pointer receiver)

错误提示告诉我们结构体实例(GO)之所以不能作为接口的实现,是没有实现方法code!(method code has pointer receiver),我们再回头看上面的第二个观点恍然大悟:
GO{}类型实现了sayhello(值接收者),所以让*Go{}类型自动拥有了sayhello方法,但是*Go类型实现了code方法,却不能让Go{}类型拥有code方法
还看不懂?再总结一下

如果实现了接收者是值类型的方法,会隐式地实现接收者是指针类型的对应方法,反之则无

所以当我们将指针接收者方法code注释掉,就能通过

type Languge interface {
    sayHello()
}
type Go struct{}

func (g Go) sayHello() {
    fmt.Println("hello world go")
}

func sayHello(l Languge) {
    l.sayHello()
}
func TestInterface(t *testing.T) {
    g := new(Go)
    sayHello(g)
}

func TestDiff(t *testing.T) {
    var g Languge = Go{}
    g.sayHello()
}

所以并不是什么接口的实现必须是指针,只是某个方法没实现而已,但是接口的实例还是遇事不决用指针总没错。

3.接口的类型

先梳理一下几个概念

type a interface{} //这种叫接口
type b struct{} //这种叫结构体
a := b{} //a被称为b的实体或实例,b就是a的实体类型

没错,接口也有类型,一种是老朋友空接口,另一种是带有方法的接口。go程序在运行时使用runtime.iface表示后者,runtime.eface表示空接口

type iface struct {
    tab  *itab
    data unsafe.Pointer
}

type eface struct {
    _type *_type
    data  unsafe.Pointer
}

iface:

  • tab指向一个itab实体,表示接口的类型以及赋给这个接口的实体类型
  • data则指向接口具体的值,一般是指向堆的内存指针


    image.png

itab

type itab struct {
    inter *interfacetype
    _type *_type
    hash  uint32 // copy of _type.hash. Used for type switches.
    _     [4]byte
    fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
  • _type是实体的类型(上文Language的Go),包括内存对齐的方式,大小
  • inter是接口的类型
  • fun放置和接口方法对应的具体数据类型的方法地址,数组大小为1是因为只存储第一个方法的函数指针,更多方法则在之后的内存空间存储,这些方法是按照函数名称的字典序进行排序的

eface就维护_type字段存放实体类型,没有方法就不需要存储。
另外这个_type,GO中的各种数据类型都是在这个字段基础上,增加一些额外字段进行管理的,这也是反射实现的基础


image.png

最后需要注意的是空接口不意味着任意类型,将类型转为inteface类型,变量在运行期间类型也会发生变化

func Print(v interface{}) {
    println(v)
}

这里调用Print时会对v进行类型转换为interface{}

image.png

3.1接口的动态类型和动态值

继续对iface进行探索,tab指向类型信息(接口类型和实例类型),data是数据指针(实例),分别被称为动态类型T动态值V
只有当动态类型和动态值都为nil,该接口值==nil,下面有三个例子
例1

func TestCompareNil(t *testing.T) {
    var g Languge
    t.Log(g == nil) 
    fmt.Printf("g:%T,%v\n", g, g)

    var g1 *Go
    t.Log(g1 == nil)

    g = g1
    t.Log(g == nil)
    fmt.Printf("g:%T,%v", g, g)
}

true
g:<nil>,<nil>
true
false
g:*main.Go,<nil>

刚声明时都为动态类型T和值V都为nil,g==nil成立
之后赋值操作g=g1,动态类型T成为*main.Go,虽然动态值V=g1为nil,但是g==nil不成立(两个条件都要成立)
例2

func TestCompareNil2(t *testing.T) {
    var p func() Languge
    p = func() Languge {
        var g *Go = nil
        return g
    }
    g1 := p()
    t.Log(g1)
    t.Log(g1 == nil)
}

nil
false

第一个nil很容易理解,值为nil,(打印是获取值嘛)
第二个为false是因为p函数返回Language接口,发生隐式类型转换,动态值为nil,但是动态类型是*Go,所以为false
例3

func TestCompareNil3(t *testing.T) {
    var p func(v interface{}) bool
    p = func(v interface{}) bool {
        return v == nil
    }
    var g *Go
    t.Log(g == nil)
    t.Log(p(g))
}

true
false

这个是空接口隐式类型转换的例子,*GO转为interface{}类型,包含了动态类型信息GO,所以不成立

综上,这一类的判断主要考察的是对T有没有进行赋值,即使是其他类型转为eface空接口也能对T赋值

4.利用编译器自动检测类型是否实现接口

一个小技巧

var _ Language = (*Go)(nil)
var _ Language = GO{}

接口比较的例子

func TestCompareInterface(t *testing.T) {
    var g1 Languge = &Go{}
    var g2 Languge = &Go{}
    t.Log(g1 == g2)
    var t1 interface{} = int(1)
    var t2 interface{} = int(1)
    t.Log(t1 == t2)
    fmt.Printf("%T,%T,%v,%v\n", t1, t2, t1, t2)

    var t3 interface{} = int64(1)
    t.Log(t3 == t1)
    fmt.Printf("%T,%T,%v,%v\n", t1, t3, t1, t3)
}

true 
true
false

T和V都相等时才相等

5.断言和类型转换

5.1类型断言

type Person interface {
   Say() string
}

type Man struct {
   Name string
}

func (m *Man) Say() string {
   return "Man"
}

func main() {
   var p Person

    m := &Man{Name: "hhf"}
    p = m

    if m1, ok := p.(*Man); ok {
      fmt.Println(m1.Name)
    }
}

断言处对应的汇编代码如下

0x0087 00135 (main.go:23)   MOVQ    "".p+104(SP), AX
0x008c 00140 (main.go:23)   MOVQ    "".p+112(SP), CX
0x0091 00145 (main.go:23)   LEAQ    go.itab.*"".Man,"".Person(SB), DX
0x0098 00152 (main.go:23)   CMPQ    DX, AX

0x0091将go.itab.*"".Man和"".Person(SB)放入DX,比较判断两者是否相等,来判断person的真实类型是否是Man,本质上就是内存排布的比较,同理类型转换也是如此

6.VScode中寻找实现的接口

以gin框架的核心结构体Engine为例


image.png

想要查看该实例实现了哪个接口:

  1. 快捷键 ctrl+f12
  2. Go->Go to Implementation,中文界面就是"转到"->“转到实现”,同理查看接口实例也可以用这种办法


    image.png

参考:
1.深度解密接口
2.GO接口
3.GO面试题
4.go interface原理剖析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352

推荐阅读更多精彩内容