preface

golang的interface是使用很广泛的一种手段, 相比java python 等语言, golang的interface不需要明显的继承和实现, 只需要实现相应的方法就可以了. 常常我们需要抽象struct的interface, 方便mock进行测试.

detail

golang的interface主要有两种: eface 和 iface. eface是不带方法的interface,

定义参看 runtime2.go 和 type.go,

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

type eface struct {
    _type *_type
    data  unsafe.Pointer
}

type itab struct {
    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.
}
type interfacetype struct {
    typ     _type
    pkgpath name
    mhdr    []imethod
}
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
}
type imethod struct {
    name nameOff
    ityp typeOff
}

在设计上, iface 比 eface多了一些设计:

  1. interfaceType: 存放interface的公共描述, 类似 maptype、arraytype、chantype.
  2. iface使用了itab存储了类型相关信息.
  3. iface和eface的data指向具体的数据
  4. _type是所有类型信息结构的公共部分

类型转换

在 iface.go 中提供了 convT2E convT2I convI2I 等方法, E I T 分别表示 eface、iface、具体类型, 除了具体类型和eface iface之间的类型转换

通常我们会用interface定义接口类型, 然后通过不同的实现来开发. 比如存储层, 提供 mysql/redis/mem 三种形式, 亦或者 代理方式.

这里我们使用模拟rpc调用客户端进行演示, 如下: 第一种显示转换:

type HelloClient interface{
    SayHello()
}

type MockHelloClient struct{}

func (MockHelloClient) SayHello() {
    println("mock hello")
}

func TestHelloClient(c HelloClient){
   c.SayHello()
} 

func main(){
    mc := HelloClient(MockHelloClient())
    TestHelloClient(mc)
}

其实, 更常见的是错误码/枚举的定制, 比如

type ErrCode int32 


第二种被动转换:

type HelloClient interface{
    SayHello()
}

type MockHelloClient struct{}

func (MockHelloClient) SayHello() {
    println("mock hello")
}

func TestHelloClient(c HelloClient){
   c.SayHello()
} 

func main(){
    var mc MockHelloClient
    TestHelloClient(mc)
}

看下汇编的实现

go build -gcflags '-l' -o main main.go 
go tool objdump -s "main\.TestHelloClient" main

得到如下图的内容

TEXT main.TestHelloClient(SB) /Users/snow_young/go/src/inter/main.go
  main.go:13        0x104e1a0       65488b0c2530000000  MOVQ GS:0x30, CX
  main.go:13        0x104e1a9       483b6110        CMPQ 0x10(CX), SP
  main.go:13        0x104e1ad       762c            JBE 0x104e1db
  main.go:13        0x104e1af       4883ec10        SUBQ $0x10, SP
  main.go:13        0x104e1b3       48896c2408      MOVQ BP, 0x8(SP)
  main.go:13        0x104e1b8       488d6c2408      LEAQ 0x8(SP), BP
  main.go:14        0x104e1bd       488b442418      MOVQ 0x18(SP), AX
  main.go:14        0x104e1c2       488b4018        MOVQ 0x18(AX), AX
  main.go:14        0x104e1c6       488b4c2420      MOVQ 0x20(SP), CX
  main.go:14        0x104e1cb       48890c24        MOVQ CX, 0(SP)
  main.go:14        0x104e1cf       ffd0            CALL AX
  main.go:15        0x104e1d1       488b6c2408      MOVQ 0x8(SP), BP
  main.go:15        0x104e1d6       4883c410        ADDQ $0x10, SP
  main.go:15        0x104e1da       c3          RET
  main.go:13        0x104e1db       e80089ffff      CALL runtime.morestack_noctxt(SB)
  main.go:13        0x104e1e0       ebbe            JMP main.TestHelloClient(SB)

按照 这篇文章的说法, 0x18(SP) 是 itab的地址, 这里进行了函数地址的存储.

因为是link的缘故, 我们并不能直观的看到函数的调用.

类型断言

iface.go中提供了 assertI2I assertI2I2 assertE2I assertE2I2 等断言方法. 什么时候使用断言呢? 在rpc调用中, 我们可能需要传递一些上下文信息, 通常我们会通过ctx传递, 如下:

import "context"

type baseKey struct{}

type reqBase struct {
    LogId   int64
    from_ip string
    ip      string
}

func main() {
    c := context.Background()

    c = context.WithValue(c, baseKey{}, &reqBase{LogId: 11, from_ip: "127.0.0.1", ip: "127.0.0.1"})
    baseInfo := c.Value(baseKey{}).(*reqBase)
    println("base info: ", baseInfo.LogId, baseInfo.from_ip)
}

如果ctx中明确传递了上下文信息的, 可以直接断言. 不确定的情况下, 建议使用:

func main() {
    c := context.Background()

    c = context.WithValue(c, baseKey{}, &reqBase{LogId: 11, from_ip: "127.0.0.1", ip: "127.0.0.1"})
    baseInfo, ok := c.Value(baseKey{}).(*reqBase)
    if !ok{
        println("convert failed.")
        return 
    }
    println("base info: ", baseInfo.LogId, baseInfo.from_ip)
}

如果使用objdump, 如果不是显式的转换, 很难找到相关的函数调用.

动态分发

反射

  1. [](https://research.swtch.com/interfaces)
  2. 深度讲解的翻译
  3. Legendtkl blog: Go Interfacey源码解析
  4. legendtkl blog: 深入理解 Go Interface
  5. youtube: understanding the interface