关于golang:真的理解go-interface了吗

34次阅读

共计 7684 个字符,预计需要花费 20 分钟才能阅读完成。

前言

我想,对于各位应用面向对象编程的程序员来说,” 接口 ” 这个名词肯定不生疏,比方 java 中的接口以及 c ++ 中的虚基类都是接口的实现。然而 golang 中的接口概念确与其余语言不同,有它本人的特点,上面咱们就来一起解密。

定义

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

type Person interface {Eat(food string) 
}

它的定义能够看进去用了 type 关键字,更精确的说 interface 是一种 具备一组办法的类型 ,这些办法定义了 interface 的行为。golang 接口定义不能蕴含变量,然而容许不带任何办法,这种类型的接口叫empty interface

如果一个类型实现了一个 interface 中所有办法,咱们就能够说该类型实现了该 interface,所以咱们咱们的所有类型都实现了empty interface,因为任何一种类型至多实现了 0 个办法。并且go 中并不像 java 中那样须要显式关键字来实现 interface,只须要实现interface 蕴含的办法即可。

实现接口

这里先拿 java 语言来举例,在 java 中,咱们要实现一个 interface 须要这样申明:

public class MyWriter implments io.Writer{}

这就意味着对于接口的实现都须要显示申明,在代码编写方面有依赖限度,同时须要解决包的依赖,而在 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)
}

下面的代码,并没有 error 接口的影子,咱们只须要实现 Error() string 办法就实现了 error 接口。在 Go 中,实现接口的所有办法就隐式地实现了接口。咱们应用上述 RPCError 构造体时并不关怀它实现了哪些接口,Go 语言只会在传递参数、返回参数以及变量赋值时才会对某个类型是否实现接口进行查看。

Go语言的这种写法很不便,不必引入包依赖。然而 interface 底层实现的时候会动静检测也会引入一些问题:

  • 性能降落。应用 interface 作为函数参数,runtime 的时候会动静的确定行为。应用具体类型则会在编译期就确定类型。
  • 不能分明的看出 struct 实现了哪些接口,须要借助 ide 或其它工具。

两种接口

这里大多数刚入门的同学必定会有疑难,怎么会有两种接口,因为 Go 语言中接口会有两种表现形式,应用 runtime.iface 示意第一种接口,也就是咱们下面实现的这种,接口中定义方法;应用 runtime.eface 示意第二种不蕴含任何办法的接口,第二种在咱们日常开发中常常应用到,所以在实现时应用了非凡的类型。从编译角度来看,golang 并不反对泛型编程。但还是能够用interface{} 来替换参数,而实现泛型。

interface 内部结构

Go 语言依据接口类型是否蕴含一组办法将接口类型分成了两类:

  • 应用 runtime.iface 构造体示意蕴含办法的接口
  • 应用 runtime.eface 构造体示意不蕴含任何办法的 interface{} 类型;

runtime.iface构造体在 Go 语言中的定义是这样的:

type eface struct { // 16 字节
    _type *_type
    data  unsafe.Pointer
}

这里只蕴含指向底层数据和类型的两个指针,从这个 type 咱们也能够推断出 Go 语言的任意类型都能够转换成interface

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

type iface struct { // 16 字节
    tab  *itab
    data unsafe.Pointer
}

上面咱们一起看看 interface 中这两个类型:

  • runtime_type

runtime_type是 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
}

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

  • size 字段存储了类型占用的内存空间,为内存空间的调配提供信息;
  • hash 字段可能帮忙咱们疾速确定类型是否相等;
  • equal 字段用于判断以后类型的多个对象是否相等,该字段是为了缩小 Go 语言二进制包大小从 typeAlg 构造体中迁徙过去的);
  • runtime_itab

runtime.itab构造体是接口类型的外围组成部分,每一个 runtime.itab 都占 32 字节,咱们能够将其看成接口类型和具体类型的组合,它们别离用 inter_type 两个字段示意:

type itab struct { // 32 字节
    inter *interfacetype
    _type *_type
    hash  uint32
    _     [4]byte
    fun   [1]uintptr
}

inter_type 是用于示意类型的字段,hash是对 _type.hash 的拷贝,当咱们想将 interface 类型转换成具体类型时,能够应用该字段疾速判断指标类型和具体类型 runtime._type是否统一,fun是一个动静大小的数组,它是一个用于动静派发的虚函数表,存储了一组函数指针。尽管该变量被申明成大小固定的数组,然而在应用时会通过原始指针获取其中的数据,所以 fun 数组中保留的元素数量是不确定的;

内部结构就做一个简略介绍吧,有趣味的同学能够自行深刻学习。

空的 interface(runtime.eface

前文曾经介绍了什么是空的 interface,上面咱们来看一看空的interface 如何应用。定义函数入参如下:

func doSomething(v interface{}){}

这个函数的入参是 interface 类型,要留神的是,interface类型不是任意类型,他与 C 语言中的 void * 不同,如果咱们将类型转换成了 interface{} 类型,变量在运行期间的类型也会发生变化,获取变量类型时会失去 interface{},之所以函数能够承受任何类型是在 go 执行时传递到函数的任何类型都被主动转换成 interface{}

那么咱们能够才来一个猜测,既然空的 interface 能够承受任何类型的参数,那么一个 interface{}类型的 slice 是不是就能够承受任何类型的 slice?上面咱们就来尝试一下:


import ("fmt")

func printStr(str []interface{}) {
    for _, val := range str {fmt.Println(val)
    }
}

func main(){names := []string{"stanley", "david", "oscar"}
    printStr(names)
}

运行下面代码,会呈现如下谬误:./main.go:15:10: cannot use names (type []string) as type []interface {} in argument to printStr

这里我也是很纳闷,为什么 Go 没有帮忙咱们主动把 slice 转换成 interface 类型的 slice,之前做我的项目就想这么用,后果失败了。起初我终于找到了答案,有趣味的能够看看原文,这里简略总结一下:interface 会占用两个字长的存储空间,一个是本身的 methods 数据,一个是指向其存储值的指针,也就是 interface 变量存储的值,因此 slice []interface{} 其长度是固定的N*2,然而 []T 的长度是N*sizeof(T),两种 slice 理论存储值的大小是有区别的。

既然这种办法行不通,那能够怎么解决呢?咱们能够间接应用元素类型是 interface 的切片。

var dataSlice []int = foo()
var interfaceSlice []interface{} = make([]interface{}, len(dataSlice))
for i, d := range dataSlice {interfaceSlice[i] = d
}

非空interface

Go语言实现接口时,既能够构造体类型的办法也能够是应用指针类型的办法。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) 
}

看下面的代码,大家对 f(&p) 这里的入参是否会有疑难呢?如果不取地址,间接传过来会怎么样?试了一下,编译谬误如下:./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)。透过正文咱们能够看到,因为 SetAge 办法的 receiver 是指针类型,那么传递给 f 的是 P 的一份拷贝,在进行 p 的拷贝到 person 的转换时,p的拷贝是不满足 SetAge 办法的 receiver 是个指针类型,这也正阐明一个问题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()}

下面定义了一个接口Animal,接口定义了两个函数:

Walk()
Eat()

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

func main() {var d Animal = Dog{"nene"}
    d.Eat()
    d.Walk()}

这样间接就会报错,咱们只改了一部分,第一次将 &Dog{"nene"} 赋值给了 d;第二次则将Dog{"nene"} 赋值给了 d。第二次报错是因为,d 没有实现Animal。这正解释了下面的论断,所以,当实现了一个接收者是值类型的办法,就能够主动生成一个接收者是对应指针类型的办法,因为两者都不会影响接收者。然而,当实现了一个接收者是指针类型的办法,如果此时主动生成一个接收者是值类型的办法,本来冀望对接收者的扭转(通过指针实现),当初无奈实现,因为值类型会产生一个拷贝,不会真正影响调用者。

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

类型断言

一个 interface 被多种类型实现时,有时候咱们须要辨别 interface 的变量到底存储哪种类型的值,go能够应用 comma,ok 的模式做辨别 value, ok := em.(T)em 是 interface 类型的变量,T 代表要断言的类型,value 是 interface 变量存储的值,ok 是 bool 类型示意是否为该断言的类型 T。总结进去语法如下:

< 指标类型的值 >,< 布尔参数 > := < 表达式 >.(指标类型) // 平安类型断言
< 指标类型的值 > := < 表达式 >.(指标类型)  // 非平安类型断言

看个简略的例子:

type Dog struct {Name string}

func main() {var d interface{} = new(Dog)
    d1,ok := d.(Dog)
    if !ok{return}
    fmt.Println(d1)
}

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

type Dog struct {Name string}

func main() {var d interface{} = new(Dog)
    d1 := d.(Dog)
    fmt.Println(d1)
}

这样就会产生谬误如下:

panic: interface conversion: interface {} is *main.Dog, not main.Dog

断言失败。这里间接产生了 panic,所以不倡议线上代码应用。

看过 fmt 源码包的同学应该晓得,fmt.println外部就是应用到了类型断言,有趣味的同学能够自行学习。

问题

下面介绍了 interface 的根本应用办法及可能会遇到的一些问题,上面出三个题,看看你们真的把握了吗?

问题一

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

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)
}

答案:B、D;解析:咱们上文提到过,interface是所有 go 类型的父类,所以 Get 办法只能接口 *interface{} 类型的参数,其余任何类型都不能够。

问题二

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

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)
}

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

type eface struct { // 16 字节
    _type *_type
    data  unsafe.Pointer
}

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

问题三

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


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")
    }
}

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

type iface struct { // 16 字节
    tab  *itab
    data unsafe.Pointer
}

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

总结

interface在咱们日常开发中应用还是比拟多,所以学好它还是很必要,心愿这篇文章能让你对 Go 语言的接口有一个新的意识,这一篇到这里完结啦,咱们下期见~~~。

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

建了一个 Golang 交换群,欢送大家的退出,第一工夫观看优质文章,不容错过哦(公众号获取)

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

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

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

我是 asong,一名普普通通的程序猿,让 gi 我一起缓缓变强吧。我本人建了一个 golang 交换群,有须要的小伙伴加我vx, 我拉你入群。欢送各位的关注,咱们下期见~~~

举荐往期文章:

  • machinery-go 异步工作队列
  • Leaf—Segment 分布式 ID 生成零碎(Golang 实现版本)
  • 十张动图带你搞懂排序算法(附 go 实现代码)
  • Go 语言相干书籍举荐(从入门到放弃)
  • go 参数传递类型
  • 手把手教姐姐写音讯队列
  • 常见面试题之缓存雪崩、缓存穿透、缓存击穿
  • 详解 Context 包,看这一篇就够了!!!
  • go-ElasticSearch 入门看这一篇就够了(一)
  • 面试官:go 中 for-range 应用过吗?这几个问题你能解释一下起因吗
  • 学会 wire 依赖注入、cron 定时工作其实就这么简略!
  • 据说你还不会 jwt 和 swagger- 饭我都不吃了带着实际我的项目我就来了
  • [把握这些 Go 语言个性,你的程度将进步 N 个品位(二)](

正文完
 0