Go中interface详解

上个章节我们学到了GO中Method的巧妙设计,本章节,我们再来深入学习下 interface 的详解。

Go 语言里面设计最精妙的应该算 interface,它让面向对象,内容组织实现非常的方便,当你看完这一章,你就会被 interface 的巧妙设计所折服。

什么是 interface

简单的说,interface 是一组 method 签名的组合,我们通过 interface 来定义对象的一组行为,只要实现了接口的所有方法,就表示该类型实现了该接口。

Go中接口的实现就可以看作duck typingDuckTyping 它描述的事物的外部行为,而非内部结构

面向对象的继承、抽象接口等等目的都是代码的复用。我们该如何考虑设计他呢?

既然是复用,那就要从使用者的角度去想,我认为是什么样子它就是什么样子。【面向接口】。 我只关心这段代码结构能做哪些事情,我复用它,我才不管它符不符合常识。【并不关心内部实现方法】。

GO 中interface特点

  • 接口是一个或多个方法签名的集合
  • 只要某个类型拥有该接口的所有方法签名,即实现该接口,无需显示声明实现了哪个接口,这称为 Structural Typing (跟其他面向对象语言中使用implement 显示表达实现有所不同)
  • 接口只有方法声明,没有实现,没有数据字段。
  • 接口可以匿名嵌入其它接口,或嵌入到结构中
  • 只有当接口存储的类型和对象都为nil时,接口才等于nil
  • 接口调用不会做receiver的自动转换
  • 接口同样支持匿名字段方法
  • 接口也可实现类似OOP中的多态
  • 空接口可以作为任何类型数据的容器 相当于C语言中的 void*

interface 类型

interface 类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口。

Go 复制代码
 type Stringer interface { // 定义接口方法
    String() string
}

其实和定义结构体的形式是一样的。主要的区别是: 内部没有像结构体一样实现里面具体的方法,仅仅是定义。

任意的类型都实现了空 interface (我们这样定义:interface {}),也就是包含 0methodinterface

interface 值

那么 interface 里面到底能存什么值呢?

定义了一个 interface 的变量,那么这个变量里面可以存实现这个 interface 的任意类型的对象。(这个变量包含了实现interface的所有方法),不需要显示的去实现接口。一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。 这种隐式实现接口的方式,极大提高了灵活性,也降低了开发者的心智。

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

通过上面的代码,你会发现 interface 就是一组抽象方法的集合,它必须由其他非 interface 类型实现,而不能自我实现。

Go 通过 interface 实现了 duck-typing: 即 "当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子"。

只要一个类型只要实现了接口定义的所有方法(是指有相同名称、参数列表 (不包括参数名) 以及返回值 ),那么这个类型就实现了这个接口,可以说这个类型现在是这个接口类型。可以直接进行赋值(其实也是隐式转换)

一个类型就可以实现多个接口,只要它拥有了这些接口类型的所有方法,那么这个类型就是实现了多个接口。

同时这个类型也就是多种形式的存在,反过来说一个接口可以被不同类型实现,这就是Go中多态。

空 interface

interface (interface {}) 不包含任何的 method

正因为如此,所有的类型都实现了空 interface

interface 对于描述起不到任何的作用 (因为它不包含任何的 method),但是空 interface 在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。它有点类似于 C 语言的 void* 类型。

Go 复制代码
func main() {
    // 定义 a 为空接口
    var a interface{}
    var i int = 5
    s := "Hello world"
    // a 可以存储任意类型的数值
    a = i
    a = s
    fmt.Println(a)
}

一个函数把 interface {} 作为参数,那么他可以接受任意类型的值作为参数,如果一个函数返回 interface {}, 那么也就可以返回任意类型的值。是不是很有用!

例如:下面的函数

Go 复制代码
func doSomething(v interface{}){    
}

如果函数的参数 v 可以接受任何类型,那么函数被调用时在函数内部 v 是不是表示的是任何类型?

并不是,虽然函数的参数可以接受任何类型,并不表示 v 就是任何类型,在函数 doSomething 内部 v 仅仅是一个 interface 类型,之所以函数可以接受任何类型是在 go 执行时传递到函数的任何类型都被自动转换成 interface{}

因为这个特性interface{},大量的用于函数参数,以及函数返回。

interface 函数参数

interface 的变量可以持有任意实现该 interface 类型的对象,这给我们编写函数 (包括 method) 提供了一些额外的思考,我们是不是可以通过定义 interface 参数,让函数接受各种类型的参数。

举个例子:fmt.Println 是我们常用的一个函数,但是你是否注意到它可以接受任意类型的数据。打开 fmt 的源码文件,你会看到这样一个定义:

Go 复制代码
type Stringer interface {
     String() string
}

也就是说,任何实现了 String 方法的类型都能作为参数被 fmt.Println 调用,让我们来试一试。

例如:

Go 复制代码
type Human struct {
    name  string
    age   int
    phone string
}

// 通过这个方法 Human 实现了 fmt.Stringer
func (h Human) String() string {
    return "❰" + h.name + " - " + strconv.Itoa(h.age) + " years -  ✆ " + h.phone + "❱"
}

func main() {
    Bob := Human{"Bob", 39, "000-7777-XXX"}
    fmt.Println("This Human is : ", Bob)
}

注:实现了 error 接口的对象(即实现了 Error () string 的对象),使用 fmt 输出时,会调用 Error () 方法,因此不必再定义 String () 方法了。

interface 变量存储的类型

我们知道 interface 的变量里面可以存储任意类型的数值 (该类型实现了 interface)。

那么我们怎么反向知道这个变量里面实际保存了的是哪个类型的对象呢?

目前常用的有两种方法:

  1. Comma-ok 断言

Go 语言里面有一个语法,可以直接判断是否是该类型的变量: value, ok = element.(T)

这里 value 就是变量的值,ok 是一个 bool 类型,elementinterface 变量,T 是断言的类型。

如果 element 里面确实存储了 T 类型的数值,那么 ok 返回 true,否则返回 false

让我们通过一个例子来更加深入的理解

Go 复制代码
type Element interface{}
type List []Element

type Person struct {
	name string
	age  int
}

// 定义了 String 方法,实现了 fmt.Stringer
func (p Person) String() string {
	return "(name: " + p.name + " - age: " + strconv.Itoa(p.age) + " years)"
}

func main() {
    list := make(List, 3)
    list[0] = 1       // an int
    list[1] = "Hello" // a string
    list[2] = Person{"Dennis", 70}

    for index, element := range list {
        if value, ok := element.(int); ok {
            fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
        } else if value, ok := element.(string); ok {
            fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
        } else if value, ok := element.(Person); ok {
            fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
        } else {
            fmt.Printf("list[%d] is of a different type\n", index)
        }
    }
}

注意:x 必须为inteface类型,不然会报错。

  1. switch 测试

最好的讲解就是代码例子,现在让我们重写上面的这个实现

Go 复制代码
for index, element := range list {
    switch value := element.(type) {
    case int:
        fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
    case string:
        fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
    case Person:
        fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
    default:
        fmt.Printf("list[%d] is of a different type", index)
    }
}

这里有一点需要强调的是:

  • element.(type) 语法不能在 switch 外的任何逻辑里面使用,如果你要在 switch 外面判断一个类型就使用 comma-ok

  • 使用不支持fallthrough.

接口转换

可以将拥有超集的接口转换为子集的接口,反之出错。

Go 复制代码
 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 { // Comma-ok 语法,判断是否包含这个interface
      fmt.Println(i)
   }
   u := o.(*User)
   // u := o.(User) // panic: interface is *main.User, not main.User
   fmt.Println(u)
}

// 1, Tom
// 1, Tom

Stringer接口定义在fmt包中,该接口包含String()方法。任何类型只要定义了String()方法,进行Print输出时,就可以得到定制输出。

这样在打印输出的时,就可以使用fmt.Printf("%s", i)形式对其进行输出。

上面代码的意思是:如果包含了string的输出形式就行输出。

嵌入(匿名) interface

Go 里面真正吸引人的是它内置的逻辑语法,就像我们在学习 Struct 时学习的匿名字段,多么的优雅啊,那么相同的逻辑引入到 interface 里面,那不是更加完美了。

如果一个 interface1 作为 interface2 的一个嵌入字段,那么 interface2 隐式的包含了 interface1 里面的 method

我们可以看到源码包 container/heap 里面有这样的一个定义

Go 复制代码
type Interface interface {
    sort.Interface // 嵌入字段 sort.Interface
    Push(x interface{}) // a Push method to push elements into the heap
    Pop() interface{} // a Pop elements that pops elements from the heap
}

我们看到 sort.Interface 其实就是嵌入字段,把 sort.Interface 的所有 method 给隐式的包含进来了。

也就是下面三个方法:

Go 复制代码
type Interface interface {
    // Len is the number of elements in the collection.
    Len() int
    // Less returns whether the element with index i should sort
    // before the element with index j.
    Less(i, j int) bool
    // Swap swaps the elements with indexes i and j.
    Swap(i, j int)
}

反射

Go 语言实现了反射,所谓反射就是能检查程序在运行时的状态。

我们一般用到的包是 reflect 包。如何运用 reflect 包,官方的这篇文章详细的讲解了 reflect 包的实现原理, laws of reflection

使用 reflect 一般分成三步,下面简要的讲解一下:

要去反射是一个类型的值 (这些值都实现了空 interface), 首先需要把它转化成 reflect 对象 (reflect.Type 或者 reflect.Value,根据不同的情况调用不同的函数)。

这两种获取方式如下:

go 复制代码
t := reflect.TypeOf(i)    // 得到类型的元数据,通过t我们能获取类型定义里面的所有元素
v := reflect.ValueOf(i)   // 得到实际的值,通过 v 我们获取存储在里面的值,还可以去改变值

转化为 reflect 对象之后我们就可以进行一些操作了,也就是将 reflect 对象转化成相应的值。

获取反射值能返回相应的类型和数值。

更多的反射用法,我们后边进行介绍,这里主要是对interface进行介绍。

接口的内部实现

接口值

接口值可以使用 ==!=来进行比较。

两个接口值相等仅当

  • 它们都是nil值
  • 它们的动态类型相同,并且动态值也根据这个动态类型的==操作相等。

因为接口值是可比较的,所以它们可以用在map的键或者作为switch语句的操作数。

然而,如果两个接口值的动态类型相同,但是这个动态类型是不可比较的(比如切片),将它们进行比较就会失败并且panic。

满足 同时是nil 或者是 接口动态类型相同并且是可比较的

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

接口内部结构

Go 复制代码
// 没有方法的interface

type eface struct {
    _type *_type   //类型信息
    data  unsafe.Pointer  //数据指针
}

// 记录着Go语言中某个数据类型的基本特征,_type是go所有类型的公共描述
// 可以简单的认为,接口可以通过一个  _type *_type 直接或间接表述go所有的类型就可以了
type _type struct {
    size       uintptr    //类型的大小
    ptrdata    uintptr    //存储所有指针的内存前缀的大小
    hash       uint32     //类型的hash
    tflag      tflag      //类型的tags
    align      uint8      //结构体内对齐
    fieldalign uint8      //结构体作为field时的对齐
    kind       uint8      //类型编号 定义于 runtime/typekind.go
    alg        *typeAlg   // 类型元方法 存储hash 和equal两个操作。
    gcdata    *byte       //GC 相关信息
    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
}

存在两种interface,一种是带有方法的iface,一种是不带方法的eface

eface: 对于不带方法的接口类型,Go语言中的所有变量都可以赋值给interface{}变量,interface可以表述Go所有的类型,_type存储类型信息,data存储类型的值的指针,指向实际值或者实际值的拷贝。

iface: 对于带方法的接口类型,tab *itab 存储指向了iTable的指针,ITable存储了类型相关的信息以及相关方法集,而data 同样存储了实例值的指针,指向实际值或者是实际值的一个拷贝。

实现了interface中定义方法的变量可以赋值给带方法的interface变量,并且可以通过interface直接调用对应的方法,实现了其它面向对象语言的多态的概念。

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

interface 与 方法

方法的定义

方法的语法定义(前面的章节已经讲过): func (r ReceiverType) funcName(parameters) (results)

go 复制代码
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)
}

func main() {
    // 创建一个 user 类型的值,并发送通知30
    u := user{"Rock", "rock@email.com"}
    sendNotification(u) // user does not implement notifier (notify method has pointer receiver)
    // 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是一个指针类型。

所以方法的作用 也就是 规范接口的实现。

方法规则

Go 复制代码
Values                    Methods Receivers
-----------------------------------------------
T                         (t T)
*T                        (t T) and (t *T)



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

上面关系描述的是 当前 变量类型 与 此变量中Methods的Recivers方法。

u := user{"Rock", "rock@email.com"} 变量类型uT, 那么他能实现的 接口方法类型 只能是 (t T) 类型的。 这也是最开始的例子为什么报错的原因。

u := &user{"Rock", "rock@email.com"} 变量类型u*T, 那么他能实现的 接口方法类型 是 (t *T)(t T) 两种类型的。

下面的俩个其实是对上面的反转描述。

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

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

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

本质原因:编译器并不是总能自动获得一个值的地址。主要是按照值传递方式实现。

嵌入类型时接口实现

重温一下什么是嵌入类型,go语言为了实现类似继承的代码复用,通过组合的方式来提高面向对象的能力。

通过嵌入类型来实现代码复用和扩展类型字段和方法。

嵌入类型 :是将已有的类型直接声明在新的结构类型里。被嵌入的类型被称为新的外部类型的内部类型

实现方法重写:外部类型也可以通过声明与内部类型标识符同名的标识符来覆盖内部标识符的字段或者方法。

  • 注意声明字段和嵌入类型在语法上的不同 ,嵌入类型直接是写个类型名就行
  • 内部类型的标识符提升到了外部类型,可以直接通过外部类型的值来访问内部类型的标识符。 也可以通过内部类型的名间接访问内部类型方法和标识符。
  • 内部类型实现接口外部类型默认也实现了该接口。注意方法集的规则。
  • 如果内部类型和外部类型同时实现一个接口,就近原则,外部类型不会直接调用内部类型实现的同名方法,而是自己的。当然可以通过内部类型间接显示的去调用内部类型的方法。

小结: 嵌入类型,就是外部类型拥有内部类型所有的字段和方法,就好比直接定义在外部类型一样,就像继承。

嵌入类型实现接口,同样应用到外部类型

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

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

// 通过 user 类型值的指针
// 调用的方法
func (u *user) notify() {
   fmt.Printf("Sending user email to %s<%s>\n",
      u.name,
      u.email)
}

// admin 代表一个拥有权限的管理员用户
type admin struct {
   user  // 嵌入类型
   level string
}

// main 是应用程序的入口
func main() {
   // 创建一个 admin 用户
   ad := admin{
      user: user{
         name:  "rock",
         email: "rock@yahoo.com",
      },
      level: "super",
   }
   // 给 admin 用户发送一个通知
   // 用于实现接口的内部类型的方法,被提升到
   // 外部类型
   sendNotification(&ad)
}

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

由于内部类型的提升,内部类型实现的接口会自动提升到外部类型,因此外部类型同样也算实现了该接口。 总结:内部类型实现接口外部类型默认也实现了该接口。

内部类型和外部类型同时实现接口

外部类型和内部类型同时实现接口,就近原则,外部类型优先调用自己实现的方法。 如果要调用内部类型的方法,需要用内部类型字段间接调用。 类似于方法重写的方式,实现和内部类型同名的方法,也是就近原则。

如果receiver都是用value实现的,不管参数是value还是pointer都能ok,原因就是如果传入的是pointer,编译器会自动转换为value

小结:毕竟通过pointer是能找到对应的value,然后在执行copy即可,但是通过copy value是无法找到原始指针的。

相关链接:

相关推荐
架构师那点事儿5 小时前
golang 用unsafe 无所畏惧,但使用不得到会panic
架构·go·掘金技术征文
于顾而言21 小时前
【笔记】Go Coding In Go Way
后端·go
qq_1728055921 小时前
GIN 反向代理功能
后端·golang·go
follycat1 天前
2024强网杯Proxy
网络·学习·网络安全·go
OT.Ter1 天前
【力扣打卡系列】单调栈
算法·leetcode·职场和发展·go·单调栈
探索云原生1 天前
GPU 环境搭建指南:如何在裸机、Docker、K8s 等环境中使用 GPU
ai·云原生·kubernetes·go·gpu
OT.Ter1 天前
【力扣打卡系列】移动零(双指针)
算法·leetcode·职场和发展·go
码财小子2 天前
k8s 集群中 Golang pprof 工具的使用
后端·kubernetes·go
明月看潮生5 天前
青少年编程与数学 02-003 Go语言网络编程 04课题、TCP/IP协议
青少年编程·go·网络编程·编程与数学
明月看潮生6 天前
青少年编程与数学 02-003 Go语言网络编程 03课题、网络编程协议
青少年编程·go·网络编程·编程与数学