Golang interface接口深刻理解

[TOC]golang

Golang interface接口深刻理解

interface 介紹

若是說goroutine和channel是Go併發的兩大基石,那麼接口是Go語言編程中數據類型的關鍵。在Go語言的實際編程中,幾乎全部的數據結構都圍繞接口展開,接口是Go語言中全部數據結構的核心。編程

Go不是一種典型的OO語言,它在語法上不支持類和繼承的概念。數組

沒有繼承是否就沒法擁有多態行爲了呢?答案是否認的,Go語言引入了一種新類型—Interface,它在效果上實現了相似於C++的「多態」概念,雖然與C++的多態在語法上並不是徹底對等,但至少在最終實現的效果上,它有多態的影子。緩存

雖然Go語言沒有類的概念,但它支持的數據類型能夠定義對應的method(s)。本質上說,所謂的method(s)其實就是函數,只不過與普通函數相比,這類函數是做用在某個數據類型上的,因此在函數簽名中,會有個receiver(接收器)來代表當前定義的函數會做用在該receiver上。bash

Go語言支持的除Interface類型外的任何其它數據類型均可以定義其method(而並不是只有struct才支持method),只不過實際項目中,method(s)多定義在struct上而已。 從這一點來看,咱們能夠把Go中的struct看做是不支持繼承行爲的輕量級的「類」。微信

從語法上看,Interface定義了一個或一組method(s),這些method(s)只有函數簽名,沒有具體的實現代碼(有沒有聯想起C++中的虛函數?)。若某個數據類型實現了Interface中定義的那些被稱爲"methods"的函數,則稱這些數據類型實現(implement)了interface。這是咱們經常使用的OO方式,以下是一個簡單的示例數據結構

type MyInterface interface{
       Print()
   }
   
   func TestFunc(x MyInterface) {}
   type MyStruct struct {}
   func (me MyStruct) Print() {}
   
   func main() {
       var me MyStruct
       TestFunc(me)
   }
複製代碼

Why Interface

爲何要用接口呢?在Gopher China 上的分享中,有大神給出了下面的理由:併發

writing generic algorithm (泛型編程)函數

hiding implementation detail (隱藏具體實現)源碼分析

providing interception points

下面大致再介紹下這三個理由

writing generic algorithm (泛型編程)

嚴格來講,在 Golang 中並不支持泛型編程。在 C++ 等高級語言中使用泛型編程很是的簡單,因此泛型編程一直是 Golang 詬病最多的地方。可是使用 interface 咱們能夠實現泛型編程,以下是一個參考示例

package sort

    // A type, typically a collection, that satisfies sort.Interface can be
    // sorted by the routines in this package.  The methods require that the
    // elements of the collection be enumerated by an integer index.
    type Interface interface {
        // Len is the number of elements in the collection.
        Len() int
        // Less reports whether the element with
        // index i should sort before the element with index j.
        Less(i, j int) bool
        // Swap swaps the elements with indexes i and j.
        Swap(i, j int)
    }
    
    ...
    
    // Sort sorts data.
    // It makes one call to data.Len to determine n, and O(n*log(n)) calls to
    // data.Less and data.Swap. The sort is not guaranteed to be stable.
    func Sort(data Interface) {
        // Switch to heapsort if depth of 2*ceil(lg(n+1)) is reached.
        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 函數,這樣就實現了「泛型編程」。

這種方式,我在閃聊項目裏面也有實際應用過,具體案例就是對消息排序。

下面給一個具體示例,代碼可以說明一切,一看就懂:

type Person struct {
   Name string
   Age  int
   }
   
   func (p Person) String() string {
       return fmt.Sprintf("%s: %d", p.Name, p.Age)
   }
   
   // ByAge implements sort.Interface for []Person based on
   // the Age field.
   type ByAge []Person //自定義
   
   func (a ByAge) Len() int           { return len(a) }
   func (a ByAge) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
   func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
   
   func main() {
       people := []Person{
           {"Bob", 31},
           {"John", 42},
           {"Michael", 17},
           {"Jenny", 26},
       }
   
       fmt.Println(people)
       sort.Sort(ByAge(people))
       fmt.Println(people)
   }
   
複製代碼

hiding implementation detail (隱藏具體實現)

隱藏具體實現,這個很好理解。好比我設計一個函數給你返回一個 interface,那麼你只能經過 interface 裏面的方法來作一些操做,可是內部的具體實現是徹底不知道的。

例如咱們經常使用的context包,就是這樣的,context 最早由 google 提供,如今已經歸入了標準庫,並且在原有 context 的基礎上增長了:cancelCtx,timerCtx,valueCtx。

恰好前面咱們有專門說過context,如今再來回顧一下

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
        c := newCancelCtx(parent)
        propagateCancel(parent, &c)
        return &c, func() { c.cancel(true, Canceled) }
    }
    
複製代碼

代表上 WithCancel 函數返回的仍是一個 Context interface,可是這個 interface 的具體實現是 cancelCtx struct。

// newCancelCtx returns an initialized cancelCtx.
       func newCancelCtx(parent Context) cancelCtx {
           return cancelCtx{
               Context: parent,
               done:    make(chan struct{}),
           }
       }
       
       // A cancelCtx can be canceled. When canceled, it also cancels any children
       // that implement canceler.
       type cancelCtx struct {
           Context     //注意一下這個地方
       
           done chan struct{} // closed by the first cancel call.
           mu       sync.Mutex
           children map[canceler]struct{} // set to nil by the first cancel call
           err      error                 // set to non-nil by the first cancel call
       }
       
       func (c *cancelCtx) Done() <-chan struct{} {
           return c.done
       }
       
       func (c *cancelCtx) Err() error {
           c.mu.Lock()
           defer c.mu.Unlock()
           return c.err
       }
       
       func (c *cancelCtx) String() string {
           return fmt.Sprintf("%v.WithCancel", c.Context)
       }
複製代碼

儘管內部實現上下面三個函數返回的具體 struct (都實現了 Context interface)不一樣,可是對於使用者來講是徹底無感知的。

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

暫無更多,待補充

interface 源碼分析

說了這麼多, 而後能夠再來瞧瞧具體源碼的實現

interface 底層結構

根據 interface 是否包含有 method,底層實現上用兩種 struct 來表示:iface 和 eface。eface表示不含 method 的 interface 結構,或者叫 empty interface。對於 Golang 中的大部分數據類型均可以抽象出來 _type 結構,同時針對不一樣的類型還會有一些其餘信息。

type eface struct {
        _type *_type
        data  unsafe.Pointer
    }
    
    type _type struct {
        size       uintptr // type size
        ptrdata    uintptr // size of memory prefix holding all pointers
        hash       uint32  // hash of type; avoids computation in hash tables
        tflag      tflag   // extra type information flags
        align      uint8   // alignment of variable with this type
        fieldalign uint8   // alignment of struct field with this type
        kind       uint8   // enumeration for C
        alg        *typeAlg  // algorithm table
        gcdata    *byte    // garbage collection data
        str       nameOff  // string form
        ptrToThis typeOff  // type for pointer to this type, may be zero
    }
    
複製代碼

iface 表示 non-empty interface 的底層實現。相比於 empty interface,non-empty 要包含一些 method。method 的具體實現存放在 itab.fun 變量裏。

type iface struct {
        tab  *itab
        data unsafe.Pointer
    }
    
    // layout of Itab known to compilers
    // allocated in non-garbage-collected memory
    // Needs to be in sync with
    // ../cmd/compile/internal/gc/reflect.go:/^func.dumptypestructs.
    type itab struct {
        inter  *interfacetype
        _type  *_type
        link   *itab
        bad    int32
        inhash int32      // has this itab been added to hash?
        fun    [1]uintptr // variable sized
    }

複製代碼

試想一下,若是 interface 包含多個 method,這裏只有一個 fun 變量怎麼存呢? 其實,經過反編譯彙編是能夠看出的,中間過程編譯器將根據咱們的轉換目標類型的 empty interface 仍是 non-empty interface,來對原數據類型進行轉換(轉換成 <*_type, unsafe.Pointer> 或者 <*itab, unsafe.Pointer>)。這裏對於 struct 滿不知足 interface 的類型要求(也就是 struct 是否實現了 interface 的全部 method),是由編譯器來檢測的。

iface 之 itab

iface 結構中最重要的是 itab 結構。itab 能夠理解爲 pair<interface type, concrete type> 。固然 itab 裏面還包含一些其餘信息,好比 interface 裏面包含的 method 的具體實現。下面細說。itab 的結構以下。

type itab struct {
        inter  *interfacetype
        _type  *_type
        link   *itab
        bad    int32
        inhash int32      // has this itab been added to hash?
        fun    [1]uintptr // variable sized
    }
複製代碼

其中 interfacetype 包含了一些關於 interface 自己的信息,好比 package path,包含的 method。上面提到的 iface 和 eface 是數據類型(built-in 和 type-define)轉換成 interface 以後的實體的 struct 結構,而這裏的 interfacetype 是咱們定義 interface 時候的一種抽象表示。

type interfacetype struct {
        typ     _type
        pkgpath name
        mhdr    []imethod
    }
    
    type imethod struct {   //這裏的 method 只是一種函數聲明的抽象,好比  func Print() error
        name nameOff
        ityp typeOff
    }
    
複製代碼

_type 表示 concrete type。fun 表示的 interface 裏面的 method 的具體實現。好比 interface type 包含了 method A, B,則經過 fun 就能夠找到這兩個 method 的具體實現。

interface的內存佈局

瞭解interface的內存結構是很是有必要的,只有瞭解了這一點,咱們才能進一步分析諸如類型斷言等狀況的效率問題。先看一個例子:

type Stringer interface {
        String() string
    }
    
    type Binary uint64
    
    func (i Binary) String() string {
        return strconv.Uitob64(i.Get(), 2)
    }
    
    func (i Binary) Get() uint64 {
        return uint64(i)
    }
    
    func main() {
        b := Binary{}
        s := Stringer(b)
        fmt.Print(s.String())
    }
    
複製代碼

根據上面interface的源碼實現,能夠知道,interface在內存上實際由兩個成員組成,以下圖,tab指向虛表,data則指向實際引用的數據。虛表描繪了實際的類型信息及該接口所須要的方法集

![Uploading interface內存佈局_731644.png]

觀察itable的結構,首先是描述type信息的一些元數據,而後是知足Stringger接口的函數指針列表(注意,這裏不是實際類型Binary的函數指針集哦)。所以咱們若是經過接口進行函數調用,實際的操做其實就是s.tab->fun0。是否是和C++的虛表很像?接下來咱們要看看golang的虛表和C++的虛表區別在哪裏。

先看C++,它爲每種類型建立了一個方法集,而它的虛表實際上就是這個方法集自己或是它的一部分而已,當面臨多繼承時(或者叫實現多個接口時,這是很常見的),C++對象結構裏就會存在多個虛表指針,每一個虛表指針指向該方法集的不一樣部分,所以,C++方法集裏面函數指針有嚴格的順序。許多C++新手在面對多繼承時就變得蛋疼菊緊了,由於它的這種設計方式,爲了保證其虛表可以正常工做,C++引入了不少概念,什麼虛繼承啊,接口函數同名問題啊,同一個接口在不一樣的層次上被繼承屢次的問題啊等等……就是老手也很容易因疏忽而寫出問題代碼出來。

咱們再來看golang的實現方式,同C++同樣,golang也爲每種類型建立了一個方法集,不一樣的是接口的虛表是在運行時專門生成的。可能細心的同窗可以發現爲何要在運行時生成虛表。由於太多了,每一種接口類型和全部知足其接口的實體類型的組合就是其可能的虛表數量,實際上其中的大部分是不須要的,所以golang選擇在運行時生成它,例如,當例子中當首次碰見s := Stringer(b)這樣的語句時,golang會生成Stringer接口對應於Binary類型的虛表,並將其緩存。

理解了golang的內存結構,再來分析諸如類型斷言等狀況的效率問題就很容易了,當斷定一種類型是否知足某個接口時,golang使用類型的方法集和接口所須要的方法集進行匹配,若是類型的方法集徹底包含接口的方法集,則可認爲該類型知足該接口。例如某類型有m個方法,某接口有n個方法,則很容易知道這種斷定的時間複雜度爲O(mXn),不過可使用預先排序的方式進行優化,實際的時間複雜度爲O(m+n)。

interface 與 nil 的比較

引用公司內部同事的討論議題,以爲以前本身也沒有理解明白,爲此,單獨羅列出來,例子是最好的說明,以下

package main

import (
	"fmt"
	"reflect"
)

type State struct{}

func testnil1(a, b interface{}) bool {
	return a == b
}

func testnil2(a *State, b interface{}) bool {
	return a == b
}

func testnil3(a interface{}) bool {
	return a == nil
}

func testnil4(a *State) bool {
	return a == nil
}

func testnil5(a interface{}) bool {
	v := reflect.ValueOf(a)
	return !v.IsValid() || v.IsNil()
}

func main() {
	var a *State
	fmt.Println(testnil1(a, nil))
	fmt.Println(testnil2(a, nil))
	fmt.Println(testnil3(a))
	fmt.Println(testnil4(a))
	fmt.Println(testnil5(a))
}
複製代碼

返回結果以下

false
false
false
true
true
複製代碼

爲啥呢?

一個interface{}類型的變量包含了2個指針,一個指針指向值的類型,另一個指針指向實際的值 對一個interface{}類型的nil變量來講,它的兩個指針都是0;可是var a *State傳進去後,指向的類型的指針不爲0了,由於有類型了, 因此比較爲false。 interface 類型比較, 要是 兩個指針都相等, 才能相等。

【"歡迎關注個人微信公衆號:Linux 服務端系統研發,後面會大力經過微信公衆號發送優質文章"】

個人微信公衆號
相關文章
相關標籤/搜索