李方治 發表於 2019-9-11 11:18:00

聊一聊 Go 语言的接口

<h3 id="楔子">楔子</h3>
<p><strong>当你使用 Go 一段时间之后,肯定会发现一个问题:那就是 Go 对类型的检查太严格了。当然这是一件好事,可以避免我们犯错误,但有些时候我们需要一个变量能够接收不同类型的值。比如在定义函数参数的时候,我们希望参数可以接收多种类型的值,那么这个时候该怎么做呢?</strong></p>
<p><strong>为了解决这一问题,Go 为我们提供了 interface{},也就是接口。</strong></p>
<h3 id="鸭子类型">鸭子类型</h3>
<p><strong>先来看看鸭子类型的定义:如果某个东西长得像鸭子,像鸭子一样游泳,像鸭子一样嘎嘎叫,那它就可以被看成是一只鸭子。</strong></p>
<p><strong>Duck Typing,鸭子类型,是动态编程语言的一种对象推断策略,它更关注对象能如何被使用,而不是对象的类型本身。Go 语言作为一门静态语言,它通过接口 interface{} 的方式完美支持鸭子类型。</strong></p>
<p><strong>笔者本人是主 Python 的,在 Python 中我们可以定义一个这样的函数:</strong></p>
<pre><code class="language-Python">def say_hi(obj):
    return obj.hi()
</code></pre>
<p><strong>在调用该函数的时候,你可以传入任意类型,那么 Python 解释器是如何做的呢?</strong></p>
<ul>
<li><strong>首先 Python 中的变量都是一个泛型指针 PyObject *。</strong></li>
<li><strong>当执行 obj.hi 的时候,解释器会调用 PyObject_GetAttr(obj, "hi") 获取返回 "hi" 对应的 value,结果也是一个PyObject *。如果没有找到的话,那么会抛出 AttributeError。</strong></li>
<li><strong>找到之后再通过 PyObject_CallObject 进行调用。</strong></li>
</ul>
<p><strong>所以我们看到给 obj 参数传递什么根本无关紧要,只要传递的变量可以调用 hi 即可,而且这一步是在运行时才发生的。换言之,如果报属性错误,那么一定是发生在运行时。</strong></p>
<p><strong>但对于静态语言来说,比如 Java,必须要显式地声明实现了某个接口之后,才能用在任何需要这个接口的地方。比如还是调用 obj.hi(),对于 Java 来说,在传入 obj 之前,必须显式地声明 obj 已经实现了 hi,否则在编译阶段就不会通过。这也是静态语言比动态语言更安全的原因。</strong></p>
<blockquote>
<p><strong>动态语言和静态语言的差别在此就有所体现:静态语言在编译期间就能发现类型不匹配的错误,不像动态语言,必须要运行到某一行代码才会报错。当然,静态语言要求程序员在编码阶段就要按照规定来编写程序,为每个变量规定数据类型,这在某种程度上加大了工作量,也增长了代码量。动态语言则没有这些要求,可以让人更专注在业务上,代码也更短,写起来更快。所以有优势就有劣势,鱼和熊掌往往是不可兼得的。</strong></p>
</blockquote>
<p><strong>Go 语言作为一门现代静态语言,是有后发优势的。它引入了动态语言的便利,同时又会进行静态语言的类型检查,写起来是非常 Happy 的。Go 采用了折中的做法:不要求类型显式地声明实现了某个接口,只要实现了相关的方法,编译器就能检测到。</strong></p>
<pre><code class="language-go">package main

import "fmt"

// 定义一个接口 People, 和接口中的函数:
type People interface {
    say(word string)
}

// 定义两个结构体
type Girl struct {name string}
type Boy struct {name string}

// 我们并没有将 Girl 显示地声明为接口 People 类型
// 只要实现了接口内部的方法,那么编译器就可以隐式地将其转成 People 类型
func (g Girl) say(word string) {
    fmt.Printf("%s say %s\n", g.name, word)
}

func (b Boy) say(word string) {
    fmt.Printf("%s say %s\n", b.name, word)
}

// 声明一个函数,参数 p 是 People 类型
func commonSay(p People, word string) {
    p.say(word)
}

func main() {
    g := Girl{"mashiro"}
    b := Boy{"sorata"}
    commonSay(g, "hello")// mashiro say hello
    commonSay(b, "hello")// sorata say hello
}
</code></pre>
<p><strong>看一下 commonSay 函数,它的第一个参数类型是 People,而 People 是一个接口类型。只要某个类型实现了接口 People 的所有方法,那么两者之间就可以相互转化。比如这里的 Girl 和 Boy 都实现了 say 方法,那么其实例就可以转成 People 类型。</strong></p>
<p><strong>因此有些时候,我们并不关心参数的类型是什么,而是更关心行为。比如这里的 commonSay,我们其实不在乎第一个参数的类型,我们只是希望它能够调用 say 方法即可。那么便可将第一个参数声明为接口类型,并规定实现该接口所需要实现的方法,因此它相比 Python 和 Java 会更友好一些。</strong></p>
<ul>
<li><code>Python:不关心是否实现了相应的方法,而是直接调用,如果找不到就报错;</code></li>
<li><code>Java:定义一个接口,规定了实现该接口所需要实现的方法,并且某个类型在实现接口的时候,还必须显式地指明自己实现的是哪一个接口;</code></li>
<li><code>Go:和 Java 类似,但不需要指明自己实现的是哪一个接口,只要该类型实现了指定接口里面的方法,那么编译器就认为该类型实现了指定接口;</code></li>
</ul>
<p><strong>所以 Go 里面如果想实现某个接口,不需要显式地声明,只需要实现对应接口中的方法即可。这样既没有 Java 那么啰嗦,又能够对类型进行检测。</strong></p>
<p><strong>顺带再提一下动态语言的特点:</strong></p>
<ul>
<li><code>变量指向的对象的类型是不确定的, 在运行期间才能确定;</code></li>
<li><code>函数和方法可以接收任何类型的参数, 且调用时不检查参数类型、不需要实现接口;</code></li>
</ul>
<p><strong>总结一下,鸭子类型是一种动态语言的风格,在这种风格中,一个对象有效的语义,不是由继承自特定的类决定,而是由它<font color="blue">当前方法和属性的集合</font>决定。Go 作为一种静态语言,通过接口实现了鸭子类型,实际上是 Go 的编译器在其中作了隐匿的转换工作。</strong></p>
<h3 id="值接收者和指针接收者的区别">值接收者和指针接收者的区别</h3>
<p><strong>方法能给用户自定义的类型添加新的行为。它和函数的区别在于方法有一个接收者,给函数添加一个接收者,那么它就变成了方法。接收者可以是<font color="blue">值接收者</font>,也可以是<font color="blue">指针接收者</font>。</strong></p>
<p><strong>在调用方法的时候,值类型既可以调用<font color="blue">值接收者</font>的方法,也可以调用<font color="blue">指针接收者</font>的方法;指针类型既可以调用<font color="blue">指针接收者</font>的方法,也可以调用<font color="blue">值接收者</font>的方法。也就是说,不管方法的接收者是什么类型,该类型的值和指针都可以调用,不必严格符合接收者的类型。</strong></p>
<p><strong>举个栗子:</strong></p>
<pre><code class="language-go">package main

import "fmt"

type girl struct {
    age int
}

// 值接收者
func (g girl) ageIncr1() {
    g.age++
}

// 指针接收者
func (g *girl) ageDecr1() {
    g.age--
}

func main() {
    g1 := girl{16}
    // 值类型 调用 值接收者的方法
    g1.ageIncr1()
    fmt.Println(g1.age) // 16
   
    g2 := girl{16}
    // 值类型 调用 指针接收者的方法
    g2.ageDecr1()
    fmt.Println(g2.age) // 15
   
    g3 := &amp;girl{16}
    // 指针类型 调用 值接收者的方法
    g3.ageIncr1()
    fmt.Println(g3.age) // 16
   
    g4 := &amp;girl{16}
    // 指针类型 调用 指针接收者的方法
    g4.ageDecr1()
    fmt.Println(g4.age) // 15
}
</code></pre>
<p><strong>我们暂时先不看结果,总之目前可以得出:不管接收者是什么类型,该类型的值和指针都可以调用。</strong></p>
<p><strong>实际上,当 调用者的类型 和 方法的接收者类型 不同时,其实是编译器在背后做了一些工作:</strong></p>
<p><img src="https://img2024.cnblogs.com/blog/3387406/202403/3387406-20240321210238920-99388811.png" alt="" loading="lazy"></p>
<p><strong>所以正如前面所说,不管接收者类型是值类型还是指针类型,都可以通过 值调用者 或 指针调用者 进行调用,这里面实际上通过语法糖起作用的。</strong></p>
<ul>
<li><code>如果是值接收者, 指针类型调用, 那么会通过 *指针 的方式调用, 并将值拷贝一份;</code></li>
<li><code>如果是指针接收者, 值类型调用, 那么会通过 &amp;值 的方式调用, 并将指针拷贝一份;</code></li>
</ul>
<p><strong>因此在调用之后 age 是否改变,取决于接收者到底是值类型还是指针类型,与调用者无关,因为 Go 编译器会进行转化。</strong></p>
<p><strong>但是问题来了,如果值接收者实现了一个方法,那么相同类型的指针接收者可不可以实现相同的方法呢?可以自己测试一下,答案是不行的。因为不管是值还是指针,它们都是同一类型的值和指针。</strong></p>
<p><strong>而在实现接口的时候,它们也是有区别的,举个栗子:</strong></p>
<pre><code class="language-go">package main

import "fmt"

type People interface {
    a()
    b()
}

type Girl struct {}

// 如果想实现某个接口, 那么只需要实现该接口中的方法即可
func (g Girl) a() {
    fmt.Println("girl -&gt; a")
}

func (g *Girl) b() {
    fmt.Println("girl -&gt; b")
}
// 但是我们看到方法 a 是值接收者实现的, 方法 b 是指针接收者实现的


func main() {
    g := Girl{}
    var p People
    // 此时将 g 赋值给 p 是报错的, 因为没有实现 People 中的所有方法
    // 但指针可以
    p = &amp;g
    p.a() //girl -&gt; a
    p.b() //girl -&gt; b
}
</code></pre>
<p><strong>所以区别如下:</strong></p>
<ul>
<li><code>实现了接收者是值类型的方法, 相当于自动实现了接口中接收者是指针类型的方法;</code></li>
<li><code>而实现了接收者是指针类型的方法, 不会自动生成接口中对应接收者是值类型的方法;</code></li>
</ul>
<p><strong>因此 Girl 实现了方法 a,会自动让 *Girl 也实现了方法 a;但是*Girl 实现了方法 b,不代表 Girl 也实现了方法 b。</strong></p>
<p><strong>如果此时将 g 强行赋值给 p,那么会出现如下编译错误。</strong></p>
<p><img src="https://img2024.cnblogs.com/blog/3387406/202403/3387406-20240321210246235-226403495.png" alt="" loading="lazy"></p>
<p><strong>报错信息给的很详细,不能将 Girl 类型的变量 g 作为 people 类型进行赋值,因为它没有实现 People 这个接口。括号里面提示:方法 b 的接收者是指针,并不会让值接收也拥有方法 b。因此 Girl 只实现了接口中的一个方法,它没有实现该接口的全部方法,因此不能赋值。</strong></p>
<p><strong>至于为什么会有这么一个设计,原因很简单:</strong></p>
<blockquote>
<p><strong>接收者是指针类型的方法,很可能在方法中会对接收者的属性进行更改操作,从而影响调用者;而对于接收者是值类型的方法,在方法中不会对调用者本身产生影响,因为不是同一个对象。</strong></p>
</blockquote>
<p><strong>所以当实现了接收者是值类型的方法时,会自动生成接口中接收者是对应指针类型的方法,因为两者都不会影响调用者。但是当实现了接收者是指针类型的方法,如果此时自动生成接口中接收者是值类型的方法,原本期望对调用者进行改变(通过指针实现),现在却无法实现,因为值类型会产生一个拷贝,不会真正影响调用者。</strong></p>
<p><strong>因此,只要记住一点就可以了:如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法,针对于接口而言。</strong></p>
<hr>
<p><font color="darkblue"><strong>那么问题又来了,在实现方法的时候到底应该采用值接收者还是指针接收者呢?</strong></font></p>
<p><strong>如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身,会影响调用者。</strong></p>
<p><strong>而使用指针作为方法的接收者的理由:</strong></p>
<ul>
<li><code>方法能够修改接收者指向的值;</code></li>
<li><code>避免在每次调用方法时复制该值, 在值的类型为大型结构体时, 这样做会更加高效;</code></li>
</ul>
<p><strong>但是判断使用值接收者还是指针接收者,不是由该方法是否修改了调用者来决定,而是应该基于该类型的本质。</strong></p>
<ul>
<li><strong>如果类型具备 "原始的本质",也就是说它的成员都是由 Go 语言里内置的原始类型组成,如字符串,整型值等,那就使用值接收者。</strong></li>
<li><strong>像内置的复合类型,如 slice,map,interface,channel,这些类型比较特殊,声明他们的时候,实际上是创建了一个 header,这些类型也是使用值接收者。这样的话,调用方法时也是直接 copy 一份,但介绍切片的时候说过,这些 header 本身不存储数据,所以直接传值即可,无需使用指针。</strong></li>
<li><strong>如果类型具备非原始的本质,不能被安全地复制,这种类型总是应该被共享,那就定义指针接收者的方法。比如 Go 源码里的文件结构体(struct File)就不应该被复制,而是应该只有一份实体。</strong></li>
</ul>
<h3 id="类型转换与断言">类型转换与断言</h3>
<p><strong>Go 语言中不允许隐式类型转换,也就是说 <code>=</code> 两边,不允许出现类型不相同的变量。而 类型转换、类型断言本质上都是把一个类型转换成另外一个类型,不同之处在于,类型断言是对接口变量进行的操作。</strong></p>
<p><strong>首先是类型转换,类型转换前后的两个类型要相互兼容才行,语法为:<code>&lt;变量&gt; := &lt;目标类型&gt; ( &lt;表达式&gt; )</code></strong></p>
<pre><code class="language-go">package main

import "fmt"

func main() {
    var i int = 9
    var j float64
    // 这个时候直接把 i 赋值给 j 是非法的
    // 因为 = 两边不允许出现类型不同的变量,我们需要进行类型转换
    j = float64(i)
    fmt.Println(j)// 9
   
    // 但是注意: int(3.14) 这种则不行, 因为会涉及截断, 前后值发生了改变
    // 将一个 float64 类型转化为 int, 除非这个 float64 的小数点后面是 0
    // 那如果遇见小数点后面不是 0 的浮点数该咋办呢?
    // 可以使用 math.Floor(), 会返回一个小数点后面是 0 的浮点数, 此时再转成 int 即可
}
</code></pre>
<p><strong>类型转换比较简单,再来看看类型断言。断言针对的是接口来说的,因为多个类型都可以实现同一个接口,那么如何判断一个接口变量对应哪一种类型呢?</strong></p>
<pre><code class="language-go">package main

import "fmt"

type People interface {
    say(word string)
}

type Girl struct {name string}
type Boy struct {name string}

func (g Girl) say(word string) {
    fmt.Printf("%s say %s\n", g.name, word)
}

func (b Boy) say(word string) {
    fmt.Printf("%s say %s\n", b.name, word)
}

func main() {
    var p1 People = Girl{"mashiro"}
    var p2 People = Boy{"sorata"}
    // 此时的 p1 和 p2 都是 People 类型
    // 那么问题来了,如何才能得到它们的原始类型呢
    var g = p1.(Girl)
    var b, ok = p2.(Boy)
    fmt.Println(g)// {mashiro}
    fmt.Println(b, ok)// {sorata} true
}
</code></pre>
<p><strong>将一个变量赋值给一个接口之后,这个变量的原始类型其实是被保存起来了的,如果想转成原始类型,那么需要进行断言。而断言有两种方式,一种是非安全类型断言,另一种是安全类型断言。</strong></p>
<ul>
<li><code>非安全类型断言: &lt;目标类型变量&gt; := &lt;接口变量&gt;.( &lt;目标类型&gt; )</code></li>
<li><code>安全类型断言: &lt;目标类型变量&gt;, &lt;布尔值变量&gt; := &lt;接口变量&gt;.( &lt;目标类型&gt; )</code></li>
</ul>
<p><strong>如果采用非安全类型断言,那么当目标类型指定错误,会抛出 panic。比如代码中变量 p1 的原始类型是 Girl,而如果写成 p1.(Boy),那么就会断言失败:main.People is main.Girl, not main.Boy。所以更建议使用安全类型断言,如果断言失败,那么返回目标类型的零值,和一个 false,但程序不会 panic。</strong></p>
<h3 id="空接口">空接口</h3>
<p><strong>一个类型如果实现了接口里面的方法,那么就实现了该接口,但要是接口里面没有定义方法呢?这种接口叫做空接口,显然任何类型都实现了空接口。</strong></p>
<pre><code class="language-go">package main

import "fmt"

func main() {
    // i 是一个空接口, 任何类型都实现了空接口
    // 这里给 i 赋一个整数 123
    var i interface{} = 123
    // 注意: i 是一个空接口, 我们要如何得知变量 i 背后的原始类型呢?
    // 可以使用断言的方式, 我们认定它是整型, 那么就可以这么做
    var num = i.(int)
    fmt.Println(num)// 123
    fmt.Println(num == 123)// true
   
    // 但如果断言是一个字符串的话, 显然是会报错的
    // 这个时候可以使用安全断言, 也就是采用两个变量来接收
    s, flag := i.(string)
    fmt.Printf("%q %t\n", s, flag)// "" false
    // 如果不能成功转换, 那么会得到 零值 和 false
    // 成功转换会得到 对应的值 和 true
   
    // 如果一个接口变量可以对应多种类型, 那么还可以使用 switch 语句
    switch i.(type) {
    case int:
      // 当匹配成功时, i 会被转成指定的类型
      fmt.Println("int", i)// int 123
    case float64:
      fmt.Println("float64", i)
    default:
      fmt.Println("Unknown type")
    }
}
</code></pre>
<p><strong>所以当一个函数既可以接收整数、字符串、浮点数的时候,就可以使用 interface{}, 然后进行断言。</strong></p>
<p><strong>目前我们便使用 interface 实现了多态,Go 语言并没有设计诸如虚函数、纯虚函数、继承、多重继承等概念,但它通过接口却非常优雅地支持了面向对象的特性。多态是一种运行时的行为,它有以下几个特点:</strong></p>
<ul>
<li><code>一种类型具有多种类型的能力;</code></li>
<li><code>允许不同的对象对同一消息做出灵活的反应;</code></li>
<li><code>以一种通用的方式对待使用的对象;</code></li>
<li><code>非动态语言必须通过继承和接口的方式来实现</code></li>
</ul>
<h3 id="接口类型的值在底层是怎么表示的">接口类型的值在底层是怎么表示的</h3>
<p><strong>如果某个结构体实现了某个接口的所有方法,那么该结构体实例便可以赋值给接口变量,举个例子:</strong></p>
<pre><code class="language-go">package main

import (
    "fmt"
)

type Car interface {
    Drive()
}

type Truck struct {
    Name string
}

func (t Truck) Drive() {
    fmt.Printf("拖拉机%s在狂飙", t.Name)
}

func main() {
    var c Car = Truck{"古尔丹"}
    fmt.Println(c)
}
</code></pre>
<p><strong>Car 是一个接口类型,内部定义了一些方法集,任何实现了这些方法的结构体实例都可以赋值给该类型的接口变量。所以接口相当于就是一个抽象,当你不关心对象的类型、而是行为时,那么便可以使用接口。比如函数的某个参数,我们不关心它到底是什么类型,只要它能调用指定的一系列方法即可,那么便可以声明为接口类型,至于接口变量的值具体对应哪一种类型,则需要通过断言来判断。</strong></p>
<p><strong>所以接口变量,本质上还是由结构体实例赋值得到的,以上都是已经说过的内容。但是问题来了,接口到底是怎么实现的呢?我们看一下它的底层定义。</strong></p>
<pre><code class="language-go">// runtime/runtime2.go
type iface struct {
    tab*itab
    data unsafe.Pointer
}
</code></pre>
<p><strong>接口在底层也是一个结构体,其中里面的 data 指向的便是具体的结构体实例,对于当前来说就是 Truck 实例。而第一个字段 tab 指向的也是结构体:</strong></p>
<pre><code class="language-go">type itab struct {
    // 接口类型本身
    inter *interfacetype
    // 接口存储的值的类型,做类型断言的时候会进行比较
    _type *_type
    hashuint32
    _   byte
    // 实现的方法,这里显示长度为 1,但具体多长取决于实现了多少个方法
    fun   uintptr
}
</code></pre>
<p><strong>还是比较简单的,但是注意:iface 不包括空接口,空接口的话专门实现了一个结构体叫 eface。</strong></p>
<pre><code class="language-go">type eface struct {
    _type *_type
    dataunsafe.Pointer
}
</code></pre>
<p><strong>因为空接口没有实现方法啥的,所以第一个字段就是一个 *_type,表示存储的值的具体类型。</strong></p>
<pre><code class="language-go">package main

import (
    "fmt"
    "unsafe"
)
type _type struct {
    size       uintptr
    ptrdata    uintptr
    hash       uint32
    tflag      uint8
    align      uint8
    fieldAlign uint8
    kind       uint8
    equal      func(unsafe.Pointer, unsafe.Pointer) bool
    gcdata    *byte
    str       int32
    ptrToThis int32
}

type eface struct {
    _type *_type
    dataunsafe.Pointer
}

func main() {
    var a interface{} = 123
    // 转成 eface 指针
    pointerEface := (*eface)(unsafe.Pointer(&amp;a))
    // 拿到里面的 data 属性,指向了具体的值
    // 由于是个整型,所以转成 *int,再解引用即可拿到具体的值
    fmt.Println(*(*int)(pointerEface.data))// 123
}
</code></pre>
<p><strong>所以当给一个接口变量赋值时,编译器会转成 iface 或 eface 之后赋值。</strong></p>
<h3 id="小结">小结</h3>
<ul>
<li><strong>Go 的隐式接口更加方便系统的扩展和重构。</strong></li>
<li><strong>结构体和指针都可以实现接口,结构体实现了接口的方法,会隐式地让其指针也实现该方法;但反过来则不是。</strong></li>
<li><strong>空接口可以承载任何类型的数据。</strong></li>
</ul>


</div>
<div id="MySignature" role="contentinfo">
    <style>.zstitle { width: 280px; text-align: center; font-size: 26px }
.zsimgweixin { width: 280px }
.zsimgali { width: 280px; padding: 0px 0px 50px 0px }
.zsleft { float: left }
.zsdiv { display: flex }
.zs { font-size: 30px }
.zspaddingright { padding: 0px 100px 0px 0px }</style>


&nbsp;
<p>如果觉得文章对您有所帮助,可以请囊中羞涩的作者喝杯柠檬水,万分感谢,愿每一个来到这里的人都生活愉快,幸福美满。</p>
<div class="zsdiv">
<div></div>
<div class="zspaddingright">
<p class="zstitle">
<span>微信赞赏</span>
<img class="zsimgweixin" src="https://images.cnblogs.com/cnblogs_com/traditional/1252937/o_21090712303361631017667_.pic_hd.gif">
</div>
<div class="zspaddingright">
<p class="zstitle">
<span>
支付宝赞赏</span>
<img class="zsimgali" src="https://images.cnblogs.com/cnblogs_com/traditional/1252937/o_2109071316231.png">
</div>

</div><br><br>
来源:https://www.cnblogs.com/traditional/p/11505189.html
頁: [1]
查看完整版本: 聊一聊 Go 语言的接口