《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語言中,你可以給任意型別(包括內建型別,但指標型別除外)增加方法,例如:
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。其他高階語言中呼叫方法的時候編譯器會通過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
len int
cap int
}
因為陣列切片內部是指向陣列的指標,所以可以改變所指向的陣列元素並不奇怪。陣列切片型別本身的賦值仍然是值語義。字典(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 //unsafe.Pointer 實際物件指標
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和C區別
}
這一切非常自然,開發者也不需要分析在使用了 new 之後到底背後發生了多少事情。在Go語言中,一切要發生的事情都直接可以看到。
匿名組合
確切地說,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)
介面(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)思想發展過程中的一個重大失誤。我之所以這樣講,是因為它從根本上是違背事物的因果關係的。
這個問題在標準庫的提供來說,變得更加突出。比如我們實現了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# 類庫的繼承樹圖。在Go中,類的繼承樹並無意義。你只需要知道這個類實現了哪些方法,每個方法是啥含義就足夠了。
其二,實現類的時候,只需要關心自己應該提供哪些方法。不用再糾結介面需要拆得多細才合理。介面是由使用方按需定義,而不用事前規劃。
其三,不用為了實現一個介面而import一個包,目的僅僅是引用其中的某個interface的定義,這是不被推薦的。因為多引用一個外部的package,就意味著更多的耦合。介面由使用方按自身需求來定義,使用方無需關心是否有其他模組定義過類似的介面。介面賦值
介面(interface)的賦值在Go語言中分為如下2種情況討論:
- 將物件例項賦值給介面
- 將介面賦值給另一個介面
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介面呢?如果實現了,則... 介面查詢是否成功,要在執行期才能夠確定。它不像介面賦值,編譯器只需要通過靜態型別檢查即可判斷賦值是否可行。
在Go語言中,你可以向介面詢問,它指向的物件是否是某個型別,例子如下:
var file1 Writer = ...
if file6, ok := file1.(*File); ok {
...
}
這個if語句的含義是:file1介面指向的物件例項是否是 *File 型別呢?如果是的,則...
你可以認為查詢介面所指向的物件是否是某個型別,只是介面查詢的一個特例。介面是對一組型別的公共特性的抽象。所以查詢介面與查詢具體型別的區別,好比是下面這兩句問話的區別:> 你是醫生嗎?
> 是。
> 你是某某某?
> 是。
第一句問話查的是一個群體,是查詢介面;而第二句問話已經到了具體的個體,是查詢具體型別。在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)”的話題。
介面組合
像之前介紹的型別組合一樣,Go語言同樣支援介面組合。我們已經介紹過Go語言包中 io.Reader介面和 io.Writer 介面,接下來我們再介紹同樣來自於 io 包的另一個介面 io.ReadWriter :
// ReadWriter 介面將基本的 Read 和 Write 方法組合起來
type ReadWriter interface {
Reader
Writer
}
這個介面組合了 Reader 和 Writer 兩個介面,它完全等同於如下寫法:
type ReadWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
因為這兩種寫法的表意完全相同: ReadWriter 介面既能做 Reader 介面的所有事情,又能做Writer 介面的所有事情。在Go語言包中,還有眾多類似的組合介面,比如 ReadWriteCloser 、ReadWriteSeeker 、 ReadSeeker 和 WriteCloser 等。 可以認為介面組合是型別匿名組合的一個特定場景,只不過介面只包含方法,而不包含任何成員變數。
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語言程式設計》