Go介面詳解

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

Go介面的設計和實現是Go整個類型系統的一大特點。介面組合和嵌入、duck typing等實現了優雅的代碼複用、解耦、模組化的特性,而且介面是方法動態指派、反射的實現基礎(當然更基礎的是編譯期為運行時提供的類型資訊)。理解了介面的實現之後,就不難理解"著名"的nil傳回值問題以及反射、type switch、type assertion等原理。本文主要基於Go1.8.1的源碼介紹介面的內部實現及其使用相關的問題。


1. 介面的實現

(1) 下面是介面在runtime中的實現,注意其中包含了介面本身和實際資料類型的類型資訊:

// src/runtime/runtime2.gotype iface struct {  // 包含介面的靜態類型資訊、資料的動態類型資訊、函數表  tab  *itab  // 指向具體資料的記憶體位址比如slice、map等,或者在介面  // 轉換時直接存放小資料(一個指標的長度)  data unsafe.Pointer}type itab struct {  // 介面的類型資訊  inter  *interfacetype  // 具體資料的類型資訊  _type  *_type  link   *itab  hash   uint32  bad    bool   inhash bool  unused [2]byte  // 函數地址表,這裡放置和介面方法對應的具體資料類型的方法地址  // 實現介面調用方法的動態指派,一般在每次給介面賦值發生轉換時  // 會更新此表,或者直接拿緩衝的itab  fun    [1]uintptr // variable sized}

(2) 另外,需要注意與介面相關的兩點最佳化,會影響到反射等的實現:

  • 空介面(interface{})的itab最佳化

當將某個類型賦值給空介面時,由於空介面沒有方法,所以空介面eface的tab會直接指向資料的具體類型。在Go的reflect包中,reflect.TypeOf和reflect.ValueOf的參數都是空介面,因此所有參數都會先轉換為空白介面類型。這樣反射就實現了對所有參數類型擷取實際資料類型的統一。這在後面反射的基本實現中會分析到。

  • 發生“介面轉換”時data欄位相關的最佳化

當被轉換為介面的資料的類型長度不超過一個指標的長度時(比如pointer、map、func、chan、[1]int等類型),介面轉換時會將資料直接拷貝存放到介面的data欄位中(DirectIface),而不再額外分配記憶體並拷貝。另外,從go1.8+的源碼來看除了DirectIface的最佳化以外,還對長度較小(不超過64位元組,未初始化資料記憶體的array,Null 字元串等)的零值做了最佳化,也不會重新分配記憶體,而是直接指向一個包級全域陣列變數zeroVal的首地址。注意這裡的最佳化發生在介面轉換時產生的臨時介面上,而不是被賦值的介面左值上。


(3) 再者,在Go中只有值傳遞(包括介面類型),與具體的類型實現無關,但是某些類型具有引用的屬性。典型的9種非基礎類型中:

  • array傳遞會拷貝整塊資料記憶體,傳遞長度為len(arr) * Sizeof(elem)
  • string、slice、interface傳遞的是其runtime的實現,所以長度是固定的,分別為16、24、16位元組(amd64)
  • map、func、chan、pointer傳遞的是指標,所以長度固定為8位元組(amd64)
  • struct傳遞的是所有欄位的記憶體拷貝,所以長度是所有欄位的長度和
  • 詳細的測試可以參考[這段程式](pass_by_value_main.go)

2. runtime中介面的轉換操作

介面相關的操作主要在於對其內部欄位itab的操作,因為介面轉換最重要的是類型資訊。這裡簡單分析幾個runtime中相關的函數。主要實現在`src/runtime/iface.go`中。值得注意的是,介面的類型轉換在編譯期會產生一個函數調用的文法樹節點(OCALL),調用runtime提供的相應介面轉換函式完成介面的類型設定,所以介面的轉換是在運行時發生的,其具體類型的方法地址表也是在運行時填寫的,這一點和C++的虛函數表不太一樣。另外,由於在運行時轉換會產生開銷,所以對轉換的itab做了緩衝。

type MyReader struct {}func (r MyReader) Read(b []byte) (n int, err error) {}// 介面的相關轉換編譯成對相關runtime函數的調用,比如convI2I/assertI2I等var i io.Reader = MyReader{}realReader := i.(MyReader)var ei interface{} = interface{}(realReader)

下面以convI2I為例來說明,編譯時間產生OCALL文法樹節點的過程。

// src/cmd/compile/internal/gc/walk.gofunc convFuncName(from, to *types.Type) string {tkind := to.Tie()switch from.Tie() {        // 將介面轉換為另一介面,返回需要在runtime中調用的函數名case 'I':switch tkind {case 'I':return "convI2I"}case 'T':/* ... */}// src/cmd/compile/internal/gc/walk.go// 這裡只給出節點操作類型為OCONVIFACE(即inerface轉換)的處理邏輯func walkexpr(n *Node, init *Nodes) *Node {    case OCONVIFACE:        n.Left = walkexpr(n.Left, init)        /* 這裡省略了很多特殊的處理邏輯,比如空介面相關的最佳化 */        // 到這裡開始進入一般的介面轉換   // 尋找需要調用的runtime的函數,在Runtimepkg中尋找        fn := syslook(convFuncName(n.Left.Type, n.Type))fn = substArgTypes(fn, n.Left.Type, n.Type)dowidth(fn.Type) // 產生函數調用節點n = nod(OCALL, fn, nil)n.List.Set(ll)n = typecheck(n, Erv)n = walkexpr(n, init)}

一旦itab的函數表設定後,後面的介面的方法調用只需要一次間接調用的開銷,不需要反覆尋找方法的地址。關於介面的實現,Russ Cox寫過一篇很好的文章


下面分析runtime中介面相關的幾個主要函數:

  • getitab
// 根據介面類型和實際資料類型產生itabfunc getitab(inter *interfacetype, typ *_type, canfail bool) *itab {  // 先從緩衝中找  h := itabhash(inter, typ)  // look twice - once without lock, once with.  // common case will be no lock contention.  var m *itab  var locked int  for locked = 0; locked < 2; locked++ {    if locked != 0 {      lock(&ifaceLock)    }    for m = (*itab)(atomic.Loadp(unsafe.Pointer(&hash[h]))); m != nil; m = m.link {      // 找到      if m.inter == inter && m._type == typ {        if m.bad {          if !canfail {            // 檢查並Binder 方法地址表            additab(m, locked != 0, false)          }          m = nil        }        if locked != 0 {          unlock(&ifaceLock)        }        return m      }    }  }  // 緩衝中沒找到則分配itab的記憶體: itab結構本身記憶體 + 末尾存方法地址表的可變長度  m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))  m.inter = inter           // 設定介面類型資訊  m._type = typ             // 設定實際資料類型資訊  additab(m, true, canfail) // 設定itab函數調用表  unlock(&ifaceLock)  if m.bad {    return nil  }  return m}
  • additab
// 檢查具體類型是否實現了介面規定的方法,並使用具體類型的方法// 地址填充方法表。func additab(m *itab, locked, canfail bool) {  inter := m.inter  typ := m._type  x := typ.uncommon()  ni := len(inter.mhdr) // 介面方法數量  nt := int(x.mcount)   // 實際資料類型方法數量  xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]  j := 0  for k := 0; k < ni; k++ {    // 對每個介面方法的地址    i := &inter.mhdr[k]    // 使用介面的類型資訊擷取實際類型, 函數名字,包名字    itype := inter.typ.typeOff(i.ityp)    name := inter.typ.nameOff(i.name)    iname := name.name()    ipkg := name.pkgPath()    if ipkg == "" {      ipkg = inter.pkgpath.name()    }    for ; j < nt; j++ {      // 對每個具體類型的方法      t := &xmhdr[j]      tname := typ.nameOff(t.name)      // 具體類型的方法類型和介面方法的類型相同,並且名字相同,則匹配成功      if typ.typeOff(t.mtyp) == itype && tname.name() == iname {        pkgPath := tname.pkgPath()        if pkgPath == "" {          pkgPath = typ.nameOff(x.pkgpath).name()        }        if tname.isExported() || pkgPath == ipkg {          if m != nil {            // 具體類型的某個方法地址            ifn := typ.textOff(t.ifn)            // 填充itab的func表地址            *(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn          }          goto nextimethod        }      }    }    // didn't find method    // 不匹配panic    if !canfail {      if locked {        unlock(&ifaceLock)      }      panic(&TypeAssertionError{"", typ.string(), inter.typ.string(), iname})    }    // 或者設定失敗標識    m.bad = true    break  nextimethod:  }  if !locked {    throw("invalid itab locking")  }  h := itabhash(inter, typ)  m.link = hash[h]  m.inhash = true  // 存到itab的hash表緩衝  atomicstorep(unsafe.Pointer(&hash[h]), unsafe.Pointer(m))}

  • convI2I
// 將已有的介面,轉換為新的介面類型,失敗panic// 比如:// var rc io.ReadCloser// var r io.Reader// rc = io.ReadCloser(r)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  }  // 否則重建itab  r.tab = getitab(inter, tab._type, false)  // 注意這裡沒有分配記憶體拷貝資料  r.data = i.data  return}
  • convT2I

// 使用itab並拷貝資料,得到ifacefunc convT2I(tab *itab, elem unsafe.Pointer) (i iface) {  t := tab._type  if raceenabled {    raceReadObjectPC(t, elem, getcallerpc(unsafe.Pointer(&tab)), funcPC(convT2I))  }  if msanenabled {    msanread(elem, t.size)  }  // 注意這裡發生了記憶體配置和資料拷貝  x := mallocgc(t.size, t, true)  // memmove內部的拷貝對大塊記憶體做了最佳化  typedmemmove(t, x, elem)  i.tab = tab  i.data = x  return}

從上面convX2I我們可以看到,在介面類型之間轉換時,並沒有分配記憶體和拷貝資料,但是將非介面類型轉換為介面類型時,卻發生了記憶體配置和資料拷貝。這裡的原因是Go介面的資料不能被改變,所以介面之間的轉換可以使用同一塊記憶體,但是其他情況為了避免外部改變導致介面內資料改變,所以會進行記憶體配置和資料拷貝。另外,這也是反射非指標變數時無法直接改變變數資料的原因,因為反射會先將變數轉換為空白介面類型。可以參考go-nuts。這裡我們用一個簡單的程式測試一下。

package mainimport "fmt"type Data struct {  n int}func main() {  d := Data{10}  fmt.Printf("address of d: %p\n", &d)  // assign not interface type variable to interface variable  // d will be copied  var i1 interface{} = d  // assign interface type variable to interface variable  // the data of i1 will directly assigned to i2.data and will not be copied  var i2 interface{} = i1  fmt.Println(d)  fmt.Println(i1)  fmt.Println(i2)}// 關掉最佳化和inlinego build -gcflags "-N -l" interface.go// 可以看到介面變數i1和i2的資料地址是相同的,但是d和i1的資料地址不相同(gdb) info locals &d = 0xc420074168i2 = {_type = 0x492e00, data = 0xc4200741a0}i1 = {_type = 0x492e00, data = 0xc4200741a0}

3. type assertion與type switch

理解了介面的實現,不難猜測type assertion和type switch的實現邏輯,我們只需要取出介面的動態類型(資料類型)與目標類型做比較即可,而目標類型的資訊在編譯期是可以確定下來的。可以參考Effective Go中的簡單例子。


4. nil介面的問題

具體的代碼可參考nil介面傳回值測試。理解了介面的底層實現,這個問題其實也比較好理解了。需要說明的是nil在Go中既指空值,也指空類型。這裡的空值並非零值,空值是指未初始化,比如slice沒有分配底層的記憶體。只有chan、interface、func、slice、map、pointer可直接與nil比較和用nil賦值。對於非介面類型來說,對其賦值nil的語義是將其資料變為未初始化的狀態,而給介面類型來說,還會將介面的類型資訊欄位itab置nil。所以:

type MyReader interface {}var r MyReader  // (nil, nil)var n *int = nilvar r1 MyReader = n // (*int, nil)var r2 MyReader // (nil, nil)var inter interface{} = r2 // (nil, nil)

5. 介面與反射

反射實現的一個基本前提是編譯期為運行時提供足夠的類型資訊,一般來說都會使用一個基本類型(比如Go中的interface、Java中的Object)來存放具體類型的資訊,以便在運行時使用。C++到目前為止也沒有比較成熟的反射庫,大部分原因就是沒有比較好的方法提供運行時所需的類型資訊,typeid等運行時資訊遠遠不夠。Go的反射的實現就是基於interface的。這裡簡單分析兩個常用方法`reflect.TypeOf, reflect.ValueOf`的實現。

// src/reflect/value.go// 注意: 從前面的分析可知當轉換為空白介面的時候,itab指標會直接// 指向資料的實際類型,所以反射的入口函數參數類型是interface{},// 轉換後,emptyInterface的rtype欄位會直接指向資料類型,所以// 整個反射才能直接得到資料類型,不然itab指向記憶體的前面部分包含// 的是介面的靜態類型資訊type emptyInterface struct {  typ  *rtype  word unsafe.Pointer}// src/reflect/type.gofunc TypeOf(i interface{}) Type {  // 參數i已經是空介面類型  eface := *(*emptyInterface)(unsafe.Pointer(&i))  return toType(eface.typ)}// src/reflect/value.gofunc ValueOf(i interface{}) Value {  if i == nil {    return Value{}  }  escapes(i)  return unpackEface(i)}// src/reflect/value.gofunc unpackEface(i interface{}) Value {  // 參數i已經是空介面類型  e := (*emptyInterface)(unsafe.Pointer(&i))  t := e.typ  if t == nil {    return Value{}  }  f := flag(t.Kind())  if ifaceIndir(t) {    f |= flagIndir  }  return Value{t, e.word, f}}

6. 介面與duck typing

嚴格說來,Go的介面可能並不算真正的duck typing,看一個Python和Go對比的例子。在這個例子中我們並不管傳入的類型是什麼,也不用在乎Say方法返回的類型是什麼。而在Go中,實現介面的Say方法的傳回值類型也必須相同。但是,這兩個例子中都不需要顯式指定實現的介面,這對於代碼的重構極其有利,這也是Go的介面相對於Java等介面的優勢。

# python duck typingdef callSay(a):    ret = a.Say()    print retclass SayerInt(object):    def Say():        return 1class SayerString(object):    def Say():        return "string"si = SayerInt()ss = SayerString()callSay(si)callSay(ss)// Gotype Sayer interface {  Say() int}func callSay(sayer Sayer) {  sayer.Say()}type Say1Struct struct {}func (s Say1Struct) Say() int {  return 1}type Say2Struct struct {}func (s Say2Struct) Say() int {  return 2}s1 := &Say1Struct{}s2 := &Say2Struct{}callSay(s1)callSay(s2)

7. 總結

綜上,介面在Go的整個類型系統起到重要的作用,而且是反射、方法動態指派、type switch、type assertion等的實現基礎。另外,介面組合和duck typing特性也讓整個類型層次變得更加扁平,寫起來更加簡潔且有利於重構。理解了介面的底層實現,也更容易避免Go使用中的很多問題。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.