開放出版:許式偉《Go語言程式設計》樣章“物件導向程式設計”

暘谷發表於2012-03-23

以下為未編輯加工的作者原稿節選。

主要作者背景

許式偉 2009-2011年就職於盛大創新院,資深研究員,2008年就職百度,做頁面搜尋,2000-2008年就職金山軟體,曾任金山技術總監,WPS Office 2005首席架構師。目前獨立做創業公司,任上海七牛資訊科技有限公司 CEO。本書七牛資訊科技有限公司團隊的共同成果。

物件導向程式設計

Go 語言的物件導向程式設計(OOP)非常簡潔而優雅。說它簡潔,簡介之處在於,它沒有了OOP中很多概念,比如:繼承、虛擬函式、建構函式和解構函式、隱藏的this指標等等。說它優雅,是它的物件導向(OOP)是語言型別系統(type system)中的天然的一部分。整個型別系統通過介面(interface)串聯,渾然一體。

型別系統(type system)

很少有程式設計類的書籍談及型別系統(type system)這個話題。但實際上型別系統是整個語言的支撐,至關重要。

型別系統(type system)是指一個語言的型別體系圖。在整個型別體系圖中,包含這些內容:

 基本型別。如byte、int、bool、float等等。
 複合型別。如陣列(array)、結構體(struct)、指標(pointer)等。
 Any型別。即可以指向任意物件的型別。
 值語義和引用語義。
 物件導向。即所有具備物件導向特徵(比如有成員方法)的型別。
 介面(interface)。

型別系統(type system)描述的是這些內容在一個語言中如何被關聯。比如我們聊聊Java的型別系統: 在Java語言中,存在兩套完全獨立的型別系統,一套是值型別系統,主要是基本型別,如byte、int、boolean、char、double、String等,這些型別基於值語義。一套是以Object型別為根的物件型別系統,這些型別可以定義成員變數、成員方法、可以有虛擬函式。這些型別基於引用語義,只允許new出來(只允許在堆上)。只有物件型別系統中的例項可以被Any型別引用。Any型別就是整個物件型別系統的根 —— Object型別。值型別想要被Any型別引用,需要裝箱(Boxing)過程,比如int型別需要裝箱成為Integer型別。只有物件型別系統中的型別才可以實現介面(方法是讓該型別從要實現的介面繼承)。

在Go語言中,多數型別都是值語義,並且都可以有方法。在需要的時候,你可以給任何型別(包括內建型別)“增加”新方法。實現某個介面(interface)無需從該介面繼承(事實上Go語言並沒有繼承語法),而只需要實現該介面要求的所有方法。任何型別都可以被Any型別引用。Any型別就是空介面,亦即 interface{}。 讓我們一一道來。

給型別增加方法

在Go語言中,你可以給任意型別(包括內建型別,但指標型別除外)增加方法,例如:

type Integer int

func (a Integer) Less(b Integer) bool {
    return a < b
}

在這個例子中,我們定義了一個新型別Integer,它和int沒有本質不同,只是它為內建的int型別增加了個新方法:Less。如此,你就可以讓整型看起來像個類那樣用:

func main() {
    var a Integer = 1
    if a.Less(2) {
        fmt.Println(a, "Less 2")
    }
}

在學其他語言的時候,很多初學者對物件導向感到很神祕。我在給初學者介紹物件導向的時候,經常說到“物件導向只是一個語法糖”。以上程式碼用程式導向的方式來寫是這樣的:

type Integer int

func Integer_Less(a Integer, b Integer) bool {
return a < b
}

func main() {
var a Integer = 1
if Integer_Less(a, 2) {
    fmt.Println(a, "Less 2")
}
}

在Go語言中,物件導向的神祕面紗被剝得一乾二淨。對比這兩段程式碼:

    func (a Integer) Less(b Integer) bool {  // 物件導向
    return a < b
}

func Integer_Less(a Integer, b Integer) bool {  // 程式導向
    return a < b
}

a.Less(2)  // 物件導向
Integer_Less(a, 2)  // 程式導向

你可以看出,物件導向只是換了一種語法形式來表達。在Go語言中沒有隱藏的this指標。這句話的含義是:

第一,方法施加的目標(也就是“物件”)顯式傳遞,沒有被隱藏起來。
第二,方法施加的目標(也就是“物件”)不需要非得是指標,也不用非得叫this。

我們對比Java語言的程式碼:

class Integer {
    private int val;
    public boolean Less(Integer b) {
        return this.val < b.val;
    }
}

這段Java程式碼初學者會比較難懂,主要是因為Integer類的Less方法隱藏了第一個引數Integer* this。如果將其翻譯成C程式碼,會更清晰:

struct Integer {
    int val;
};

bool Integer_Less(Integer* this, Integer* b) {
    return this->val < b->val;
}

在Go語言中的物件導向最為直觀,也無需支付額外的成本。如果要求物件必須以指標傳遞,這有時會是個額外成本,因為物件有時很小(比如4個位元組),用指標傳遞並不划算。

只有在你需要修改物件的時候,才必須用指標。它不是Go語言的約束,而是一種自然約束。舉個例子:

func (a *Integer) Add(b Integer) {
    *a += b
}

這裡為Integer型別增加了Add方法。由於Add方法需要修改物件的值,所以需要用指標引用。呼叫如下:

func main() {
    var a Integer = 1
a.Add(2)
    fmt.Println("a =", a)
}

執行該程式得到的結果是:a = 3。如果你不用指標:

func (a Integer) Add(b Integer) {
    a += b
}

執行程式得到的結果是:a = 1,也就是維持原來的值。究其原因,是因為Go和C語言一樣,型別都是基於值傳遞。要想修改變數的值,只能傳遞指標。

值語義和引用語義

值語義和引用語義的差別在於賦值:

b = a
b.Modify()

如果b的修改不會影響a的值,那麼此型別屬於值型別。如果會影響a的值,那麼此型別是引用型別。

多數Go語言中的型別,包括:

 基本型別。如byte、int、bool、float32、float64、string等等。
 複合型別。如陣列(array)、結構體(struct)、指標(pointer)等。

都基於值語義。Go語言中型別的值語義表現得非常徹底。我們這麼說是因為陣列(array)。如果你學習過C語言,你會知道C語言中的陣列(array)比較特別。通過函式傳遞一個陣列的時候基於引用語義,但是在結構體中定義陣列變數的時候是值語義(表現在結構體賦值的時候,該陣列會被完整地拷貝一份新的副本)。

Go語言中的陣列(array)和基本型別沒有區別,是很純粹的值型別。例如:

var a = [3]int{1, 2, 3}
var b = a
b[1]++
fmt.Println(a, b)

程式執行結果:[1 2 3] [1 3 3]。這表明b = a賦值語句是陣列內容的完整拷貝。要想表達引用,需要用指標:

var a = [3]int{1, 2, 3}
var b = &a
b[1]++
fmt.Println(a, *b)

程式執行結果:[1 3 3] [1 3 3]。這表明b=&a賦值語句是陣列內容的引用。變數b的型別不是[3]int,而是*[3]int型別。

Go語言中有4個型別比較特別,看起來像引用型別:

 切片(slice):指向陣列(array)的一個區間。
 字典(map):極其常見的資料結構,提供key-value查詢能力。
 通道(chan):執行體(goroutine)間通訊設施。
 介面(interface):對一組滿足某個契約的型別的抽象。

但是這並不影響我們將Go語言型別是值語義的本質。我們一個個來看這些型別:

切片(slice)本質上是range,你可以大致將 []T 表示為:

type slice struct {
    first *T
    last *T
    end *T
}

因為切片(slice)內部是一系列的指標,所以可以改變所指向的陣列(array)的元素並不奇怪。slice型別本身的賦值仍然是值語義。

字典(map)本質上是一個字典指標,你可以大致將map[K]V表示為:

type Map_K_V struct {
    ...
}

type map[K]V struct {
    impl *Map_K_V
}

基於指標(pointer),我們完全可以自定義一個引用型別,如:

type IntegerRef struct { impl *int }

通道(chan)和字典(map)類似,本質上是一個指標。為什麼將他們設計為是引用型別而不是統一的值型別,是因為完整拷貝一個通道(chan)或字典(map)並是常規需求。

同樣,介面(interface)具備引用語義,是因為內部維持了兩個指標。示意為:

type interface struct {
    data *void
    itab *Itab
}

介面在Go語言中的地位非常重要。關於介面(interface)內部實現細節,後面在高階話題中,我們再細細剖析。

結構體(struct)

Go語言的結構體(struct)和其它語言的類(class)有同等的地位。但Go語言放棄了包括繼承在內的大量OOP特性,只保留了組合(compose)這個最基礎的特性。

組合(compose)甚至不能算OOP的特性。因為連C語言這樣的程式式程式設計語言中,也有結構體(struct),也有組合(compose)。組合只是形成複合型別的基礎。

上面我們說到,所有的Go語言的型別(指標型別除外)都是可以有自己的方法。在這個背景下,Go語言的結構體(struct)它只是很普通的複合型別,平淡無奇。例如我們要定義一個矩形型別:

type Rect struct {
    x, y float64
    width, height float64
}

然後我們定義方法Area來計算矩形的面積:

func (r *Rect) Area() float64 {
return r.width * r.height
}

初始化

定義了Rect型別後,我們如何建立並初始化Rect型別的物件例項?有如下方法:

rect1 := new(Rect)
rect2 := &Rect{}
rect3 := &Rect{0, 0, 100, 200}
rect4 := &Rect{width: 100, height: 200}

在Go語言中,未顯式進行初始化的變數,都會初始化為該型別的零值(例如對於bool型別的零值為false,對於int型別零值為0,對於string型別零值為空字串)。

建構函式?不需要。在Go語言中你只需要定義一個普通的函式,只是通常以NewXXX來命名,表示“建構函式”:

func NewRect(x, y, width, height float64) *Rect {
    return &Rect{x, y, width, height}
}

這一切非常自然,沒有任何突兀之處。

匿名組合

確切地說,Go語言也提供了繼承,但是採用了組合的文法,我們稱之為匿名組合:

type Base struct {
    ...
}

func (base *Base) Foo() { ... }
func (base *Base) Bar() { ... }

type Foo struct {
    Base
    ...
}

func (foo *Foo) Bar() {
    foo.Base.Bar()
    ...
}

以上程式碼定義了一個Base類(實現了Foo、Bar兩個成員方法),然後定義了一個Foo類,從 Base“繼承”並實現了改寫了Bar方法,該方法實現時先呼叫了基類的Bar方法。

在“派生類”Foo沒有改寫“基類”Base的成員方法時,相應的方法就被“繼承”。例如在上面的例子中,呼叫foo.Foo() 和呼叫foo.Base.Foo() 效果一致。

區別於其他語言,Go語言很清晰地告訴你類的記憶體佈局是怎麼樣的。在Go語言中你還可以隨心所欲地修改記憶體佈局,如:

type Foo struct {
...
    Base
}

這段程式碼從語義上來說,和上面給例子並無不同,但記憶體佈局發生了改變。“基類”Base的資料被放在了“派生類”Foo 的最後。

另外,在Go語言中你還可以以指標方式從一個類“派生”:

type Foo struct {
    *Base
    ...
}

這段Go程式碼仍然有“派生”的效果,只是Foo建立例項的時候,需要外部提供一個Base類例項的指標。C++ 中其實也有類似的功能,那就是虛基類。但是虛基類是非常讓人難以理解的特性,普遍上來說 C++ 的開發者都會遺忘這個特性。

成員的可訪問性

Go語言對關鍵字的增加非常吝嗇。在Go語言中沒有private、protected、public這樣的關鍵字。要想某個符號可被其他包(package)訪問,需要將該符號定義為大寫字母開頭。如:

type Rect struct {
    X, Y float64
    Width, Height float64
}

這樣,Rect型別的成員變數就全部被public了。成員方法遵循同樣的規則,例如:

func (r *Rect) area() float64 {
    return r.Width * r.Height
}

這樣,Rect的area方法只能在該型別所在的包(package)內使用。

需要強調的一點是,Go語言中符號的可訪問性是包(package)一級的,而不是類一級的。儘管area是Rect的內部方法,但是在同一個包中的其他型別可以訪問到它。這樣的可訪問性控制很粗曠,很特別,但是非常實用。如果Go語言符號的可訪問性是類一級的,少不了還要加上friend這樣的關鍵字,以表示兩個類是朋友關係,可以訪問其中的私有成員。

介面(interface)

Rob Pike曾經說,如果只能選擇一個Go語言的特性移植到其他語言中,他會選擇介面。

介面(interface)在Go語言有著至關重要的地位。如果說goroutine和channel 是支撐起Go語言的併發模型的基石,讓Go語言在如今叢集化與多核化的時代,成為一道極為亮麗的風景;那麼介面(interface)是Go語言整個型別系統(type system)的基石,讓Go語言在基礎程式設計哲學的探索上,達到史無先例的高度。

我曾在多個場合說,Go語言在程式設計哲學上是變革派,而不是改良派。這不是因為Go語言有 goroutine和channel,而更重要的是因為Go語言的型別系統,因為Go語言的介面。因為有介面,才讓Go語言的程式設計哲學變得完美。

Go 語言的介面(interface)不單單只是介面。

為什麼這麼說?讓我們細細道來。

其他語言(C++/Java/C#)的介面

Go語言的介面,並不是你之前在其他語言(C++/Java/C#等)中接觸到的介面。

在Go語言之前的介面(interface),主要作為不同元件之間的契約存在。對契約的實現是強制的,你必須宣告你的確實現了該介面。為了實現一個介面,你需要從該介面繼承:

interface IFoo {
    void Bar();
}

class Foo implements IFoo { // Java 文法
    ...
}

class Foo : public IFoo { // C++ 文法
...
}

IFoo* foo = new Foo;

哪怕另外存在一個一模一樣的介面,只是名字不同叫IFoo2(名字一樣但是在不同的名字空間下,也是名字不同),上面的類Foo只實現了IFoo,但沒有實現IFoo2。

這類介面(interface),我們稱之為侵入式的介面。“侵入式”的主要表現在於實現類需要明確宣告自己實現了某個介面。

這種強制性的介面繼承,是物件導向程式設計(OOP)思想發展過程中的一個重大失誤。我之所以這樣講,是因為它從根本上是違背事物的因果關係的。

讓我們從契約的形成過程談起。設想我們現在要實現一個簡單搜尋引擎(SE)。該搜尋引擎需要依賴兩個模組,一個是雜湊表(HT),一個是HTML分析器(HtmlParser)。

搜尋引擎的實現者認為,SE對雜湊表(HT)的依賴是確定性的,所以他不併認為需要在SE和HT之間定義介面,而是直接import(或者include)的方式使用了HT;而模組SE對HtmlParser的依賴是不確定的,未來可能需要有WordParser、PdfParser等模組來替代HtmlParser,以達到不同的業務要求。為此,他定義了SE和HtmlParser之間的介面,在模組SE中通過介面呼叫方式間接引用模組HtmlParser。

應當注意到,介面(interface)的需求方是搜尋引擎(SE)。只有SE才知道介面應該定義成什麼樣子才比更為合理。但是介面的實現方是HtmlParser。基於模組設計的單向依賴原則,模組HtmlParser實現自身的業務時,不應該關心某個具體使用方的要求。HtmlParser在實現的時候,甚至還不知道未來有一天SE會用上它。 要求模組HtmlParser知道所有它的需求方的需要的介面,並提前宣告實現了這些介面是不合理的。同樣的道理髮生在搜尋引擎(SE)自己身上。SE並不能夠預計未來會有哪些需求方需要用到自己,並且實現他們所要求的介面。

這個問題在標準庫的提供來說,變得更加突出。比如我們實現了File類(這裡我們用Go語言的文法來描述要實現的方法,請忽略文法上的細節),它有這些方法:

Read(buf []byte) (n int, err error)
Write(buf []byte) (n int, err error)
Seek(off int64, whence int) (pos int64, err error)
Close() error

那麼,到底是應該定義一個IFile介面,還是應該定義一系列的IReader, IWriter, ISeeker, ICloser介面,然後讓File從他們繼承好呢?脫離了實際的使用者場景,討論這兩個設計哪個更好並無意義。問題在於,實現File類的時候,我怎麼知道外部會如何用它呢?

正因為這種不合理的設計,使得Java、C# 的類庫每個類實現的時候都需要糾結:

 問題1:我提供哪些介面好呢?
 問題2:如果兩個類實現了相同的介面,應該把介面放到哪個包好呢?

非侵入式介面

在Go語言中,一個類只需要實現了介面要求的所有函式,那麼我們就說這個類實現了該介面。例如:

type File struct {
    ...
}

func (f *File) Read(buf []byte) (n int, err error)
func (f *File) Write(buf []byte) (n int, err error)
func (f *File) Seek(off int64, whence int) (pos int64, err error)
func (f *File) Close() error

這裡我們定義了一個File類,並實現有Read,Write,Seek,Close等方法。設想我們有如下介面:

type IFile interface {
    Read(buf []byte) (n int, err error)
    Write(buf []byte) (n int, err error)
    Seek(off int64, whence int) (pos int64, err error)
    Close() error
}

type IReader interface {
    Read(buf []byte) (n int, err error)
}

type IWriter interface {
    Write(buf []byte) (n int, err error)
}

type ICloser interface {
    Close() error
}

儘管File類並沒有從這些介面繼承,甚至可以不知道這些介面的存在,但是File類實現了這些介面,可以進行賦值:

var file1 IFile = new(File)
var file2 IReader = new(File)
var file3 IWriter = new(File)
var file4 ICloser = new(File)

Go語言的非侵入式介面,看似只是做了很小的文法調整,但實則影響深遠。

其一,Go語言的標準庫,再也不需要繪製類庫的繼承樹圖。你一定見過不少C++、Java、C# 類庫的繼承樹圖。這裡給個Java繼承樹圖:

http://docs.oracle.com/javase/1.4.2/docs/api/overview-tree.html

在Go中,類的繼承樹並無意義。你只需要知道這個類實現了哪些方法,每個方法是啥含義就足夠了。

其二,實現類的時候,只需要關心自己應該提供哪些方法。不用再糾結介面需要拆得多細才合理。介面是由使用方按需定義,而不用事前規劃。

其三,不用為了實現一個介面而import一個包,目的僅僅是引用其中的某個interface的定義,這是不被推薦的。因為多引用一個外部的package,就意味著更多的耦合。介面由使用方按自身需求來定義,使用方無需關心是否有其他模組定義過類似的介面。

介面賦值

介面(interface)的賦值在Go語言中分為如下2種情況討論:

 將物件例項賦值給介面
 將介面賦值給另一個介面

先討論將某種型別的物件例項賦值給介面。這要求該物件例項實現了介面要求的所有方法。例如,在之前我們有實作過一個Integer型別,如下:

type Integer int

func (a Integer) Less(b Integer) bool {
    return a < b
}

func (a *Integer) Add(b Integer) {
    *a += b
}

相應地,我們定義介面LessAdder,如下:

type LessAdder interface {
    Less(b Integer) bool
    Add(b Integer)
}

現在有個問題:假設我們定義一個Integer型別的物件例項,怎麼其賦值給LessAdder介面呢?應該用下面的語句(1),還是語句(2)呢?

var a Integer = 1
var b LessAdder = &a     ... (1)
var b LessAdder = a      ... (2)

答案是應該用語句(1)。原因在於,Go語言可以根據

func (a Integer) Less(b Integer) bool

這個函式自動生成一個新的Less方法:

func (a *Integer) Less(b Integer) bool {
    return (*a).Less(b)
}

這樣,型別 *Integer就既存在Less方法,也存在Add方法,滿足LessAdder介面。而從另一方面來說,根據

func (a *Integer) Add(b Integer)

這個函式無法自動生成

func (a Integer) Add(b Integer) {
    (&a).Add(b)
}

因為 (&a).Add改變的只是函式引數a,對外部實際要操作的物件並無影響,這不符合使用者的預期。故此,Go語言不會自動為其生成該函式。因此,型別Integer只存在Less方法,缺少Add方法,不滿足LessAdder介面,故此上面的語句(2)不能賦值。

為了進一步證明以上的推理,我們不妨再定義一個Lesser介面,如下:

type Lesser interface {
    Less(b Integer) bool
}

然後我們定義一個Integer型別的物件例項,將其賦值給Lesser介面:

var a Integer = 1
var b1 Lesser = &a     ... (1)
var b2 Lesser = a      ... (2)

正如如我們所料的那樣,語句(1)和語句(2)均可以編譯通過。

我們再來討論另一種情形:將介面賦值給另一個介面。在Go語言中,只要兩個介面擁有相同的方法列表(次序不同不要緊),那麼他們就是等同的,可以相互賦值。例如:

package one

type ReadWriter interface {
    Read(buf []byte) (n int, err error)
    Write(buf []byte) (n int, err error)
}

package two

type IStream interface {
    Write(buf []byte) (n int, err error)
    Read(buf []byte) (n int, err error)
}

這裡我們定義了兩個介面,一個叫 one.ReadWriter,一個叫 two.IStream。兩者都定義了Read、Write方法,只是定義的次序相反。one.ReadWriter先定義了Read再定義Write,而two.IStream反之。

在Go語言中,這兩個介面實際上並無區別。因為:

 任何實現了one.ReadWriter介面的類,均實現了two.IStream。
 任何one.ReadWriter介面物件可賦值給two.IStream,反之亦然。
 在任何地方使用one.ReadWriter介面,和使用two.IStream並無差異。

以下這些程式碼可編譯通過:

var file1 two.IStream = new(File)
var file2 one.ReadWriter = file1
var file3 two.IStream = file2

介面賦並不要求兩個介面必須等價。如果介面A方法列表是介面B方法列表的子集,那麼介面B可以賦值給介面A。例如假設我們有Writer介面:

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

我們可以將上面的one.ReadWriter、two.IStream介面的例項賦值給Writer介面:

var file1 two.IStream = new(File)
var file4 Writer = file1

但是反過來並不成立:

var file1 Writer = new(File)
var file5 two.IStream = file1 // 編譯不能通過!

這段程式碼無法編譯通過。原因是顯然的:file1並沒有Read方法。

介面查詢

有辦法讓上面Writer介面轉換為two.IStream介面麼?有。那就是我們即將討論的介面查詢語法。程式碼如下:

var file1 Writer = ...
if file5, ok := file1.(two.IStream); ok {
    ...
}

這個if語句的含義是:file1介面指向的物件例項是否實現了two.IStream介面呢?如果實現了,則... 介面查詢是否成功,要在執行期才能夠確定。它不像介面賦值,編譯器只需要通過靜態型別檢查即可判斷賦值是否可行。

在Windows下做過開發的人,通常都接觸過COM,知道COM也有一個介面查詢(QueryInterface)。是的,Go語言的介面查詢和COM的介面查詢(QueryInterface)非常類似,都可以通過物件(元件)的某個介面來查詢物件實現的其他介面。當然Go語言的介面查詢優雅很多。在Go語言中,物件是否滿足某個介面、通過某個介面查詢其他介面,這一切都是完全自動完成的。

讓語言內建介面查詢,這是一件非常了不起的事情。在COM中實現QueryInterface的過程非常繁複,但QueryInterface是COM體系的根本。COM書籍對QueryInterface的介紹,往往從類似下面這樣一段問話開始,它在Go語言中同樣適用:

\> 你會飛嗎?    // IFly
\> 不會。
> 你會游泳嗎?    // ISwim
> 會。
> 你會叫麼?    // IShout
> 會。
> ...

隨著問題深入,你從開始對物件(元件)一無所知(在Go語言中是interface{},在COM中是IUnknown),到逐步有了深入的瞭解。

但是你最終能夠完全瞭解物件麼?COM說不能,你只能無限逼近,但永遠不能完全瞭解一個元件。Go語言說:你能。

在Go語言中,你可以向介面詢問,它指向的物件是否是某個型別,例子如下:

var file1 Writer = ...
if file6, ok := file1.(*File); ok {
    ...
}

這個if語句的含義是:file1介面指向的物件例項是否是 *File 型別呢?如果是的,則...

你可以認為查詢介面所指向的物件是否是某個型別,只是介面查詢的一個特例。介面是對一組型別的公共特性的抽象。所以查詢介面與查詢具體型別的區別,好比是下面這兩句問話的區別:

> 你是醫生嗎?
> 是。
> 你是某某某?
> 是。

第一句問話查的是一個群體,是查詢介面;而第二句問話已經到了具體的個體,是查詢具體型別。

在C++/Java/C# 等語言中,也有一些類似的動態查詢能力,比如查詢一個物件的型別是否是繼承自某個型別(基類查詢),或者是否實現了某個介面(介面派生查詢)。但是他們的動態查詢與Go的動態查詢很不一樣。

> 你是醫生嗎?

對於這個問題,基類查詢看起來像是在這麼問:“你老爸是醫生嗎?”;介面派生查詢則看起來像是這麼問:“你有醫師執照嗎?”;在Go語言中,則是先確定滿足什麼樣的條件才是醫生,比如技能要求有哪些,然後才是按條件一一拷問,確認是否滿足條件,只要滿足了你就是醫生,不關心你是否有醫師執照,或者是小國執照不被天朝承認。

型別查詢

在Go語言中,你還可以更加直接了當地詢問介面指向的物件例項的型別。例如:

var v1 interface{} = ...
switch v := v1.(type) {
case int: // 現在v的型別是int
case string: // 現在v的型別是string
...
}

就像現實生活中物種多得數不清一樣,語言中的型別也多的數不清。所以型別查詢並不經常被使用。它更多看起來是個補充,需要配合介面查詢使用。例如:

type Stringer interface {
    String() string
}

func Println(args ...interface{}) {
    for _, arg := range args { 
    switch v := v1.(type) {
    case int: // 現在v的型別是int
    case string: // 現在v的型別是string
default:
        if v, ok := arg.(Stringer); ok { // 現在v的型別是Stringer
            val := v.String()
            ...
        } else {
            ...
        }
    }
}

Go語言標準庫的Println當然比這個例子要複雜很多。我們這裡摘取其中的關鍵部分進行分析。對於內建型別,Println採用窮舉法來,針對每個型別分別轉換為字串進行列印。對於更一般的情況,首先確定該型別是否實現了String()方法,如果實現了則用String()方法轉換為字串進行列印。否則,Println利用反射(reflect)遍歷物件的所有成員變數進行列印。

是的,利用反射(reflect)也可以進行型別查詢,詳細可參閱reflect.TypeOf方法相關文件。在後文高階話題中我們也會探討有關“反射(reflect)”的話題。

Any型別

由於Go語言中任何物件例項都滿足空介面interface{},故此interface{}看起來像是可以指向任何物件的Any型別。如下:

var v1 interface{} = 1      // 將int型別賦值給interface{}
var v2 interface{} = "abc"    // 將string型別賦值給interface{}
var v3 interface{} = &v2    // 將*interface{}型別賦值給interface{}
var v4 interface{} = struct{ X int }{1}
var v5 interface{} = &struct{ X int }{1}

當一個函式可以接受任意的物件例項時,我們會將其宣告為interface{}。最典型的例子是標準庫fmt中PrintXXX系列的函式。例如:

func Printf(fmt string, args ...interface{})
func Println(args ...interface{})
...

前面我們已經簡單分析過Println的實現,也已經展示過interface{}的用法。總結來說,interface{} 類似於COM中的IUnknown,我們剛開始對其一無所知,但我們可以通過介面查詢和型別查詢逐步瞭解它。

總結

我們說,Go 語言的介面(interface)不單單只是介面。在其他語言中,介面僅僅作為元件間的契約存在。從這個層面講,Go語言介面的重要突破是,其介面是非侵入式的,把其他語言介面的副作用消除了。

但是Go語言的介面不僅僅是契約作用。它是Go語言型別系統(type system)的綱。這表現在:

 介面查詢:通過介面你可以查詢介面所指向的物件是否實現了另外的介面。
 型別查詢:通過介面你可以查詢介面所指向的物件的具體型別。
 Any型別:在Go語言中interface{}可指向任意的物件例項。

相關文章