教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 真的理解go interface了吗?

真的理解go interface了吗?

发布时间:2022-03-26   编辑:jiaochengji.com
教程集为您提供真的理解go interface了吗?等资源,欢迎您收藏本站,我们将为您提供最新的真的理解go interface了吗?资源
<h2>前言</h2><blockquote>我想,对于各位使用面向对象编程的程序员来说,"接口"这个名词一定不陌生,比如java中的接口以及c 中的虚基类都是接口的实现。但是<code>golang</code>中的接口概念确与其他语言不同,有它自己的特点,下面我们就来一起解密。</blockquote><h2>定义</h2>

Go 语言中的接口是一组方法的签名,它是 Go 语言的重要组成部分。简单的说,interface是一组method签名的组合,我们通过interface来定义对象的一组行为。interface 是一种类型,定义如下:

<pre><code class="go">type Person interface { Eat(food string) }</code></pre>

它的定义可以看出来用了 type 关键字,更准确的说 interface 是一种具有一组方法的类型,这些方法定义了 interface 的行为。<code>golang</code>接口定义不能包含变量,但是允许不带任何方法,这种类型的接口叫<code>empty interface</code>。

如果一个类型实现了一个<code>interface</code>中所有方法,我们就可以说该类型实现了该<code>interface</code>,所以我们我们的所有类型都实现了<code>empty interface</code>,因为任何一种类型至少实现了0个方法。并且<code>go</code>中并不像<code>java</code>中那样需要显式关键字来实现<code>interface</code>,只需要实现<code>interface</code>包含的方法即可。

<h3>实现接口</h3>

这里先拿<code>java</code>语言来举例,在<code>java</code>中,我们要实现一个<code>interface</code>需要这样声明:

<pre><code class="java">public class MyWriter implments io.Writer{}</code></pre>

这就意味着对于接口的实现都需要显示声明,在代码编写方面有依赖限制,同时需要处理包的依赖,而在<code>Go</code>语言中实现接口就是隐式的,举例说明:

<pre><code class="go">type error interface { Error() string } type RPCError struct { Code int64 Message string } func (e *RPCError) Error() string { return fmt.Sprintf("%s, code=%d", e.Message, e.Code) }</code></pre>

上面的代码,并没有<code>error</code>接口的影子,我们只需要实现<code>Error() string</code>方法就实现了<code>error</code>接口。在<code>Go</code>中,实现接口的所有方法就隐式地实现了接口。我们使用上述 <code>RPCError</code> 结构体时并不关心它实现了哪些接口,Go 语言只会在传递参数、返回参数以及变量赋值时才会对某个类型是否实现接口进行检查。

<code>Go</code>语言的这种写法很方便,不用引入包依赖。但是<code>interface</code>底层实现的时候会动态检测也会引入一些问题:

<ul><li>性能下降。使用interface作为函数参数,runtime 的时候会动态的确定行为。使用具体类型则会在编译期就确定类型。</li><li>不能清楚的看出struct实现了哪些接口,需要借助ide或其它工具。</li></ul><h2>两种接口</h2>

这里大多数刚入门的同学肯定会有疑问,怎么会有两种接口,因为<code>Go</code>语言中接口会有两种表现形式,使用<code>runtime.iface</code>表示第一种接口,也就是我们上面实现的这种,接口中定义方法;使用<code>runtime.eface</code>表示第二种不包含任何方法的接口,第二种在我们日常开发中经常使用到,所以在实现时使用了特殊的类型。从编译角度来看,golang并不支持泛型编程。但还是可以用<code>interface{}</code> 来替换参数,而实现泛型。

<h3>interface内部结构</h3>

Go 语言根据接口类型是否包含一组方法将接口类型分成了两类:

<ul><li>使用 <code>runtime.iface</code> 结构体表示包含方法的接口</li><li>使用 <code>runtime.eface</code> 结构体表示不包含任何方法的 <code>interface{}</code> 类型;</li></ul>

<code>runtime.iface</code>结构体在<code>Go</code>语言中的定义是这样的:

<pre><code class="go">type eface struct { // 16 字节 _type *_type data unsafe.Pointer }</code></pre>

这里只包含指向底层数据和类型的两个指针,从这个<code>type</code>我们也可以推断出Go语言的任意类型都可以转换成<code>interface</code>。

另一个用于表示接口的结构体是 <code>runtime.iface</code>,这个结构体中有指向原始数据的指针 <code>data</code>,不过更重要的是 <code>runtime.itab</code> 类型的 <code>tab</code> 字段。

<pre><code class="go">type iface struct { // 16 字节 tab *itab data unsafe.Pointer }</code></pre>

下面我们一起看看<code>interface</code>中这两个类型:

<ul><li><code>runtime_type</code></li></ul>

<code>runtime_type</code>是 Go 语言类型的运行时表示。下面是运行时包中的结构体,其中包含了很多类型的元信息,例如:类型的大小、哈希、对齐以及种类等。

<pre><code class="go">type _type struct { size uintptr ptrdata uintptr hash uint32 tflag tflag align uint8 fieldAlign uint8 kind uint8 equal func(unsafe.Pointer, unsafe.Pointer) bool gcdata *byte str nameOff ptrToThis typeOff }</code></pre>

这里我只对几个比较重要的字段进行讲解:

<ul><li><code>size</code> 字段存储了类型占用的内存空间,为内存空间的分配提供信息;</li><li><code>hash</code> 字段能够帮助我们快速确定类型是否相等;</li><li><code>equal</code> 字段用于判断当前类型的多个对象是否相等,该字段是为了减少 Go 语言二进制包大小从 <code>typeAlg</code> 结构体中迁移过来的);</li></ul><ul><li><code>runtime_itab</code></li></ul>

<code>runtime.itab</code>结构体是接口类型的核心组成部分,每一个 <code>runtime.itab</code> 都占 32 字节,我们可以将其看成接口类型和具体类型的组合,它们分别用 <code>inter</code> 和 <code>_type</code> 两个字段表示:

<pre><code class="go">type itab struct { // 32 字节 inter *interfacetype _type *_type hash uint32 _ [4]byte fun [1]uintptr }</code></pre>

<code>inter</code>和<code>_type</code>是用于表示类型的字段,<code>hash</code>是对<code>_type.hash</code>的拷贝,当我们想将 <code>interface</code> 类型转换成具体类型时,可以使用该字段快速判断目标类型和具体类型 <code>runtime._type</code>是否一致,<code>fun</code>是一个动态大小的数组,它是一个用于动态派发的虚函数表,存储了一组函数指针。虽然该变量被声明成大小固定的数组,但是在使用时会通过原始指针获取其中的数据,所以 <code>fun</code> 数组中保存的元素数量是不确定的;

内部结构就做一个简单介绍吧,有兴趣的同学可以自行深入学习。

<h3>空的interface(<code>runtime.eface</code>)</h3>

前文已经介绍了什么是空的<code>interface</code>,下面我们来看一看空的<code>interface</code>如何使用。定义函数入参如下:

<pre><code class="go">func doSomething(v interface{}){ }</code></pre>

这个函数的入参是<code>interface</code>类型,要注意的是,<code>interface</code>类型不是任意类型,他与C语言中的<code>void *</code>不同,如果我们将类型转换成了 <code>interface{}</code> 类型,变量在运行期间的类型也会发生变化,获取变量类型时会得到 <code>interface{}</code>,之所以函数可以接受任何类型是在 go 执行时传递到函数的任何类型都被自动转换成 <code>interface{}</code>。

那么我们可以才来一个猜想,既然空的 interface 可以接受任何类型的参数,那么一个 <code>interface{}</code>类型的 slice 是不是就可以接受任何类型的 slice ?下面我们就来尝试一下:

<pre><code class="go"> import ( "fmt" ) func printStr(str []interface{}) { for _, val := range str { fmt.Println(val) } } func main(){ names := []string{"stanley", "david", "oscar"} printStr(names) }</code></pre>

运行上面代码,会出现如下错误:<code>./main.go:15:10: cannot use names (type []string) as type []interface {} in argument to printStr</code>。

这里我也是很疑惑,为什么<code>Go</code>没有帮助我们自动把<code>slice</code>转换成<code>interface</code>类型的<code>slice</code>,之前做项目就想这么用,结果失败了。后来我终于找到了答案,有兴趣的可以看看原文,这里简单总结一下:<code>interface</code>会占用两个字长的存储空间,一个是自身的 methods 数据,一个是指向其存储值的指针,也就是 interface 变量存储的值,因而 slice []interface{} 其长度是固定的<code>N*2</code>,但是 []T 的长度是<code>N*sizeof(T)</code>,两种 slice 实际存储值的大小是有区别的。

既然这种方法行不通,那可以怎样解决呢?我们可以直接使用元素类型是interface的切片。

<pre><code class="go">var dataSlice []int = foo() var interfaceSlice []interface{} = make([]interface{}, len(dataSlice)) for i, d := range dataSlice { interfaceSlice[i] = d }</code></pre><h3>非空<code>interface</code></h3>

<code>Go</code>语言实现接口时,既可以结构体类型的方法也可以是使用指针类型的方法。<code>Go</code>语言中并没有严格规定实现者的方法是值类型还是指针,那我们猜想一下,如果同时使用值类型和指针类型方法实现接口,会有什么问题吗?

先看这样一个例子:

<pre><code class="go">package main import ( "fmt" ) type Person interface { GetAge () int SetAge (int) } type Man struct { Name string Age int } func(s Man) GetAge()int { return s.Age } func(s *Man) SetAge(age int) { s.Age = age } func f(p Person){ p.SetAge(10) fmt.Println(p.GetAge()) } func main() { p := Man{} f(&p) }</code></pre>

看上面的代码,大家对<code>f(&p)</code>这里的入参是否会有疑问呢?如果不取地址,直接传过去会怎么样?试了一下,编译错误如下:<code>./main.go:34:3: cannot use p (type Man) as type Person in argument to f: Man does not implement Person (SetAge method has pointer receiver)</code>。透过注释我们可以看到,因为<code>SetAge</code>方法的<code>receiver</code>是指针类型,那么传递给<code>f</code>的是<code>P</code>的一份拷贝,在进行<code>p</code>的拷贝到<code>person</code>的转换时,<code>p</code>的拷贝是不满足<code>SetAge</code>方法的<code>receiver</code>是个指针类型,这也正说明一个问题go中函数都是按值传递

上面的例子是因为发生了值传递才会导致出现这个问题。实际上不管接收者类型是值类型还是指针类型,都可以通过值类型或指针类型调用,这里面实际上通过语法糖起作用的。实现了接收者是值类型的方法,相当于自动实现了接收者是指针类型的方法;而实现了接收者是指针类型的方法,不会自动生成对应接收者是值类型的方法。

举个例子:

<pre><code class="go">type Animal interface { Walk() Eat() } type Dog struct { Name string } func (d *Dog)Walk() { fmt.Println("go") } func (d *Dog)Eat() { fmt.Println("eat shit") } func main() { var d Animal = &Dog{"nene"} d.Eat() d.Walk() }</code></pre>

上面定义了一个接口<code>Animal</code>,接口定义了两个函数:

<pre><code class="go">Walk() Eat()</code></pre>

接着定义了一个结构体<code>Dog</code>,他实现了两个方法,一个是值接受者,一个是指针接收者。我们通过接口类型的变量调用了定义的两个函数是没有问题的,如果我们改成这样呢:

<pre><code class="go">func main() { var d Animal = Dog{"nene"} d.Eat() d.Walk() }</code></pre>

这样直接就会报错,我们只改了一部分,第一次将<code>&Dog{"nene"}</code>赋值给了<code>d</code>;第二次则将<code>Dog{"nene"}</code>赋值给了<code>d</code>。第二次报错是因为,<code>d</code>没有实现<code>Animal</code>。这正解释了上面的结论,所以,当实现了一个接收者是值类型的方法,就可以自动生成一个接收者是对应指针类型的方法,因为两者都不会影响接收者。但是,当实现了一个接收者是指针类型的方法,如果此时自动生成一个接收者是值类型的方法,原本期望对接收者的改变(通过指针实现),现在无法实现,因为值类型会产生一个拷贝,不会真正影响调用者。

总结一句话就是:如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法。

<h2>类型断言</h2>

一个<code>interface</code>被多种类型实现时,有时候我们需要区分<code>interface</code>的变量究竟存储哪种类型的值,<code>go</code>可以使用<code>comma,ok</code>的形式做区分 <code>value, ok := em.(T)</code>:em 是 interface 类型的变量,T代表要断言的类型,value 是 interface 变量存储的值,ok 是 bool 类型表示是否为该断言的类型 T。总结出来语法如下:

<pre><code class="go"><目标类型的值>,<布尔参数> := <表达式>.( 目标类型 ) // 安全类型断言 <目标类型的值> := <表达式>.( 目标类型 )  //非安全类型断言</code></pre>

看个简单的例子:

<pre><code class="go">type Dog struct { Name string } func main() { var d interface{} = new(Dog) d1,ok := d.(Dog) if !ok{ return } fmt.Println(d1) }</code></pre>

这种就属于安全类型断言,更适合在线上代码使用,如果使用非安全类型断言会怎么样呢?

<pre><code class="go">type Dog struct { Name string } func main() { var d interface{} = new(Dog) d1 := d.(Dog) fmt.Println(d1) }</code></pre>

这样就会发生错误如下:

<pre><code class="go">panic: interface conversion: interface {} is *main.Dog, not main.Dog</code></pre>

断言失败。这里直接发生了 <code>panic</code>,所以不建议线上代码使用。

看过<code>fmt</code>源码包的同学应该知道,<code>fmt.println</code>内部就是使用到了类型断言,有兴趣的同学可以自行学习。

<h2>问题</h2>

上面介绍了<code>interface</code>的基本使用方法及可能会遇到的一些问题,下面出三个题,看看你们真的掌握了吗?

<h3>问题一</h3>

下面代码,哪一行存在编译错误?(多选)

<pre><code class="go">type Student struct { } func Set(x interface{}) { } func Get(x *interface{}) { } func main() { s := Student{} p := &s // A B C D Set(s) Get(s) Set(p) Get(p) }</code></pre>

答案:B、D;解析:我们上文提到过,<code>interface</code>是所有<code>go</code>类型的父类,所以<code>Get</code>方法只能接口<code>*interface{}</code>类型的参数,其他任何类型都不可以。

<h3>问题二</h3>

这段代码的运行结果是什么?

<pre><code class="go">func PrintInterface(val interface{}) { if val == nil { fmt.Println("this is empty interface") return } fmt.Println("this is non-empty interface") } func main() { var pointer *string = nil PrintInterface(pointer) }</code></pre>

答案:<code>this is non-empty interface</code>。解析:这里的<code>interface{}</code>是空接口类型,他的结构如下:

<pre><code class="go">type eface struct { // 16 字节 _type *_type data unsafe.Pointer }</code></pre>

所以在调用函数<code>PrintInterface</code>时发生了隐式的类型转换,除了向方法传入参数之外,变量的赋值也会触发隐式类型转换。在类型转换时,<code>*string</code>类型会转换成<code>interface</code>类型,发生值拷贝,所以<code>eface struct{}</code>是不为<code>nil</code>,不过<code>data</code>指针指向的<code>poniter</code>为<code>nil</code>。

<h3>问题三</h3>

这段代码的运行结果是什么?

<pre><code class="go"> type Animal interface { Walk() } type Dog struct{} func (d *Dog) Walk() { fmt.Println("walk") } func NewAnimal() Animal { var d *Dog return d } func main() { if NewAnimal() == nil { fmt.Println("this is empty interface") } else { fmt.Println("this is non-empty interface") } }</code></pre>

答案:<code>this is non-empty interface</code>. 解析:这里的<code>interface</code>是非空接口<code>iface</code>,他的结构如下:

<pre><code class="go">type iface struct { // 16 字节 tab *itab data unsafe.Pointer }</code></pre>

<code>d</code>是一个指向nil的空指针,但是最后<code>return d</code> 会触发<code>匿名变量 Animal = p</code>值拷贝动作,所以最后<code>NewAnimal()</code>返回给上层的是一个<code>Animal interface{}</code>类型,也就是一个<code>iface struct{}</code>类型。 <code>p</code>为nil,只是<code>iface</code>中的data 为nil而已。 但是<code>iface struct{}</code>本身并不为nil.

<h2>总结</h2>

<code>interface</code>在我们日常开发中使用还是比较多,所以学好它还是很必要,希望这篇文章能让你对<code>Go</code>语言的接口有一个新的认识,这一篇到这里结束啦,我们下期见~~~。

素质三连(分享、点赞、在看)都是笔者持续创作更多优质内容的动力!

建了一个Golang交流群,欢迎大家的加入,第一时间观看优质文章,不容错过哦(公众号获取)

结尾给大家发一个小福利吧,最近我在看[微服务架构设计模式]这一本书,讲的很好,自己也收集了一本PDF,有需要的小伙可以到自行下载。获取方式:关注公众号:[Golang梦工厂],后台回复:[微服务],即可获取。

我翻译了一份GIN中文文档,会定期进行维护,有需要的小伙伴后台回复[gin]即可下载。

翻译了一份Machinery中文文档,会定期进行维护,有需要的小伙伴们后台回复[machinery]即可获取。

我是asong,一名普普通通的程序猿,让gi我一起慢慢变强吧。我自己建了一个<code>golang</code>交流群,有需要的小伙伴加我<code>vx</code>,我拉你入群。欢迎各位的关注,我们下期见~~~

<span class="img-wrap"></span>

推荐往期文章:

<ul><li>machinery-go异步任务队列</li><li>Leaf—Segment分布式ID生成系统(Golang实现版本)</li><li>十张动图带你搞懂排序算法(附go实现代码)</li><li>Go语言相关书籍推荐(从入门到放弃)</li><li>go参数传递类型</li><li>手把手教姐姐写消息队列</li><li>常见面试题之缓存雪崩、缓存穿透、缓存击穿</li><li>详解Context包,看这一篇就够了!!!</li><li>go-ElasticSearch入门看这一篇就够了(一)</li><li>面试官:go中for-range使用过吗?这几个问题你能解释一下原因吗</li><li>学会wire依赖注入、cron定时任务其实就这么简单!</li><li>听说你还不会jwt和swagger-饭我都不吃了带着实践项目我就来了</li><li>[掌握这些Go语言特性,你的水平将提高N个档次(二)](</li></ul> 到此这篇关于“真的理解go interface了吗?”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
Golang中interface内部构造与面试真题分析
go interface
Go语言第一深坑-interface与nil的比较
真的理解go interface了吗?
36. 图解:Go 语言的反射三定律,也没什么难的嘛
Go 回答汇总三
Go语言interface详解
【golang基础】go语言中的方法和interface的实现
go语言学习-Any类型
Go 开发关键技术指南 | 为什么你要选择 Go?(内含超全知识大图)

[关闭]
~ ~