許式偉《Go語言編程》樣章“物件導向編程”

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

原文轉自:http://www.ituring.com.cn/article/1339

物件導向編程

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{}可指向任意的對象執行個體。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.