深入理解Golang接口

interface 介紹

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

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

沒有繼承是否就無法擁有多態行為了呢?答案是否定的,Go語言引入了一種新類型—Interface,它在效果上實現了類似於C++的“多態”概念,雖然與C++的多態在語法上並非完全對等,但至少在最終實現的效果上,它有多態的影子。

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

Go語言支持的除Interface類型外的任何其它數據類型都可以定義其method(而並非只有struct才支持method),只不過實際項目中,method(s)多定義在struct上而已。 從這一點來看,我們可以把Go中的struct看作是不支持繼承行為的輕量級的“類”。

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

package main
import "fmt"
type MyInterface interface {
Print()
}
func TestFunc(x MyInterface) {
x.Print()
}
type MyStruct struct{}
func (me MyStruct) Print() {
fmt.Println("hello world")
}
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)
}


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() 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


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> 。當然 itab 裡面還包含一些其他信息,比如 interface 裡面包含的 method 的具體實現。下面細說。itab 的結構如下。/<interface>

 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則指向實際引用的數據。虛表描繪了實際的類型信息及該接口所需要的方法集

觀察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)。


分享到:


相關文章: