評: 為什麼我不喜歡Go語言式的介面
最近在Go語言的QQ群裡看到關於圖靈社群有牛人老趙吐槽許式偉《Go語言程式設計》的各種爭論.
我之前也看了老趙吐槽許式偉《Go語言程式設計》的文章, 當時想老趙如果能將許大書中不足部分補充完善了也是一個好事情. 因此, 對老趙的後續文章甚是期待.
誰知道看了老趙之後的兩篇吐槽Go語言的文章, 發現完全不是那回事情, 吐槽內容偏差太遠. 本來沒想摻和進來, 但是看到QQ群裡和圖靈社群有很多人甚至把老趙的文章當作真理一樣. 實在忍不住, 昨天註冊了帳號, 進來也說下我的觀點.
這是老趙的幾篇文章:
- Go是一門有亮點的語言,老許是牛人,但這本書著實一般
- 為什麼我認為goroutine和channel是把別的平臺上類庫的功能內建在語言裡
- 為什麼我不喜歡Go語言式的介面(即Structural Typing)
補充說明:
因為當前這篇文章主要是針對老趙的不喜歡Go語言式的介面做 評論. 因為標題的原因, 也造成了很大的爭議性(因為很多人說我理解的很多觀點和老趙的原文不相符).
後面我會對Go語言的一些特性一些簡單的介紹, 但是不會是現在這種方式.
所謂Go語言式的介面,就是不用顯示宣告型別T實現了介面I,只要型別T的公開方法完全滿足介面I的要求,就可以把型別T的物件用在需要介面I的地方。這種做法的學名叫做Structural Typing,有人也把它看作是一種靜態的Duck Typing。除了Go的介面以外,類似的東西也有比如Scala裡的Traits等等。有人覺得這個特性很好,但我個人並不喜歡這種做法,所以在這裡談談它的缺點。當然這跟動態語言靜態語言的討論類似,不能簡單粗暴的下一個“好”或“不好”的結論。
原文觀點:
- Go的隱式介面其實就是靜態的Duck Typing. 很多語言(主要是動態語言)早就有.
- 靜態型別和動態型別沒有絕對的好和不好.
我的觀點:
- Go的隱式介面Duck Typing確實不是新技術, 但是在主流靜態程式語言中支援Duck Typing應該是很少的(不清楚目前是否只有Go語言支援).
- 靜態型別和動態型別雖然沒有絕對的好和不好, 但是每個都是有自己的優勢的, 沒有哪一個可以包辦一切. 而Go是試圖結合靜態型別和動態型別(
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
協議的一部分。
原文觀點:
- 介面就是一個協議, 規定了一組成員.
- 介面還規定了每個行為對應時間複雜度的"特徵".
- 介面還規定了每個行為還包含是否會修改集合的隱藏要求.
我的觀點:
- 第一條: 沒什麼可解釋的, 應該是介面的通俗含義.
- 第二條: 但是介面還包含時間複雜度的"特徵"就比較扯了. 請問這個特徵是由語言特性來約束(語言如何約束?), 還只是由介面的文件作補充說明(這是語言的特性嗎)?
- 第三條: 這個還算是吐槽到了點子上. Go的介面確實不支援C++類似的
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不叫做ArrayList,當然這些都只是我的推測。我的想法是,由於List與IList介面是配套出現的,而像IList的某些方法,例如索引器要求能夠快速獲取元素,這樣使用IList介面的方法才能放心地使用下標進行訪問,而滿足這種特徵的資料結構就基本與陣列難以割捨了,於是名字裡的Array就顯得有些多餘。
假如List改名為ArrayList,那麼似乎就暗示著IList可以有其他實現,難道是LinkedList嗎?事實上,LinkedList根本與IList沒有任何關係,因為它的特徵和List相差太多,它有的盡是些AddFirst、InsertBefore方法等等。當然,LinkedList與List都是ICollection,所以我們可以放心地使用其中一小部分成員,它們的行為特徵是明確的。
原文觀點:
- 推測: 因為為了和
IList<T>
介面配套出現的原因, 才沒有將List<T>
命名為ArrayList<T>
. - 因為
IList<T>
(這個應該是筆誤, 我覺得作者是說List<T>
)索引器要求能夠快速獲取元素, 這樣使用IList介面的方法才能放心地使用下標進行訪問(實現的演算法複雜度特徵向介面方向傳遞了). - 不能將
List<T>
改為ArrayList<T>
的另一個原因是LinkedList<T>
. 因為List<T>
和LinkedList<T>
的時間複雜度不一樣, 所以不能是一個介面(大概是一個演算法複雜度一個介面的意思?). LinkedList<T>
與List<T>
都屬於ICollection<T>
這個祖宗介面.
我的觀點:
- 第一條: 我不知道原作者是怎麼推測的. 介面的本意就是要和實現分離. 現在卻完全繫結到一起了, 那這樣還要介面做什麼(一個
Xxx<T>
對應一個IXxx<T>
介面)? - 第二條: 因為執行時向介面傳遞了某個時間複雜度的實現, 就推匯出介面的都符合某種時間複雜度, 邏輯上根本就不通!
- 第三條: 和前兩個差不多的意思, 沒什麼可說的.
- 第四條: 這個應該是Go非入侵介面的優點. C++/Java就是因為介面的入侵性, 才導致了介面和實現無法完全分離. 因為, C++/Java大部分時間都在整理介面間/實現間的祖宗八代之間的關係了(重要的不是如何分類, 而是能做什麼). 可以參考許式偉給的Java的例子(瞭解祖宗八代之間的關係真的很重要嗎): http://docs.oracle.com/javase/1.4.2/docs/api/overview-tree.html.
這方面的反面案例之一便是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,真不知道當年的設計者是怎麼想的。
簡單地說,假如介面不能保證行為特徵,則“面向介面程式設計”沒有意義。
原文觀點:
- Java的
ArrayList
和LinkedList
都實現了List
介面, 但是get
方法的時間複雜度不同. - 假如介面不能保證行為特徵,則“面向介面程式設計”沒有意義。
我的觀點:
- 第一條: 這其實是原作者列的一個前提, 是為了推出第二條的結論. 但是, 我覺得這裡的邏輯同樣是有問題的. 有這個例子只能說明介面有它的不足, 但是怎麼就證明了 則“面向介面程式設計”沒有意義?
- 第二條: 我要反問一句, 為什麼非要在這裡使用介面(難道是被C++/Java的物件導向洗腦了)? 介面有它合適的地方(面向邏輯層面), 也有它不合適的地方(面向底層演算法層面). 在這裡為什麼不直接使用
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的取捨標準。你說這樣一門強調物件導向強調介面強調設計的語言,還要求強制異常,怎麼就不支援介面的顯示實現呢?
原文觀點:
- 不同實現的
Draw
含義不同, 因此介面最好也能支援不同的實現. - Java/Go之類的介面都沒有C#的介面強大.
我的觀點:
- 第一條: 不要因為自己有個錘子, 就把什麼東西都當作釘子! 你這個是C#的例子(我不懂C#), 但是請不要往Go語言上套! 之前是C++搞出了個函式過載(語義還是相似的, 但是簽名不同), 沒想到C#還搞了個支援同一個單詞不同含義的特性.
- 第二條: 只能說原作者真的不懂Go語言.
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#中顯式標註異常的原因。因為程式是人寫的,完全不會因為一個類只是因為存在某些成員,就會被當做某些介面去使用,一切都是經過“設計”而不是自然發生的。就好像我們
在泰國不會因為一個人看上去是美女就把它當做女人,這年頭的化妝和PS技術太可怕了。
原文觀點:
- 介面是經過“設計”而不是自然發生的.
- 介面有不足, 因為在泰國不能根據
美女
這個介面來推斷這個人是女人
這個型別.
我的觀點:
- Go的哲學是先構造具體物件, 然後再根據共性慢慢歸納出介面, 一開始不用關心祖宗八代的關係.
- 請問
女人
是怎麼定義的, 難道這不是一個介面?
我這裡再小人之心一把:我估計有人看到這裡會說我只是酸葡萄心理,因為C#中沒有這特性所以說它不好。還真不是這樣,早在當年我還沒聽說Structural Typing這學名的時候就考慮過這個問題。我寫了一個輔助方法,它可以將任意型別轉化為某種介面,例如:
XiaoMing xm = new XiaoMing(); ICowBoy cb = StructuralTyping.From(xm).To<ICowBoy>();
於是,我們就很快樂地將只懂畫畫的小明送去決鬥了。其內部實現原理很簡單,只是使用Emit在執行時動態生成一個封裝類而已。此外,我還在編譯後使用
Mono.Cecil
分析程式集,檢查From
與To
的泛型引數是否匹配,這樣也等於提供了編譯期的靜態檢查。此外,我還支援了協變逆變,還可以讓不需要返回值的介面方法相容存在返回值的方法,這可比簡單通過名稱和引數型別判斷要強大多了。
原文觀點:
- C#介面的這個特性很NB...
我的觀點:
我們看看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語言雖然有缺點, 即使老趙是牛人, 但是這篇吐槽也著實一般!
相關文章
- 為什麼我不喜歡Go語言式的介面(即Structural Typing)GoStruct
- 為什麼我最喜歡的程式語言是 GoGo
- 為什麼我喜歡 Lisp 程式語言Lisp
- 我為什麼不喜歡框架框架
- 為什麼我喜歡富於表達性的程式語言
- 為什麼我喜歡JavaJava
- 為什麼我們不喜歡IT行業的7個原因行業
- 我為什麼會從程式不喜歡加{}到加{}
- 為什麼Go語言設計受到歡迎?Go
- 為什麼Python要比其他語言更受喜歡?Python
- 我為什麼喜歡程式設計程式設計
- 為什麼客戶不喜歡我們開發的軟體
- 為什麼我們越來越不喜歡用網站?網站
- 在 Go 語言中,我為什麼使用介面Go
- 我們為什麼要使用GO語言?Go
- 為什麼我喜歡JavaScript的Optional ChainingJavaScriptAI
- 日本玩家為什麼不喜歡PVP遊戲?遊戲
- [譯] 為什麼我更喜歡物件而不是switch語句物件
- 為什麼我們需要一門新語言——Go語言Go
- 程式猿為什麼不招妹子喜歡的原因
- 我們為什麼會喜歡挖礦遊戲?遊戲
- 為什麼我喜歡單獨程式設計程式設計
- 我不喜歡的行為之工作版
- 4.我為什麼喜歡用Dart中的字串?Dart字串
- 這麼多程式語言,為何Python深受喜歡?Python
- 程式老鳥:我為什麼喜歡敏捷開發框架敏捷框架
- 為什麼我們喜歡看別人在遊戲裡受苦遊戲
- 為什麼那麼多人要學習go語言?go語言有什麼特點?Go
- 為什麼很多公司都轉型go語言開發?Go語言能做什麼Go
- 為什麼開發者不喜歡市場人員的 8 個理由
- 為什麼我不推薦 JavsScript 為首選程式語言
- 為什麼我不推薦JavsScript為首選程式語言
- 為何我不喜歡使用儲存過程儲存過程
- 什麼是Go語言?Go語言有什麼特點?Go
- 開放出版:為什麼我們需要一門新語言?許式偉《Go語言程式設計》序Go程式設計
- 為什麼亞馬遜、臉書和Discord的開發人員喜歡Rust程式語言? - businessinsider亞馬遜RustIDE
- 為什麼 Go 語言能在中國這麼火?Go
- 你為什麼不應該過度關注go語言的逃逸分析Go