<!-- 爲何我不喜歡Go語言式的接口(評) -->html
最近在Go語言的QQ羣裏看到關於圖靈社區有牛人老趙吐槽許式偉《Go語言編程》的各類爭論.java
我以前也看了老趙吐槽許式偉《Go語言編程》的文章, 當時想老趙若是能將許大書中不足部分補充完善了也是一個好事情. 所以, 對老趙的後續文章甚是期待.算法
誰知道看了老趙以後的兩篇吐槽Go語言的文章, 發現徹底不是那回事情, 吐槽內容誤差太遠. 原本沒想摻和進來, 可是看到QQ羣裏和圖靈社區有不少人甚至把老趙的文章看成真理同樣. 實在忍不住, 昨天註冊了賬號, 進來也說下個人觀點.編程
這是老趙的幾篇文章:api
本文在圖靈社區的網址:數組
補充說明:安全
由於當前這篇文章主要是針對老趙的不喜歡Go語言式的接口作 評論. 由於標題的緣由, 也形成了很大的爭議性(由於不少人說我理解的不少觀點和老趙的原文不相符).數據結構
後面我會對Go語言的一些特性一些簡單的介紹, 可是不會是如今這種方式.閉包
所謂Go語言式的接口,就是不用顯示聲明類型T實現了接口I,只要類型T的公開方法徹底知足接口I的要求,就能夠把類型T的對象用在須要接口I的地方。這種作法的學名叫作Structural Typing,有人也把它看做是一種靜態的Duck Typing。除了Go的接口之外,相似的東西也有好比Scala裏的Traits等等。有人以爲這個特性很好,但我我的並不喜歡這種作法,因此在這裏談談它的缺點。固然這跟動態語言靜態語言的討論相似,不能簡單粗暴的下一個「好」或「很差」的結論。oracle
原文觀點:<br>
個人觀點:<br>
interface
)各自的優點.那麼就從頭談起:什麼是接口。其實通俗的講,接口就是一個協議,規定了一組成員,例如.NET裏的
ICollection
接口:
public interface ICollection { int Count { get; } object SyncRoot { get; } bool IsSynchronized { get; } void CopyTo(Array array, int index); }
這就是一個協議的所有了嗎?事實並不是如此,其實接口還規定了每一個行爲的「特徵」。打個比方,這個接口的
Count
除了須要返回集合內元素的數目之外,還隱含了它須要在O(1)時間內返回這個要求。這樣一個使用了ICollection
接口的方法才能放心地使用Count
屬性來獲取集合大小,才能在知道這些特徵的狀況下選用正確的算法來編寫程序,而不用擔憂帶來性能問題,這才能實現所謂的「面向接口編程」。固然這種「特徵」並不但指「性能」上的,例如Count
還包含了例如「不修改集合內容」這種看似十分天然的隱藏要求,這都是ICollection
協議的一部分。
原文觀點:<br>
個人觀點:<br>
const
修飾, 除了接口外的method也不支持(Go的const
關鍵字是另外一個語義).可是, C++中有了const
就真的安全了嗎?
class Foo { private: mutable Mutex mutex_; public: void doSomething()const { MutexLocker locker(&mutex_); // const 已經被繞過了 } };
C++中方法const
修飾惟一的用處就是增長各類編譯麻煩, 對使用者沒法做出任何承諾. 使用者更關心的是doSomething
的要作什麼, 上面的方法其實和void doSomethingConst()
要表達的是相似的意思.
無論是靜態庫仍是動態庫, 哪一個能從庫一級保證某個函數是不能幹什麼的? 若是C++的const
關鍵字並不能 真正的保證const
, 而相似的實現細節(也包括前面提到的和時間複雜度相關的性能特徵)必須有文檔來補充. 那文檔應該以什麼形式提供(代碼註釋?Word文檔?其餘格式文檔?)? 這些文檔真多能保證每一個都會有人看嗎? 文檔說到底還只是人直接的口頭約定, 若是文檔真的那麼好使(還有實現), 那麼彙編語言也能夠解決一切問題.
那在Go語言是如何解決const
和性能問題?
首先, 對於C語言的函數參數傳值的語義, const
是必然的結果. 可是, 若是參數太大要考慮性能的話, 就會考慮傳指針(仍是傳值的語義), 經過傳指針就不能保證const
的語義了. 若是連使用的庫函數都不能相信, 那怎麼就能相信它對於的頭文件所提供的const
信息呢?
由於, const
和性能是相互矛盾的. Go語言中若是想絕對安全, 那就傳值. 若是想要性能(或者是返回反作用), 那就傳指針:
type Foo int // 要性能 func (self *Foo)Get() int { return *self } // 要安全 func (self Foo)GetConst() int { return self }
Go語言怎麼對待性能問題(還有單元測試問題)? 答案是集成go test
測試工具. 在Go語言中測試代碼是pkg(包含package main
)的一個組成部分. 不只是普通的pkg能夠go test
, package main
也能夠用go test
進行測試.
咱們給前面的代碼加上單元測試和性能測試.
// foo_test.go func TestGet(t *testing.T) { var foo Foo = 0 if v := foo.Get(); v != 0 { t.Errorf("Bad Get. Need=%v, Got=%v", 0, v) } } func TestGetConst(t *testing.T) { var foo Foo = 0 if v := foo.GetConst(); v != 0 { t.Errorf("Bad GetConst. Need=%v, Got=%v", 0, v) } } func BenchmarkGet(b *testing.B) { var foo Foo = 0 for i := 0; i < b.N; i++ { _ = foo.Get() } } func BenchmarkGetConst(b *testing.B) { var foo Foo = 0 for i := 0; i < b.N; i++ { _ = foo.GetConst() } }
固然, 最終的測試結果仍是給人來看的. 若是實現者/使用者故意搞破壞, 再好的工具也是沒辦法的.
由此咱們還能夠解釋另一些問題,例如爲何.NET裏的List<T>不叫作ArrayList<T>,固然這些都只是個人推測。個人想法是,因爲List<T>與IList<T>接口是配套出現的,而像IList<T>的某些方法,例如索引器要求可以快速獲取元素,這樣使用IList<T>接口的方法才能放心地使用下標進行訪問,而知足這種特徵的數據結構就基本與數組難以割捨了,因而名字裏的Array就顯得有些多餘。
假如List<T>更名爲ArrayList<T>,那麼彷佛就暗示着IList<T>能夠有其餘實現,難道是LinkedList<T>嗎?事實上,LinkedList<T>根本與IList<T>沒有任何關係,由於它的特徵和List<T>相差太多,它有的滿是些AddFirst、InsertBefore方法等等。固然,LinkedList<T>與List<T>都是ICollection<T>,因此咱們能夠放心地使用其中一小部分紅員,它們的行爲特徵是明確的。
原文觀點:<br>
IList<T>
接口配套出現的緣由, 纔沒有將List<T>
命名爲ArrayList<T>
.IList<T>
(這個應該是筆誤, 我以爲做者是說List<T>
)索引器要求可以快速獲取元素, 這樣使用IList<T>接口的方法才能放心地使用下標進行訪問(實現的算法複雜度特徵向接口方向傳遞了).List<T>
改成ArrayList<T>
的另外一個緣由是LinkedList<T>
. 由於List<T>
和LinkedList<T>
的時間複雜度不同, 因此不能是一個接口(大概是一個算法複雜度一個接口的意思?).LinkedList<T>
與List<T>
都屬於ICollection<T>
這個祖宗接口.個人觀點:<br>
Xxx<T>
對應一個IXxx<T>
接口)?這方面的反面案例之一即是Java了。在Java類庫中,ArrayList和LinkedList都實現了List接口,它們都有get方法,傳入一個下標,返回那個位置的元素,可是這兩種實現中前者耗時O(1)後者耗時O(N),二者大相近庭。那麼好,我如今要實現一個方法,它要求從第一個元素開始,返回每隔P個位置的元素,咱們還能面向List接口編程麼?假如咱們依賴下標訪問,則外部一不當心傳入LinkedList的時候,算法的時間複雜度就從指望的O(N/P)變成了O(N2/P)。假如咱們選擇遍歷整個列表,則即使是ArrayList咱們也只能獲得O(N)的效率。話說回來,Java類庫的List接口就是個笑話,連Stack類都實現了List,真不知道當年的設計者是怎麼想的。
簡單地說,假如接口不能保證行爲特徵,則「面向接口編程」沒有意義。
原文觀點:<br>
ArrayList
和LinkedList
都實現了List
接口, 可是get
方法的時間複雜度不一樣.個人觀點:<br>
ArrayList
或LinkedList
?而Go語言式的接口也有相似的問題,由於Structural Typing都只是從表面(成員名,參數數量和類型等等)去理解一個接口,並不關注接口的規則和含義,也無法檢查。忘了是Coursera裏哪一個課程中提到這麼一個例子:
nterface IPainter { void Draw(); } nterface ICowBoy { void Draw(); }
在英語中Draw同時具備「畫畫」和「拔槍」的含義,所以對於畫家(Painter)和牛仔(Cow Boy)均可以有Draw這個行爲,可是二者的含義大相徑庭。假如咱們實現了一個「小明」類型,他明明只是一個畫家,可是咱們卻讓他去跟其餘牛仔決鬥,這樣就等於讓他去送死嘛。另外一方面,「小王」也能夠既是一個「畫家」也是個「牛仔」,他兩種Draw都會,在C#裏面咱們就能夠把他實現爲:
class XiaoWang : IPainter, ICowBoy { void IPainter.Draw() { // 畫畫 } void ICowBoy.Draw() { // 掏槍 } }所以我也一直不理解Java的取捨標準。你說這樣一門強調面向對象強調接口強調設計的語言,還要求強制異常,怎麼就不支持接口的顯示實現呢?
原文觀點:<br>
Draw
含義不一樣, 所以接口最好也能支持不一樣的實現.個人觀點:<br>
Go語言爲何不支持這些花哨的特性? 由於, 它們太複雜且沒多大用處, 寫出的代碼很差理解(若是原做者不提示, 誰能發現Darw
的不一樣含義這個坑?). Go語言的哲學是: "Less is more!".
看看Go語言該怎麼作:
type Painter interface { Draw() } type CowBoyer interface { DrawTheGun() } type XiaoWang struct { // ... } func (self *XiaoWang)Draw() { // ... } func (self *XiaoWang)DrawTheGun() { // ... }
XiaoWang
須要關心的只是本身有哪些功能(method
), 至於祖宗關係開始根本不用關心. 等到XiaoWang
各類特性逐漸成熟穩定以後, 發現新來的XiaoMing
也有相似的功能特徵, 這個時候纔會考慮如何用接口來描述XiaoWang
和XiaoMing
共同特徵.
這就是我更傾向於Java和C#中顯式標註異常的緣由。由於程序是人寫的,徹底不會由於一個類只是由於存在某些成員,就會被當作某些接口去使用,一切都是通過「設計」而不是天然發生的。就好像咱們<del>在泰國</del>不會由於一我的看上去是美女就把它當作女人,這年頭的化妝和PS技術太可怕了。
原文觀點:<br>
美女
這個接口來推斷這我的是女人
這個類型.個人觀點:<br>
女人
是怎麼定義的, 難道這不是一個接口?我這裏再小人之心一把:我估計有人看到這裏會說我只是酸葡萄心理,由於C#中沒有這特性因此說它很差。還真不是這樣,早在當年我還沒據說Structural Typing這學名的時候就考慮過這個問題。我寫了一個輔助方法,它能夠將任意類型轉化爲某種接口,例如:
XiaoMing xm = new XiaoMing(); ICowBoy cb = StructuralTyping.From(xm).To<ICowBoy>();
因而,咱們就很快樂地將只懂畫畫的小明送去決鬥了。其內部實現原理很簡單,只是使用Emit在運行時動態生成一個封裝類而已。此外,我還在編譯後使用
Mono.Cecil
分析程序集,檢查From
與To
的泛型參數是否匹配,這樣也等於提供了編譯期的靜態檢查。此外,我還支持了協變逆變,還可讓不須要返回值的接口方法兼容存在返回值的方法,這可比簡單經過名稱和參數類型判斷要強大多了。
原文觀點:<br>
個人觀點:<br>
咱們看看Go是該怎麼寫(基於前面的Go代碼, 沒有Draw
重載):
var xm interface{} = new(XiaoWang) cb := xm.(Painter).(CowBoyer)
可是, 我以爲這樣寫真的很變態. Go語言是爲了解決實際的工程問題的, 不是要像C++那樣成爲各類NB技術的大雜燴.
我始終認同一個觀點: 任何語言均可以寫出垃圾代碼, 可是不能以這些垃圾代碼來證實原語言也垃圾.
有了多種選擇,我才放心地說我喜歡哪一個。JavaScript中只能用回調編寫代碼,因而不少人說它是JavaScript的優勢,說回調多麼多麼美妙我會深不覺得然——只是無法反抗開始享受罷了嘛……
這篇文章好像吐槽有點多?不過這小文章還挺爽的。
這段不是接口相關, 懶得整理/吐槽了.
最後我只想說一個例子, 從C語言時代就很流行的printf
函數. 咱們看看Go語言中是什麼樣子(fmt.Fprintf
):
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
在Go語言中, fmt.Fprintf
只關心怎麼識別各類a ...interface{}
, 怎麼format這些參數, 至於怎麼寫, 寫到哪裏去那徹底是w io.Writer
的事情.
這裏第一個參數的w io.Writer
就是一個接口, 它不只能夠寫到File
, 也能夠寫到net.Conn
, 準確的說是能夠寫到任何實現了io.Writer
接口的對象中.
由於, Go語言接口的非入侵性, 咱們能夠獨立實現本身的對象, 只要符合io.Writer
接口就行, 而後就能夠和fmt.Fprintf
配合工做.
後面的可變參數interface{}
一樣是一個接口, 它代替了C語言的void*
, 用於格式化輸出各類類型的值. (更準確的講, 除了基礎類型, 參數a
必須是一個實現了Stringer
接口的擴展類型).
接口是一個徹底正交的特性, 能夠將Fprintf
從各類a ...interface{}
, 以及各類w io.Writer
徹底剝離出來. Go語言也是這樣, struct
等基礎類型的內存佈局仍是和C語言中同樣, 只是加了個method
(在Go1.1中, method value
就是一個普通閉包函數), 接口以及goroutine
都是在沒有破壞原有的類型語義基礎上正交擴展(而不是像C++那樣搞個構造函數, 之後又是析構函數的).
我到很想知道, 在C++/C#/Java之類的語言中, 是如何實現fmt.Fprintf
的.
套用原做者的一句話做爲結束: Go語言雖然有缺點, 即便老趙是牛人, 可是這篇吐槽也着實通常!