概述

在Go中的,interface是一个非常重要的概念,一般情况下会有两种用法,一种类似于Java语言的接口的概念,作为Go语言中的一组方法的定义;一种类似于c 中的void *的概念,是Go中的抽象类型。

方法

比如经常使用的Context类型,只定义了其方法的集合,具体的类型可以有不同的实现方式,传输过程中可以不传递其实现类型,而是直接传递context即可。这样就隐藏了其具体实现。

1
2
3
4
5
6
type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}

不同于其他语言的接口,go语言接口的实现是 隐式的 。我们不需要像Java那样显式的implement interface。在golang中 我们只需要给类型定义好接口中的几个方法,那么该类型就自动实现了该接口。

Go语言会在编译器对代码进行类型检查,如下面代码所示,共触发了三次类型检查:

  1. *RPCError 类型的变量赋值给 error 类型的变量 rpcErr
  2. *RPCError 类型的变量 rpcErr 传递给签名中参数类型为 errorAsErr 函数;
  3. *RPCError 类型的变量从函数签名的返回值类型为 errorNewRPCError 函数中返回;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func main() {
    var rpcErr error = NewRPCError(400, "unknown err") // typecheck1
    err := AsErr(rpcErr) // typecheck2
    println(err) 
}

func NewRPCError(code int64, msg string) error {
    return &RPCError{ // typecheck3
        Code:    code,
        Message: msg,
    }
}

func AsErr(err error) error {
    return err
}

从编译器类型检查的过程来看,编译器仅在需要时才会对类型进行检查,类型实现接口时也只需要隐式的实现所有接口中的方法即可。

类型

不带有任何方法的interface{}类型,使用起来像c中的空指针。不过不同的地方是,interface{}类型不代表任意类型,interface{}类型的变量在运行期间的类型只是interface{}

1
2
3
4
5
func Print(v interface{}){
  println(v)
}

Print(Struct{a int}{a:1})

上述Print函数并不能接收任意类型的阐述,只是在调用Print函数时,将参数转换成了interface{}类型。

如果需要将interface{}类型重新转化为其实际类型,需要进行类型断言。

在Go语言中的源码中,将接口实现表示成了 iface结构体,将 不包含任何方法的抽象类型表示为eface结构体。虽然两种类型都使用了interface进行声明,但是后者由于在Go语言中非常常见,所以在实现时也将它实现成了一种特殊的类型。

指针与接口

Go语言是一个有指针类型的语言,接口在定义一组方法时,并没有对实现的接收者做限制,所以可以在一个类型上看到两种不同的实现方式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
//方式1
type Plane interface{
  Fly()
}

type Helicopter struct{}

func (h *Helicopter)Fly{
  fmt.Println("helicopter flying")
}

//方式2
type Plane interface{
  Fly()
}

type Helicopter struct{}

func (h Helicopter)Fly{
  fmt.Println("helicopter flying")
}

理论上,调用者和接收者各有两种类型,一共四种组合。而实际上,方法集规范只包括三种方式,如下表:

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

有一种情况会编译不通过。就是方法接收者是指针类型,而实际调用者是结构体。

因为在实际调用者是结构体时,并不是总能获取到结构体的指针的。而反过来,指针总是能获取到其指向的实际结构体,那么指针自然能调用接收器为结构体的方法了。

因为不能总是获取到一个值的地址,所以指针结构体的方法不能由结构体来调用。

1
2
3
4
5
6
7
8
9
package main

import "fmt"

type myStruct int

func main() {
	fmt.Printf("%p\n", &myStruct(1))        //can't take the address of myStruct(1)
}

nil && non-nil

我们可以再通过一个例子理解**『Go 语言的接口类型不是任意类型』**这一句话,下面的代码在 main函数中初始化了一个 *TestStruct 结构体指针,由于指针的零值是 nil,所以变量 s 在初始化之后也是 nil

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import "fmt"

type TestStruct struct{}

func NilOrNot(v interface{}) {
	if v == nil {
		println("nil")
	} else {
		println("non-nil")
	}
}

func main() {
	var s *TestStruct
	if s == nil {
		fmt.Println("nil")
	} else {
		fmt.Println("non nil")
	}
	NilOrNot(s)
}


$ go run main.go
nil
non-nil

但是当我们将 s 变量传入 NilOrNot 时,该方法却打印出了 non-nil 字符串,这主要是因为调用 NilOrNot 函数时其实会发生隐式的类型转换,变量 nil 会被转换成 interface{} 类型,interface{} 类型是一个结构体,它除了包含 nil 变量之外还包含变量的类型信息,也就是 TestStruct,所以在这里会打印出 nil non-nil,在转换为接口之前,变量s确实为nil,但是转化为interface之后就不再是nil了。

类型断言

类型断言可以看成一系列类型转换,对每一个swtich case而言,试图转换value的类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type Stringer interface {
    String() string
}

var value interface{} // Value provided by caller.
switch str := value.(type) {
case string:
    return str
case Stringer:
    return str.String()
}

使用类型转换,等价于如上流程的代码如下:

1
2
3
4
5
if str, ok := value.(string); ok {
    return str
} else if str, ok := value.(Stringer); ok {
    return str.String()
}

进行类型转换时,str,ok := value.(string)这种方式,可以通过ok来判断value是否转换成功,如果转换不成功,那么str也会变成转换之后类型的零值。当然,也有一种写法 str := value.(string)这种方式直接进行类型转换,而一旦转换不成功,那么代码就会直接panic。

why interface

泛型

使用 interfae可以实现范型编程,比如我们现在要写一个泛型算法,形参定义采用 interface 就可以了,以标准库的 sort 为例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
type Interface interface {
    Len() int
    Less(i, j int) bool
    Swap(i, j int)
}

func Sort(data Interface) {
    n := data.Len()
    maxDepth := 0
    for i := n; i > 0; i >>= 1 {
        maxDepth++
    }
    maxDepth *= 2
    quickSort(data, 0, n, maxDepth)
}

Sort 函数的形参是一个 interface,包含了三个方法:Len()Less(i,j int)Swap(i, j int)。使用的时候不管数组的元素类型是什么类型(int, float, string…),只要我们实现了这三个方法就可以使用 Sort 函数,这样就实现了“泛型编程”。有一点比较麻烦的是,我们需要将数组自定义一下。

隐藏具体实现

隐藏具体实现,这个很好理解。比如我设计一个函数给你返回一个 interface,那么你只能通过 interface 里面的方法来做一些操作,但是内部的具体实现是完全不知道的。如 下图演示的context 接口:尽管内部实现上下面三个函数返回的具体 struct (都实现了 Context interface)不同,但是对于使用者来说是完全无感知的。

1
2
3
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)    //返回 cancelCtx
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) //返回 timerCtx
func WithValue(parent Context, key, val interface{}) Context    //返回 valueCtx

providing interception points

这里应该是 wrapper 或者装饰器,文章末尾视频中Francesc给出了一个例子如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type header struct {
    rt  http.RoundTripper
    v   map[string]string
}

func (h header) RoundTrip(r *http.Request) *http.Response {
    for k, v := range h.v {
        r.Header.Set(k,v)
    }
    return h.rt.RoundTrip(r)
}

通过 interface,我们可以通过类似这种方式实现 dynamic dispatch。

另外golang中一个重要的原则就是,返回具体的类型,而接收参数的时候接收 接口类型,这样可以提升程序的健壮性,而且可以使得方法很容易进行测试:

Return concrete types, receive interfaces as parameter. — Robustness Principle applied to Go

非侵入式

上面提过,golang的接口是隐式的实现的,在编译期一个结构体作为一个接口传递,会检测该结构体是否实现了接口的所有方法。一个结构体可以实现多个接口,只要其实现了这几个接口的方法。假设接口的方法个数为m,结构体实现的方法的个数为n,检查结构体是否实现接口的时间复杂度为 O( m*n )。在将方法进行排序之后,时间复杂度可以将为O( m + n )。

同时不同于Java中实现一个接口需要显示的implement 一个接口,golang中可以减少对包的依赖。但是这样也会带来一个新的问题:

  1. 性能下降,使用 interface作为函数参数,runtime的时候会动态的确定行为。而使用struct作为参数的话,编译期间就可以确定了。
  2. 不知道 struct 实现了哪些interface。

tip:使用规范:可以实现多个接口,编译时检测vs运行时检测

如果没有额外的导出函数,实现接口的方法可以用接口来传递(方便替换,open - close原则)

返回具体值,接收方接收interface,但是如果不想让接收方看到具体细节,也可以直接返回interface。

内部实现原理

上面提到: Go 语言中其实有两种略微不同的接口,其中一种是带有一组方法的接口,另一种是不带有任何方法的 interface{} 类型。这两种不同的接口,在go语言的源代码中也有着不同的定义:第一种接口表示为 iface, 而第二种表示成 eface结构体。

eface

1
2
3
4
type eface struct { // 16 bytes
    _type *_type
    data  unsafe.Pointer
}

由于interface{}类型不包含任何方法,所以其结构也相对简单,只包含指向底层数据和类型的两个指针,从这里的结构可以推断出,任意的类型都可以转换成interface{}类型。

iface

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type iface struct { // 16 bytes
    tab  *itab
    data unsafe.Pointer
}

type itab struct { // 32 bytes
    inter *interfacetype
    _type *_type
    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.
}

用于表示接口的 interface类型的底层数据结构就是 iface 了。在这个结构体中也有指向原始数据的指针 data,但是这个结构体中更为重要的其实是 itab类型的tab字段。

itab

在itab结构中,也包含了 eface 中包含的 _type 结构体,每个 _type 结构体中都包含了类型的大小,对齐以及hash等信息。

hash 字段其实是对 _type.hash 的拷贝,它会在从 interface 到具体类型的切换时用于快速判断目标类型和接口中类型是否一致;最后的 fun 数组其实是一个动态大小的数组,如果如果当前数组中内容为空就表示 _type 没有实现 inter 接口,虽然这是一个大小固定的数组,但是在使用时会直接通过指针获取其中的数据并不会检查数组的边界,所以该数组中保存的元素数量是不确定的。用户缓存是否实现某个接口

itable在头部存储了一些类型相关元数据,之后是一个函数指针的列表。注意itable不是和不动态类型对应的,而是和interface类型相对应,只有interface中的方法会存入itable中。

Reference

  1. Go Data Structures: Interfaces
  2. go 接口