在Go语言的实际编程中,几乎所有的数据结构都围绕接口展开,接口是Go语言中所有数据结构的核心。

基本概念

goroutine和channel是Go并发的两大基石,那么接口是Go语言编程中数据类型的关键。在Go语言的实际编程中,几乎所有的数据结构都围绕接口展开,所以接口是Go语言中所有数据结构的核心。

Go不是一种典型的OO语言,它在语法上不支持类和继承的概念。但是我们可以使用组合实现了类似于继承,struct类似于class,interface也可以实现多态的功能。

类与继承

虽然Go语言没有类的概念,但它支持的数据类型可以定义对应的method(s)。本质上说,所谓的method(s)其实就是函数,只不过与普通函数相比,这类函数是作用在某个数据类型上的,所以在函数签名中,会有个receiver(接收器)来表明当前定义的函数会作用在该receiver上。

Go语言支持的除Interface类型外的任何其它数据类型都可以定义其method(而并非只有struct才支持method),只不过实际项目中,method(s)多定义在struct上而已。从这一点来看,我们可以把Go中的struct看作是不支持继承行为的轻量级的“类”。

使用struct的组合功能,就可以其struct的函数,类似于面向对象编程的继承功能。当然interface组合嵌入能够优雅的编码,比如io.readwriter

// io.ReadWriter
type ReadWriter interface {
    Reader
    Writer
}

多态

没有继承是否就无法拥有多态行为了呢?答案是否定的,Go语言引入了一种新类型—Interface,它在效果上实现了类似于C++的“多态”概念,虽然与C++的多态在语法上并非完全对等,但至少在最终实现的效果上,它有多态的影子。

interface是一组method的组合,我们通过interface来定义对象的一组行为。实现interface中的所有方法的类就是实现了这个接口,可以调用这个接口以及这个接口中的方法。interface中的方法不能重载。

空的interface可以存储任意类型的值。

从语法上看,Interface定义了一个或一组method(s),这些method只有函数签名,没有具体的实现代码(有没有联想起C++中的虚函数?)。若某个数据类型实现了Interface中定义的那些被称为”methods”的函数,则称这些数据类型实现(implement)了interface。

Interface类型的更通用定义可归纳如下:

type Namer interface {
    Method1(param_list) return_type
    Method2(param_list) return_type
    ...
}

以fmt包的Printf()函数为例,其函数签名格式如下:

func Printf(format string, a ...interface{}) (n int, err error)

该函数在实现底层的打印行为时,要求传入的可变长参数实现了fmt包中定义的Stringer接口,这个接口类型定义及描述如下:

type Stringer interface {
    String() string
}

下面是一段简单的打印代码:

package main

import "fmt"

type IPAddr [4]byte

func main() {
    addrs := map[string]IPAddr{
        "loopback":  {127, 0, 0, 1},
        "googleDNS": {8, 8, 8, 8},
    }
    for n, a := range addrs {
        fmt.Printf("%v: %v\n", n, a)
    }
}

loopback: [127 0 0 1]
googleDNS: [8 8 8 8]

现在要求按规定的格式打印:IPAddr{1, 2, 3, 4}应该输出为”1.2.3.4”的格式,所以IPAddr这个自定义类型需要实现Stringer接口,实现代码如下:

package main

import "fmt"

type IPAddr [4]byte

// TODO: Add a "String() string" method to IPAddr.
func (ip IPAddr) String() string {    
    return fmt.Sprintf("%v.%v.%v.%v", ip[0], ip[1], ip[2], ip[3])
}

func main() {
    addrs := map[string]IPAddr{
        "loopback":  {127, 0, 0, 1},
        "googleDNS": {8, 8, 8, 8},
    }
    for n, a := range addrs {
        fmt.Printf("%v: %v\n", n, a)
    }
}

googleDNS: 8.8.8.8
loopback: 127.0.0.1

为什么使用interface

泛型编程最初诞生于C++中,由Alexander Stepanov和David Musser创立。目的是为了实现C++的STL(标准模板库)。其语言支持机制就是模板(Templates)。模板的精神其实很简单:参数化类型。换句话说,把一个原本特定于某个类型的算法或类当中的类型信息抽掉,抽出来做成模板参数T 泛型就是没有类型,通用于所有的类型,是一种抽象

go中没有泛型,但是可以通过接口来实现泛型

interface{} 同时被叫做空接口类型,意义在于其语义本身能绕过 Go 语言的静态类型检查。但在不必要的情况下使用它会使你得不偿失。

譬如,它可能强制让你使用反射,而这是一个运行时特性(而非安全且快速度的编译时)。你可能需要自行检查类型错误,而不是让编译器来为你寻找他们。

使用空接口前务必三思。基于清晰的类型或接口之上来实现你所需的函数行为会更好。

基本使用

实例对象

interface可以被任意的对象实现。同理,一个对象可以实现任意多个interface。

若某个数据类型实现了Interface中定义的那些被称为”methods”的函数,则称这些数据类型实现(implement)了interface。

空interface

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

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

判断interface的类型

Comma-ok断言:

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

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

底层实现

Go的interface源码在Golang源码的runtime目录中。Go在不同版本之间的interface结构可能会有所不同,但是,整体的结构是不会改变的。

Go的interface是由两种类型来实现的:iface和eface。

其中,iface表示的是包含方法的interface,例如:

type Person interface {
    Print()
}

而eface代表的是不包含方法的interface,即

type Person interface {}
或者

var person interface{} = xxxx实体

eface

eface的具体结构是:

  • 一共有两个属性构成,一个是类型信息_type,一个是数据信息。
  • 其中,_type可以认为是Go语言中所有类型的公共描述,Go语言中几乎所有的数据结构都可以抽象成_type,是所有类型的表现,可以说是万能类型,
  • data是指向具体数据的指针。

type的具体代码为:

type _type struct {
    size       uintptr
    ptrdata    uintptr // size of memory prefix holding all pointers
    hash       uint32
    tflag      tflag
    align      uint8
    fieldalign uint8
    kind       uint8
    alg        *typeAlg
    // gcdata stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, gcdata is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    gcdata    *byte
    str       nameOff
    ptrToThis typeOff
}

对于没有方法的interface赋值后的内部结构是怎样的呢?

可以先看段代码:

import (
    "fmt"
    "strconv"
)

type Binary uint64

func main() {
    b := Binary(200)
    any := (interface{})(b)
    fmt.Println(any)
}

输出200,赋值后的结构图是这样的:

对于将不同类型转化成type万能结构的方法,是运行时的convT2E方法,在runtime包中。

iface

所有包含方法的接口,都会使用iface结构。包含方法的接口就是一下这种最常见,最普通的接口:

type Person interface {
    Print()
}

iface的源代码是:

type iface struct {
    tab  *itab
    data unsafe.Pointer
}

itab是iface不同于eface比较关键的数据结构。其可包含两部分:一部分是确定唯一的包含方法的interface的具体结构类型,一部分是指向具体方法集的指针。

具体结构为:

属性 itab的源代码是:

type itab struct {
    inter *interfacetype //此属性用于定位到具体interface
    _type *_type //此属性用于定位到具体interface
    hash  uint32 // copy of _type.hash. Used for type switches.
    _     [4]byte
    fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

属性interfacetype类似于_type,其作用就是interface的公共描述,类似的还有maptype、arraytype、chantype…其都是各个结构的公共描述,可以理解为一种外在的表现信息。interfacetype源码如下:

type interfacetype struct {
    typ     _type
    pkgpath name
    mhdr    []imethod
}
type imethod struct {
    name nameOff
    ityp typeOff
}

iface的整体结构为:

对于含有方法的interface赋值后的内部结构是怎样的呢?

package main

import (
    "fmt"
    "strconv"
)

type Binary uint64
func (i Binary) String() string {
    return strconv.FormatUint(i.Get(), 10)
}

func (i Binary) Get() uint64 {
    return uint64(i)
}

func main() {
    b := Binary(200)
    any := fmt.Stringer(b)
    fmt.Println(any)
}

首先,要知道代码运行结果为:200。

其次,了解到fmt.Stringer是一个包含String方法的接口。

type Stringer interface {
    String() string
}

最后,赋值后接口Stringer的内部结构为:

对于将不同类型转化成itable中type(Binary)的方法,是运行时的convT2I方法,在runtime包中。