歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

基於類型系統的面向對象編程語言Go

面向對象編程

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 文法
    ...
}

```C++
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{}可指向任意的對象實例。
Copyright © Linux教程網 All Rights Reserved