Interface實現原理分析

Duck Typing

什麼是鴨子類型

在這裏插入圖片描述
圖中的大黃鴨是一隻鴨子嗎?如果從傳統角度來看,圖中的大黃鴨並非是一隻鴨子,因爲它即不會叫也不會跑,甚至連生命都沒有

首先看下鴨子類型的定義

If it walks like a duck and it quacks like a duck, then it must be a duck
如果某個東西像鴨子一樣走,像鴨子一樣嘎嘎叫,那它一定是鴨子

所以,從Duck Typing角度來看,圖中的大黃鴨是一隻鴨子

鴨子類型,是程序設計中的一種類型推斷風格,它描述事物的外部行爲而非內部結構

Go語言的鴨子類型

Go語言通過接口的方式實現Duck Typing。不像其他動態語言那樣,只能在運行時才能檢查到類型不匹配,也不像大多數靜態語言那樣,需要顯示聲明實現哪個接口,Go語言接口的獨特之處在於它是隱式實現。

概述

接口類型

接口是一種抽象類型,它沒有暴露所含數據的佈局或者內部結構,當然也沒有哪些數據的基本操作,所提供的僅僅是一些方法。當你拿到一個接口類型的變量,你無從知道它是什麼,但你能知道它能做什麼,或者更精確地講,僅僅是它提供了哪些方法。

接口定義

Go語言提供了 interface關鍵字,接口中只能定義需要實現的方法,不能包含任何的變量

type 接口類型名 interface{
    方法名1( 參數列表1 ) 返回值列表1
    方法名2( 參數列表2 ) 返回值列表2}

例如 io.Writer 其實就是接口類型

type Writer interface {
    Write(p []byte) (n int, err error)
}

接口與接口間可以嵌套得到新接口,如 io.ReadWriter


type Reader interface {
    Read(p []byte) (n int, err error)
}

type ReadWriter interface{
    Reader
    Writer
}

不包含方法的接口,叫做空接口類型

interface{}

實現接口

如果一個具體類型實現了一個接口要求的所有方法,那麼這個類型實現了這個接口。當具體類型實現了一個接口時,這個具體類型纔可以賦值給該接口

如下示例中,定義一個 Runner 接口,只包含一個 run() 方法, Person 結構體實現了 Run() 方法,那麼就實現了 Runner 接口

type Runner interface {
    Run()
}

type Person struct {
    Name string
}

func (p Person) Run() {
    fmt.Printf("%s is running\n", p.Name)
}

func main() {
    var r Runner
    r = Person{Name: "song_chh"}
    r.Run()
}

另外,因爲空接口類型是沒有定義任何方法的接口,因此所有類型都實現了空接口,也就是說可以把任何類型賦給空接口類型

接口和指針

接口在定義一組方法時,沒有對實現的接收者做限制,所以有兩種實現方式的接收者,一種是指針接收者,另一種是值接收者

同一個方法不能兩種實現同時存在

爲Runner接口增加一個 Say() 方法,Person結構體類型使用指針接收者實現 Say() 方法

type Runner interface {
    Run()
    Say()
}

type Person struct {
    Name string
}

func (p Person) Run() {
    fmt.Printf("%s is running\n", p.Name)
}

func (p *Person) Say() {
    fmt.Printf("hello, %s", p.Name)
}

在對接口變量進行初始化時,可以使用結構體或者結構體指針

var r Runner
r = &Person{Name: "song_chh"}
r = Person{Name: "song_chh"}

因爲實現接口的接受者類型和接口初始化時的類型都有兩個維度,就會產生四種不同情況的編碼
在這裏插入圖片描述

  • × 表示編譯不通過

下面兩種情況能夠通過編譯很好理解:

  • 方法接受者和初始化類型都是結構體值
  • 方法接受者和初始化類型都是結構體指針

首先,我們來看一下能夠通過編譯的情況,也就是方法接收者是結構體,而初始化的變量是指針類型


type Runner interface {
    Run()
    Say()
}

type Person struct {
    Name string
}

func (p Person) Run() {
    fmt.Printf("%s is running\n", p.Name)
}

func (p *Person) Say() {
    fmt.Printf("hello, %s", p.Name)
}

func main() {
    var r Runner
    r = &Person{Name: "song_chh"}
    r.Run()
    r.Say()
}

上述代碼中,Person結構體指針是能夠直接調用Run和Say,因爲作爲結構體指針,能夠隱式獲取到底層的結構體,然後在通過結構體調用對應的方法
如果將引用去掉,即變量初始化使用結構體類型

r = Person{Name: "song_chh"}

則會提示編譯不通過

./pointer.go:24:4: cannot use Person literal (type Person) as type Runner in assignment:
  Person does not implement Runner (Say method has pointer receiver)

那麼爲什麼會編譯不通過呢?首先在Go語言在進行參數傳遞都是 值傳遞
當代碼中的變量是 &Person{} 時,在方法調用的過程中會對參數進行復制,創建一個新的 Person 結構體指針,指針指向一個確定的結構體,所以編譯器會隱式的對變量解引用獲取指針指向的結構體,完成方法的調用
在這裏插入圖片描述
當代碼中的變量是 Person{}時,在方法調用的過程中會對參數進行復制,也就是 Run() 和 Say() 會接受一個新的 Person{} 變量。如果方法接收者是 *Person ,編譯器無法根據結構體找到一個唯一的指針,所以編譯器會報錯
在這裏插入圖片描述

注意:一個具體類型T的變量,直接調用*T的方法也是合法的,因爲編譯器會隱式的幫你完成取地址操作,但這僅僅是一個語法糖

nil和non-nil

再看一段示例,還是Runner接口和Person結構體,注意看main()函數體,首先聲明一個接口變量r,打印是否爲nil,緊接着定義一個*Person類型的p,打印p是否爲nil,最後將p賦值給r,打印此時的r是否爲nil


type Runner interface {
    Run()
}

type Person struct {
    Name string
}

func (p Person) Run() {
    fmt.Printf("%s is running\n", p.Name)
}

func main() {
    var r Runner
    fmt.Println("r:", r == nil)

    var p *Person
    fmt.Println("p:", p == nil)

    r = p 
    fmt.Println("r:", r == nil)
}

輸出結果是什麼?

r: true or false
p: true or false
r: true or false

實際輸出結果爲:

r: true
p: true
r: false

前兩個輸出r爲nil和p爲nil,因爲接口類型和指針類型的零值爲nil,那麼當p賦值給r後,r卻不爲nil呢?其實是有個接口值的概念

接口值

從概念上來講,一個接口類型的值(簡稱接口值)其實有兩個部分:分別是 具體類型 和 該類型的值 ,二者稱爲接口的動態類型 和動態值 ,所以當且僅當接口的動態類型和動態值都爲nil時,接口值才爲nil
回到2.5的示例中,當p賦值給r接口後,r實際結構如圖所示
在這裏插入圖片描述
驗證一下是否真的是這樣,在main函數體末尾加上一行代碼

fmt.Printf("r type: %T, data: %v\n", r, r)

運行結果

r type: *main.Person, data: <nil>

可以看到動態值確實爲nil

現在已經知道接口值的概念,那麼接口底層實現具體是怎樣的呢?

實現原理

Go語言中的接口類型會根據是否包含一組方法而分成兩種不同的實現,分別爲包含一組方法的iface結構體和不包含任何方法的eface結構體

iface

iface底層是一個結構體,定義如下:


//runtime/runtime2.go
type iface struct {
  tab  *itab
  data unsafe.Pointer
}

iface內部有兩個指針,一個是itab結構體指針,另一個是指向數據的指針

unsafe.Pointer類型是一種特殊類型的指針,它可以存儲任何變量的地址(類似C語言的void*)


//runtime/runtime2.go
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.
}

itab用於表示具體類型和接口類型關係,其中 inter 是接口類型定義信息,_type 是具體類型的信息,hash是_type.hash的拷貝,在類型轉換時,快速判斷目標類型和接口中類型是否一致,fun是實現方法地址列表,雖然fun固定長度爲1的數組,但是這其實是一個柔型數組,保存元素的數量是不確定的,如有多個方法,則會按照字典順序排序


//runtime/type.go
type interfacetype struct {
  typ     _type
  pkgpath name
  mhdr    []imethod
}

interfacetype是描述接口定義的信息,_type:接口的類型信息,pkgpath是定義接口的包名;,mhdr是接口中定義的函數表,按字典序排序

假設接口有ni個方法,實現接口的結構體有nt個方法,正常情況itab函數表生成時間複雜爲O(ni*nt),如果接口方法列表和結構體方法列表有序,那麼函數表生成時間複雜度爲O(ni+nt)


//runtime/type.go
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是所有類型的公共描述。size是類型的大小,hash是類型的哈希值;tflag是類型的tags,與反射相關,align和fieldalign與內存對齊相關,kind是類型編號,具體定義位於runtime/typekind.go中,gcdata是gc相關信息
整個iface的結構圖如下所示:

在這裏插入圖片描述

eface

相對於iface,eface結構比較簡單


//runtime/runtime2.go
type eface struct {
  _type *_type
  data  unsafe.Pointer
}

eface內部同樣有兩個指針,一個具體類型信息_type結構體的指針,一個指向數據的指針
在這裏插入圖片描述

具體類型轉換成接口類型

到此已經知道什麼是接口以及接口的底層結構,那麼當具體類型賦值給接口類型時,是如何進行轉換的?再來看下2.3中的示例


package main

import "fmt"

type Runner interface {
  Run()
}

type Person struct {
  Name string
}

func (p Person) Run() {
  fmt.Printf("%s is running\n", p.Name)
}

func main() {
  var r Runner
  r = Person{Name: "song_chh"}
  r.Run()
}

通過Go提供的工具生成彙編代碼

go tool compile -S interface.go

只截取與第19行相關的代碼

0x001d 00029 (interface.go:19)  PCDATA  $2, $0
0x001d 00029 (interface.go:19)  PCDATA  $0, $1
0x001d 00029 (interface.go:19)  XORPS  X0, X0
0x0020 00032 (interface.go:19)  MOVUPS  X0, ""..autotmp_1+32(SP)
0x0025 00037 (interface.go:19)  PCDATA  $2, $1
0x0025 00037 (interface.go:19)  LEAQ  go.string."song_chh"(SB), AX
0x002c 00044 (interface.go:19)  PCDATA  $2, $0
0x002c 00044 (interface.go:19)  MOVQ  AX, ""..autotmp_1+32(SP)
0x0031 00049 (interface.go:19)  MOVQ  $8, ""..autotmp_1+40(SP)
0x003a 00058 (interface.go:19)  PCDATA  $2, $1
0x003a 00058 (interface.go:19)  LEAQ  go.itab."".Person,"".Runner(SB), AX
0x0041 00065 (interface.go:19)  PCDATA  $2, $0
0x0041 00065 (interface.go:19)  MOVQ  AX, (SP)
0x0045 00069 (interface.go:19)  PCDATA  $2, $1
0x0045 00069 (interface.go:19)  PCDATA  $0, $0
0x0045 00069 (interface.go:19)  LEAQ  ""..autotmp_1+32(SP), AX
0x004a 00074 (interface.go:19)  PCDATA  $2, $0
0x004a 00074 (interface.go:19)  MOVQ  AX, 8(SP)
0x004f 00079 (interface.go:19)  CALL  runtime.convT2I(SB)
0x0054 00084 (interface.go:19)  MOVQ  16(SP), AX
0x0059 00089 (interface.go:19)  PCDATA  $2, $2
0x0059 00089 (interface.go:19)  MOVQ  24(SP), CX

可以看到,編譯器在構造itab後調用runtime.convT2I(SB)轉換函數,看下函數的實現

//runtime/iface.go
func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
  t := tab._type
  if raceenabled {
    raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I))
  }
  if msanenabled {
    msanread(elem, t.size)
  }
  x := mallocgc(t.size, t, true)
  typedmemmove(t, x, elem)
  i.tab = tab
  i.data = x
  return
}

首先根據類型大小調用mallocgc申請一塊內存空間,將elem指針的內容拷貝到新空間,將tab賦值給iface的tab,將新內存指針賦值給iface的data,這樣一個iface就創建完成

將示例代碼稍作更改,使用結構體指針類型的變量賦值給接口變量

r = &Person{Name: "song_chh"}

再次通過工具生成彙編代碼

go tool compile -S interface.go

查看如下彙編代碼


0x001d 00029 (interface.go:19)  PCDATA  $2, $1
0x001d 00029 (interface.go:19)  PCDATA  $0, $0
0x001d 00029 (interface.go:19)  LEAQ  type."".Person(SB), AX
0x0024 00036 (interface.go:19)  PCDATA  $2, $0
0x0024 00036 (interface.go:19)  MOVQ  AX, (SP)
0x0028 00040 (interface.go:19)  CALL  runtime.newobject(SB)
0x002d 00045 (interface.go:19)  PCDATA  $2, $2
0x002d 00045 (interface.go:19)  MOVQ  8(SP), DI
0x0032 00050 (interface.go:19)  MOVQ  $8, 8(DI)
0x003a 00058 (interface.go:19)  PCDATA  $2, $-2
0x003a 00058 (interface.go:19)  PCDATA  $0, $-2
0x003a 00058 (interface.go:19)  CMPL  runtime.writeBarrier(SB), $0
0x0041 00065 (interface.go:19)  JNE  105
0x0043 00067 (interface.go:19)  LEAQ  go.string."song_chh"(SB), AX
0x004a 00074 (interface.go:19)  MOVQ  AX, (DI)

首先編譯器通過type."".Person(SB)獲取Person結構體類型,作爲參數調用runtime.newobject()函數,同樣的在源碼中查看函數定義


import "unsafe"

// runtime/malloc.go

// implementation of new builtin
// compiler (both frontend and SSA backend) knows the signature
// of this function
func newobject(typ *_type) unsafe.Pointer {
  return mallocgc(typ.size, typ, true)
}

newobject以*Person作爲入參,創建新的Person結構體指針,之後由編譯器設置值,iface由編譯器直接生成
除了convT2I函數外,其實在runtime/runtime.go文件中,還有很多轉換函數的定義

// Non-empty-interface to non-empty-interface conversion.
func convI2I(typ *byte, elem any) (ret any)

// Specialized type-to-interface conversion.
// These return only a data pointer.
func convT16(val any) unsafe.Pointer     // val must be uint16-like (same size and alignment as a uint16)
func convT32(val any) unsafe.Pointer     // val must be uint32-like (same size and alignment as a uint32)
func convT64(val any) unsafe.Pointer     // val must be uint64-like (same size and alignment as a uint64 and contains no pointers)
func convTstring(val any) unsafe.Pointer // val must be a string
func convTslice(val any) unsafe.Pointer  // val must be a slice

// Type to empty-interface conversion.
func convT2E(typ *byte, elem *any) (ret any)
func convT2Enoptr(typ *byte, elem *any) (ret any)

// Type to non-empty-interface conversion.   
func convT2I(tab *byte, elem *any) (ret any)        //for the general case
func convT2Inoptr(tab *byte, elem *any) (ret any)   //for structs that do not contain pointers

convI2I用於接口轉換成另一個接口時調用,在3.4會進行講解

convT2Inoptr用於變量內部不含指針的轉換,noptr可以理解爲no pointer,轉換過程與convT2I類似

convT16、convT32、convT64、convTstring 和 convTslice是針對簡單類型轉接口的特例優化,有興趣的可以看下函數實現的源碼,因爲這幾個函數內容相似,這裏就簡單介紹下convT64

//runtime/iface.go
func convT64(val uint64) (x unsafe.Pointer) {
  if val == 0 {
    x = unsafe.Pointer(&zeroVal[0])
  } else {
    x = mallocgc(8, uint64Type, false)
    *(*uint64)(x) = val
  }
  return
}

相較於convT2系列函數,缺少typedmemmove和memmove函數的調用,減少內存拷貝。另外如果變量值爲該類型的零值,則不會調用 mallocgc 去申請一塊新內存,而是直接返回指向zeroVal[0]的指針

再來看下,空接口轉換函數convT2E

func convT2E(t *_type, elem unsafe.Pointer) (e eface) {
  if raceenabled {
    raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2E))
  }
  if msanenabled {
    msanread(elem, t.size)
  }
  x := mallocgc(t.size, t, true)
  // TODO: We allocate a zeroed object only to overwrite it with actual data.
  // Figure out how to avoid zeroing. Also below in convT2Eslice, convT2I, convT2Islice.
  typedmemmove(t, x, elem)
  e._type = t
  e.data = x
  return
}

convT2E 和 convT2I類似,同樣在轉換成eface時*_type是由編譯器生成,當做入參調用convT2E

接口與接口的轉換

如果某個類型實現多個接口,那接口直接是如何進行轉換的,還是先看一段示例:


package main

import "fmt"

type Runner interface {
  Run()
  Say()
}

type Sayer interface {
  Say()
}

type Person struct {
  Name string
}

func (p Person) Run() {
  fmt.Printf("%s is running\n", p.Name)
}

func (p Person) Say() {
  fmt.Printf("hello, %s", p.Name)
}

func main() {
  var r Runner
  r = Person{Name: "song_chh"}

  var s Sayer
  s = r
  s.Say()
}

增加Sayer接口定義,包含Say()方法,在main函數中聲明一個Sayer變量,並將Runner接口變量賦值給Sayer變量。因爲Person實現了Say()方法,所以說Person既實現了是Runner接口,又實現了Sayer接口

執行命令

go tool compile -S interface.go

截取32行彙編代碼

0x0062 00098 (interface.go:32)  PCDATA  $2, $3
0x0062 00098 (interface.go:32)  LEAQ    type."".Sayer(SB), DX
0x0069 00105 (interface.go:32)  PCDATA  $2, $2
0x0069 00105 (interface.go:32)  MOVQ    DX, (SP) 
0x006d 00109 (interface.go:32)  MOVQ    AX, 8(SP)
0x0072 00114 (interface.go:32)  PCDATA  $2, $0
0x0072 00114 (interface.go:32)  MOVQ    CX, 16(SP)
0x0077 00119 (interface.go:32)  CALL    runtime.convI2I(SB)
0x007c 00124 (interface.go:32)  MOVQ    24(SP), AX
0x0081 00129 (interface.go:32)  PCDATA  $2, $2
0x0081 00129 (interface.go:32)  MOVQ    32(SP), CX

可以看到在執行期間,調用runtime.convI2I進行接口轉換,接下來看下源代碼


func convI2I(inter *interfacetype, i iface) (r iface) {
  tab := i.tab
  if tab == nil {
    return
  }
  if tab.inter == inter {
    r.tab = tab
    r.data = i.data
    return
  }
  r.tab = (inter, tab._type, false)
  r.data = i.data
  return
}

函數參數inter表示接口的類型,由編譯器生成,即type."".Sayer(SB),i 是綁定實體的接口, r 是轉換後新的接口,如果要轉換的接口是同一類型,則直接把 i 的tab和data給新接口 r ,將 r 返回。如果要轉換的接口不是同一類型,則通過getitab生成一個新的tab複製給r.tab,然後將 r 返回

那麼具體來看一下getitab這個函數,還是先看源碼


func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
  if len(inter.mhdr) == 0 {
    throw("internal error - misuse of itab")
  }

  // easy case
  if typ.tflag&tflagUncommon == 0 {
    if canfail {
      return nil
    }
    name := inter.typ.nameOff(inter.mhdr[0].name)
    panic(&TypeAssertionError{nil, typ, &inter.typ, name.name()})
  }

  var m *itab

  // First, look in the existing table to see if we can find the itab we need.
  // This is by far the most common case, so do it without locks.
  // Use atomic to ensure we see any previous writes done by the thread
  // that updates the itabTable field (with atomic.Storep in itabAdd).
  t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))
  if m = t.find(inter, typ); m != nil {
    goto finish
  }

  // Not found.  Grab the lock and try again.
  lock(&itabLock)
  if m = itabTable.find(inter, typ); m != nil {
    unlock(&itabLock)
    goto finish
  }

  // Entry doesn't exist yet. Make a new entry & add it.
  m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))
  m.inter = inter
  m._type = typ
  m.init()
  itabAdd(m)
  unlock(&itabLock)
finish:
  if m.fun[0] != 0 {
    return m
  }
  if canfail {
    return nil
  }
  // this can only happen if the conversion
  // was already done once using the , ok form
  // and we have a cached negative result.
  // The cached result doesn't record which
  // interface function was missing, so initialize
  // the itab again to get the missing function name.
  panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})
}

首先,用 t 保存全局itabTable的地址,使用t.find進行查找,這個是通用的查找

如果沒有查找到,就會上鎖,重新使用itabTable.find進行查找

再沒有找到,就會根據具體類型typ和接口類型inter生成一個itab,並將這個新生成的itab添加到全局的itabTable中。如果具體類型並沒有實現接口,根據canfail值返回nil或者painc

斷言

上一節的內容主要介紹如何把具體類型轉換成接口類型,那麼怎樣將接口類型轉換成具體類型呢?Go語言提供兩種方式,分別是類型斷言和類型分支

type assertion
類型斷言有兩種寫法

v := x.(T)
v, ok := x.(T)
  • x:是一個接口類型的的表達式
  • T:是一個已知類型

注意第一種寫法,如果類型斷言失敗,會觸發painc

type switch

switch x := x.(type) { /* ... */}

使用示例

switch i.(type) {
case string:
    fmt.Println("i'm a string")
case int:
    fmt.Println("i'm a int")
default:
    fmt.Println("unknown")
}

接口轉具體類型時,是由編譯器進行對比,進行轉換的,並非是在運行時動態調用某個函數

參考文獻

【1】 《Go程序設計語言》機械工業出版社

【2】 《golang中interface底層分析 出自簡書 XITEHIP》

【3】 《淺談 Go 語言實現原理》出自博客[draveness.me]

【4】 《深度解密Go語言之關於interface的10個問題 出自博客園 Stefno》

【4】 https://mp.weixin.qq.com/s/hMbCN13RWnq_f3t4-Me2xA

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章