14 Go语言——接口interface详解

杨旭 · · 878 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

Go语言——接口interface详解

[TOC]

1、Duck Typing 概念

go语言中的duck typing并不是真正的duck typing,但是他是类似的概念,go语言接口的实现就可以看做为duck typing。举例:什么是鸭子?

其他面向对象的语言可能认为,活生生的鸭子才是鸭子,要定义它的属性和方法。但是go语言中,鸭子的定义并不是真实的对象,它是由使用者来决定到底什么是鸭子。比如,真的鸭子和玩具的鸭子。像鸭子走路,像鸭子叫(长得像鸭子),那么就是鸭子。所以,go语言中的接口就是这样,它不必显示的去声明它,它只关注是否实现了相应的方法。它描述的事物的外部行为,而非内部结构

面向对象的继承、抽象接口等等目的都是代码的复用。既然是复用,那就要从使用者的角度去想,我认为是什么样子它就是什么样子。我只关心这段代码结构能做哪些事情,我复用它,我才不管它符不符合常识。

go语言就是一个结构化类型系统,类似 duck typing。只要实现了接口的所有方法,就表示该类型实现了该接口。

2、GO 语言interface特点

  • 接口是一个或多个方法签名的集合
  • 只要某个类型拥有该接口的所有方法签名,即算实现该接口,无需显示声明实现了哪个接口,这称为 Structural Typing
  • 接口只有方法声明,没有实现,没有数据字段
  • 接口可以匿名嵌入其它接口,或嵌入到结构中
  • 将对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个复制品的指针,既无法修改复制品的状态,也无法获取指针
  • 只有当接口存储的类型和对象都为nil时,接口才等于nil
  • 接口调用不会做receiver的自动转换
  • 接口同样支持匿名字段方法
  • 接口也可实现类似OOP中的多态
  • 空接口可以作为任何类型数据的容器

3、接口定义

3.1 接口类型

go语言中的接口也是一种类型,他具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。 定义接口很简单,使用type关键字,其实就是定义一个结构体,但是内部只有方法的声明,没有实现。

type Stringer interface {//接口的定义就是如此的简单。
    String() string
}

3.2 接口的实现方式

go语言接口的独特之处就是,不需要显示的去实现接口。一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。 这种隐式实现接口的方式,同时也提高了灵活性。

type Stringer interface {
    String() string
}
type Printer interface {
    Stringer // 接口嵌⼊。
    Print()
}
type User struct {
    id int
    name string
}
func (self *User) String() string {
    return fmt.Sprintf("user %d, %s", self.id, self.name)
}
func (self *User) Print() {
    fmt.Println(self.String())
}
func main() {
    var t Printer = &User{1, "Tom"} // *User ⽅法集包含 String、 Print。
    t.Print()
}

上面的代码可以看到,一个类型只要实现了接口定义的所有方法(是指有相同名称、参数列表 (不包括参数名) 以及返回值 ),那么这个类型就实现了这个接口,可以说这个类型现在是这个接口类型,可以直接进行赋值(其实也是隐式转换),比如var t Printer = &User{1, "Tom"}。 那么既然如此,一个类型就可以实现多个接口,只要它拥有了这些接口类型的所有方法,那么这个类型就是实现了多个接口。同时这个类型也就是多种形式的存在,反过来说一个接口可以被不同类型实现,这就是go语言中的多态了。

3.3 interface{}空接口的实现

空接⼝ interface{} 没有任何⽅法签名,也就意味着任何类型都实现了空接⼝。其作⽤类似⾯向对象语⾔中的根对象 object。

3.4 类型断言

一个类型断言检查接口类型实例是否为某一类型 。语法为x.(T) ,x为类型实例,T为目标接口的类型。比如

value, ok := x.(T) x :代表要判断的变量,T :代表被判断的类型,value:代表返回的值,ok:代表是否为该类型。即:ok partern方式。

不过我们一般用switch进行判断,叫做 type switch。注意:不支持fallthrough.

func main() {
    var o interface{} = &User{1, "Tom"}
    switch v := o.(type) {
        case nil:             // o == nil
            fmt.Println("nil")
        case fmt.Stringer:     // interface
            fmt.Println(v)
        case func() string: // func
            fmt.Println(v())
        case *User:         // *struct
            fmt.Printf("%d, %s\n", v.id, v.name)
        default:
            fmt.Println("unknown")
    }
}

3.5 接口转换

可以将拥有超集的接口转换为子集的接口。

type User struct {
    id int
    name string
}
func (self *User) String() string {
    return fmt.Sprintf("%d, %s", self.id, self.name)
}
func main() {
    var o interface{} = &User{1, "Tom"}
    if i, ok := o.(fmt.Stringer); ok { // ok-idiom
        fmt.Println(i)
    }
    u := o.(*User)
    // u := o.(User) // panic: interface is *main.User, not main.User
    fmt.Println(u)
}

通过类型判断,如果不同类型转换会发生panic.

3.6 匿名接口

匿名接口可用作变量类型,或者是结构成员。

type Tester struct {
    s interface {
        String() string
    }
}
type User struct {
    id int
    name string
}
func (self *User) String() string {
    return fmt.Sprintf("user %d, %s", self.id, self.name)
}
func main() {
    t := Tester{&User{1, "Tom"}}
    fmt.Println(t.s.String())
}
//输出:
user 1, Tom

4、接口的内部实现

4.1 接口值

接口值可以使用 == 和 !=来进行比较。两个接口值相等仅当它们都是nil值或者它们的动态类型相同并且动态值也根据这个动态类型的==操作相等。因为接口值是可比较的,所以它们可以用在map的键或者作为switch语句的操作数。
然而,如果两个接口值的动态类型相同,但是这个动态类型是不可比较的(比如切片) ,将它们进行比较就会失败并且panic。

那么接口值内部到底是什么结构呢?

4.2 接口内部结构

// 没有方法的interface
type eface struct {
    _type *_type
    data  unsafe.Pointer
}

// 记录着Go语言中某个数据类型的基本特征
type _type struct {
    size       uintptr
    ptrdata    uintptr
    hash       uint32
    tflag      tflag
    align      uint8
    fieldalign uint8
    kind       uint8
    alg        *typeAlg
    gcdata    *byte
    str       nameOff
    ptrToThis typeOff
}

// 有方法的interface
type iface struct {
    tab  *itab
    data unsafe.Pointer
}

type itab struct {
    inter  *interfacetype
    _type  *_type
    link   *itab
    hash   uint32
    bad    bool
    inhash bool
    unused [2]byte
    fun    [1]uintptr
}

// interface数据类型对应的type
type interfacetype struct {
    typ     _type
    pkgpath name
    mhdr    []imethod
}

go语言interface的源码表示,接口其实是一个两个字段长度的数据结构。所以任何一个interface变量都是占用16个byte的内存空间。从大的方面来说,如图:

在这里插入图片描述

var n notifier n=user("Bill") 将一个实现了notifier接口实例user赋给变量n。那我们先来看有方法的接口的内部是怎么样的。接口n 内部两个字段 tab *itab 和 data unsafe.Pointer, 第一个字段存储的是指向ITable(j接口表)的指针,这个内部表包括已经存储值得类型和与这个值相关联的一组方法。第二个字段存储的是,指向所存储值的指针。注意:这里是将一个值传递给数组,并非指针,那么就会先将值拷贝一份,开辟内存空间存储,然后将此内存地址赋给接口的data字段。也就是说,值传递时,接口存储的值的指针其实是指向一个副本。

在这里插入图片描述

如果是将指针赋值给接口类型,那么第二个字段data存储的就是指针的拷贝,指向的是原来的内存。

再进一步了解,内部是如何存储的。

没有方法的interface 内部变量第一个字段为*_type 类型,这个_type记录这某种数据类型的基本信息,比如占用内存大小(size),数据类型名称等等。然后,第二个字段存储的还是指向值的指针。

每种数据类型都存在一个与之对应的_type结构体(Go语言原生的各种数据类型,用户自定义的结构体,用户自定义的interface等等)。

在这里引用的https://www.jianshu.com/p/700...

在这里插入图片描述

小结:总的来说接口是一个类型,它是一个struct,是一个或多个方法的集合。任何类型都可以实现接口,并且是隐式实现,可以同时实现多个接口。接口内部只有方法声明没有实现。接口内部存储的其实就是接口值的类型和值,一部分存储类型等各种信息,另一部分存储指向值的指针。如果是将值传给接口,那么这里第二个字段存储的就是原值的副本的指针。接口可以调用实现了接口的方法。

5、方法集

5.1 方法集定义

方法集:方法集定义了一组关联到给定类型的值或者指针的方法。定义方法时使用的接受者的类型决定了这个方法时关联到值,还是关联到指针,还是两个都关联。

// 这个示例程序展示 Go 语言里如何使用接口
 package main
 import (
     "fmt"
 )

 // notifier 是一个定义了
 // 通知类行为的接口
 type notifier interface {
     notify()
 }

 // user 在程序里定义一个用户类型
 type user struct {
     name string
     email string
 }

 // notify 是使用指针接收者实现的方法
 func (u *user) notify() {
     fmt.Printf("Sending user email to %s<%s>\n",
     u.name,
     u.email)
 }

 // main 是应用程序的入口
 func main() {
 // 创建一个 user 类型的值,并发送通知30 u := user{"Bill", "bill@email.com"}

 sendNotification(u)
 // ./listing36.go:32: 不能将 u(类型是 user)作为
 // sendNotification 的参数类型 notifier:
 // user 类型并没有实现 notifier
 // (notify 方法使用指针接收者声明)
 }

 // sendNotification 接受一个实现了 notifier 接口的值
 // 并发送通知
 func sendNotification(n notifier) {
     n.notify()
 }

如上面代码,当为struct实现接口的方法notify()方法时,定义的接受者receiver是一个指针类型,所以,它要遵循方法集的规则,如果方法集的receiver 是T 即指针类型,那么属于接口的值必须同样是 T 指针类型。

user 实现了notify 方法,也就是它实现了notifier 接口,当时如果将user 实例传给notifier实例,必须是一个指针类型,因为它实现的方法的receiver是一个指针类型。所以方法集的作用也就是规范接口的实现。

5.2 方法集规则

Values                    Methods Receivers
-----------------------------------------------
T                         (t T)
*T                         (t T) and (t *T)



Methods Receivers          Values
-----------------------------------------------
(t T)                     T and *T
(t *T)                     *T

如果方法的接受者是 指针类型 ,那么用指针接受者方式实现这个接口,只有指向那个类型的指针才能够算实现对应的接口,所以接口值接收的只能也是一个指针类型。

如果方法的接受者是 值类型,那么用值接收者实现接口,那个类型的值和指针都能够实现对应的接口。

简单讲就是,接受者是(t T),那么T 和 T 都可以实现接口,如果接受者是(t T)那么只有 *T才算实现接口。

反过来看稍微复杂点,判断这个类型变量是否实现了接口,看一下他是值类型还是指针类型,如果是T 值类型,那就看它实现接口方法的receiver是什么类型,如果也是值类型,那么它就实现了接口,如果不是,就没有实现,就不能进行传递。如果他是指针类型,那么不管它的receiver是值还是指针都实现了接口。所以记住上面的图就好。


有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:杨旭

查看原文:14 Go语言——接口interface详解

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

878 次点击  ∙  2 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传