(整理自網絡)html
Go語言的面向對象編程(OOP)很是簡潔而優雅。說它簡潔,在於它沒有了OOP中不少概念,好比:繼承、虛函數、構造函數和析構函數、隱藏的this指針等等。說它優雅,是它的面向對象(OOP)是語言類型系統(type system)中的自然的一部分。整個類型系統經過接口(interface)串聯,渾然一體。java
不多有編程類的書籍談及類型系統(type system)這個話題。但實際上類型系統是整個語言的支撐,相當重要。c++
類型系統(type system)是指一個語言的類型體系圖。在整個類型體系圖中,包含這些內容:編程
類型系統(type system)描述的是這些內容在一個語言中如何被關聯。好比咱們聊聊Java的類型系統:在Java語言中,存在兩套徹底獨立的類型系統,一套是值類型系統,主要是基本類型,如byte、int、boolean、char、double、String等,這些類型基於值語義。一套是以Object類型爲根的對象類型系統,這些類型能夠定義成員變量、成員方法、能夠有虛函數。這些類型基於引用語義,只容許new出來(只容許在堆上)。只有對象類型系統中的實例能夠被Any類型引用。Any類型就是整個對象類型系統的根 —— Object類型。值類型想要被Any類型引用,須要裝箱(Boxing)過程,好比int類型須要裝箱成爲Integer類型。只有對象類型系統中的類型才能夠實現接口(方法是讓該類型從要實現的接口繼承)。api
在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語言中,面向對象的神祕面紗被剝得一乾二淨。對比這兩段代碼:oracle
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語言中的類型,包括:
都基於值語義。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個類型比較特別,看起來像引用類型:
可是這並不影響咱們將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)內部實現細節,後面在高階話題中,咱們再細細剖析。
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這樣的關鍵字,以表示兩個類是朋友關係,能夠訪問其中的私有成員。
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# 的類庫每一個類實現的時候都須要糾結:
在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語言中,這兩個接口實際上並沒有區別。由於:
如下這些代碼可編譯經過:
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)」的話題。
因爲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)的綱。這表如今: