開放出版:許式偉《Go語言程式設計》樣章“物件導向程式設計”
以下為未編輯加工的作者原稿節選。
主要作者背景
許式偉 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{}可指向任意的物件例項。
相關文章
- 許式偉《Go語言程式設計》章節摘錄:Go語言簡史Go程式設計
- 開放出版:為什麼我們需要一門新語言?許式偉《Go語言程式設計》序Go程式設計
- go語言程式設計前景怎麼樣?國內Go語言佈道師許式偉這樣說Go程式設計
- 函數語言程式設計 VS 物件導向程式設計函數程式設計物件
- 聊聊 Go 語言中的物件導向程式設計Go物件程式設計
- 物件導向程式設計-java語言 第二週程式設計題物件程式設計Java
- Facebook 開源 Skip 物件導向+函數語言程式設計語言物件函數程式設計
- 函數語言程式設計 vs 物件導向程式設計 vs 程式式程式設計的JS演示比較 - DEV函數程式設計物件JSdev
- OOC 物件導向 C 語言程式設計實踐物件程式設計
- 物件導向 vs. 函數語言程式設計物件函數程式設計
- SOLID Go Design - Go語言物件導向設計SolidGo物件
- SOLID Go Design - Go 語言物件導向設計SolidGo物件
- Go語言結構體(struct)物件導向程式設計基礎篇Go結構體Struct物件程式設計
- 物件導向程式設計物件程式設計
- 物件導向變成 VS 函數語言程式設計物件函數程式設計
- iOS 開發之 OOA (物件導向分析) & OOD (物件導向設計)& OOP (物件導向程式設計)iOS物件OOP程式設計
- [.net 物件導向程式設計基礎] (2) 關於物件導向程式設計物件程式設計
- Go物件導向程式設計OOP的實現Go物件程式設計OOP
- 十三、物件導向程式設計物件程式設計
- js物件導向程式設計JS物件程式設計
- 程式設計思想 物件導向程式設計物件
- 十六、物件導向程式設計物件程式設計
- perl 物件導向程式設計物件程式設計
- LotusScript物件導向程式設計物件程式設計
- Javascript 物件導向程式設計JavaScript物件程式設計
- Matlab AppDesigner程式設計教程第1章——物件導向程式設計MatlabAPP程式設計物件
- 為什麼說物件導向程式設計和函數語言程式設計都有問題物件程式設計函數
- 物件導向程式設計和`GP`泛型程式設計物件程式設計泛型
- JavaScript設計模式之物件導向程式設計JavaScript設計模式物件程式設計
- JavaScript-設計模式-物件導向程式設計JavaScript設計模式物件程式設計
- JS物件導向程式設計(一):物件JS物件程式設計
- 真的可以,用C語言實現物件導向程式設計OOPC語言物件程式設計OOP
- JavaScript建構函式,物件導向程式設計JavaScript函式物件程式設計
- 物件導向的程式設計和函數語言程式設計基本相同的證據 - vavr物件程式設計函數VR
- Scala的物件導向程式設計物件程式設計
- JavaScript物件導向程式設計理解!JavaScript物件程式設計
- Python物件導向程式設計Python物件程式設計
- 物件導向程式設計C++物件程式設計C++