2018年2月21日 星期三

[轉載]淺析Go語言Interface類型的語法行為及用法

原文
Go不是一種典型的OO語言,它在語法上不支持類和繼承的概念。 沒有繼承是否就無法擁有多態行為了呢?答案是否定的,Go語言引入了一種新類型—Interface,它在效果上實現了類似於C++的「多態」概念,雖然與C++的多態在語法上並非完全對等,但至少在最終實現的效果上,它有多態的影子。
那麼,Go的Interface類型到底是什麼呢?怎麼使用呢?這正是本篇筆記試圖說明的問題。

1. Method(s) in Go

在說明Interface類型前,不得不先用Go的method(s)概念來熱身,因為Go語言的interface與method(s)這兩個語法有非常緊密的聯繫。
雖然Go語言沒有類的概念,但它支持的數據類型可以定義對應的method(s)。本質上說,所謂的method(s)其實就是函數,只不過與普通函數相比,這類函數是作用在某個數據類型上的,所以在函數簽名中,會有個receiver來表明當前定義的函數會作用在該receiver上。
關於methods的精確語法規範,可以參考language specificationEffective Go中的說明,這裡略過。
注意:Go語言支持的除Interface類型外的任何其它數據類型都可以定義其method(而並非只有struct才支持method),只不過實際項目中,method(s)多定義在struct上而已。
在struct類型上定義method(s)的語法特性與C++中的struct支持的語法非常類似(c++中的struct定義了數據,此外也支持定義數據的操作方法),從這一點來看,我們可以把Go中的struct看作是不支持繼承行為的輕量級的「類」。

2. What is Interface type in Go ?

GoLang官網language specification文檔對interface type的概念說明如下:
An interface type specifies a method set called its interface. A variable of interface type can store a value of any type with a method set that is any superset of the interface. Such a type is said to implement the interface. The value of an uninitialized variable of interface type is nil.

說實話,這段說明對新手來說比較晦澀,這正是本篇筆記試圖解釋清楚的地方。

從語法上看,Interface定義了一個或一組method(s),這些method(s)只有函數簽名,沒有具體的實現代碼(有沒有聯想起C++中的虛函數?)。若某個數據類型實現了Interface中定義的那些被稱為"methods"的函數,則稱這些數據類型實現(implement)了interface。舉個例子來說明。
package main  
  
import (  
    "fmt"  
    "math"  
)  
  
type Abser interface {  
    Abs() float64  
}  
  
type MyFloat float64  
  
func (f MyFloat) Abs() float64 {  
    if f < 0 {  
        return float64(-f)  
    }  
    return float64(f)  
}  
  
func main() {  
    var a Abser  
    f := MyFloat(-math.Sqrt2)  
    a = f  // a MyFloat implements Abser  
    fmt.Println(a.Abs())  
}  

上面的代碼中,第8-10行是通過type語法聲明了一個名為Abser的interface類型(Go中約定的interface類型名通常取其內部聲明的method名的er形式)。而第12-19行通過type語法聲明了MyFloat類型且為該類型定義了名為Abs()的method。
根據上面的解釋,Abs()是interface類型Abser定義的方法,而MyFloat實現了該方法,所以,MyFloat實現了Abser接口。
Interface類型的更通用定義可歸納如下:
type Namer interface {  
    Method1(param_list) return_type  
    Method2(param_list) return_type  
    ...  
}  
上面的示例用type語法聲明了一個名為Namer的interface類型(但Namer不是個具體的變量,此時內存中還沒有它對應的對象)。interface類型是可以定義變量的,也即interface type can have values,例如:
var ai Namer
此時,定義了一個變量名為ai的Namer類型變量,在Go的底層實現中,ai本質上是個指針,其內存佈局如下(內存佈局圖引用自<The Way to Go - A Thorough Introduction to the Go Programming Language>一書第11.1節):

它的method table ptr是不是與C++中類的虛函數表非常類似?而這正是interface類型的變量具有多態特性的關鍵:
ai共佔2個機器字,1個為receiver字段,1個為method table ptr字段。ai可以被賦值為任何變量,只要這個變量實現了interface定義的method(s) set,賦值後,ai的receiver字段用來hold那個變量或變量副本的地址(若變量類型小於等於1個機器字大小,則receiver直接存儲那個變量;若變量類型大於1個機器字,則Go底層會在堆上申請空間存儲那個變量的副本,然後receiver存儲那個副本的地址,即此時receiver是個指向變量副本的指針)。而由變量實現的接口method(s)組成的interface table的指針會填充到ai的method table ptr字段。當ai被賦值為另一個變量後,其receiver和method table ptr會更新為新變量的相關值。
關於interface類型內部實現細節,可以參考GoLang官網Blog推薦過的一篇文章「Go Data Structures: Interfaces」,寫的很清楚,強烈推薦。
這正是Interface類型在Go語言中的威力。
引用<The Way to Go>一書第11.5節對interface類型的總結如下,值得每個Go學習者理解:
An interface is a kind of contract which the implementing type(s) must fulfill. Interfaces describe the behaviorof types, what they can do. They completely separate the definition of what an object can do from how it does it, allowing distinct implementations to be represented at different times by the same interface variable, which is what polymorphism essentially is.
Writing functions so that they accept an interface variable as a parameter makes them more general.

3. Interface「多態」特性實例

Go語言自帶的標準Packages提供的接口很多都借助了Interface以具備「可以處理任何未知數據類型」的能力。例如被廣泛使用的fmt包,其功能描述如下:
Package fmt implements formatted I/O with functions analogous to C's printf and scanf. The format 'verbs' are derived from C's but are simpler.
它除了可以格式化打印Go的built-in類型外,還可以正確打印各種自定義類型,只要這些自定義數據類型實現了fmt的Print API入參所需的interface接口。
以fmt包的Printf()函數為例,其函數簽名格式如下:
func Printf(format string, a ...interface{}) (n int, err error)  
它的入參除了用以描述如何格式化的'format'參數外,還需要interface類型的可變長參數。該函數在實現底層的打印行為時,要求傳入的可變長參數實現了fmt包中定義的Stringer接口,這個接口類型定義及描述如下:
type Stringer interface {  
        String() string  
}  
Stringer is implemented by any value that has a String method, which defines the “native” format for that value. The String method is used to print values passed as an operand to any format that accepts a string or to an unformatted printer such as Print.
所以,自定義類型想要調用fmt.Printf()做格式化打印,那只需實現Stringer接口就行。
例如,下面是一段簡單的打印代碼:
package main  
  
import "fmt"  
  
type IPAddr [4]byte  
  
func main() {  
    addrs := map[string]IPAddr{  
        "loopback":  {127, 0, 0, 1},  
        "googleDNS": {8, 8, 8, 8},  
    }  
    for n, a := range addrs {  
        fmt.Printf("%v: %v\n", n, a)  
    }  
}  
其輸出如下:
loopback: [127 0 0 1]  
googleDNS: [8 8 8 8]  

現在要求按規定的格式打印:IPAddr{1, 2, 3, 4}應該輸出為"1.2.3.4"的格式,所以IPAddr這個自定義類型需要實現Stringer接口,實現代碼如下:
// exercise-stringer.go  
package main  
  
import "fmt"  
  
type IPAddr [4]byte  
  
// TODO: Add a "String() string" method to IPAddr.  
func (ip IPAddr) String() string {      
    return fmt.Sprintf("%v.%v.%v.%v", ip[0], ip[1], ip[2], ip[3])  
}  
  
func main() {  
    addrs := map[string]IPAddr{  
        "loopback":  {127, 0, 0, 1},  
        "googleDNS": {8, 8, 8, 8},  
    }  
    for n, a := range addrs {  
        fmt.Printf("%v: %v\n", n, a)  
    }  
}  

執行結果符合需求:
googleDNS: 8.8.8.8  
loopback: 127.0.0.1  


【參考資料】
1. Golang Language Specification - Methods Expression
2. Golang Language Specification - Interface Type
3. <The Way to Go - A Thorough Introduction to the Go Programming Language>一書第11.1節
4. Go Data Structures: Interfaces
5. Go Package fmt
6. A Tour of Go - Exercise: Stringers

===================== EOF ====================

沒有留言:

張貼留言