通過我的上一篇文章以及最近幾個月期間對於Go程式設計語言的間接推廣,我與許多開始對這門語言感興趣的人們進行了交流,所以現在我打算轉而去寫一些我對這門語言的不滿,依據我目前積累的經驗來提供一種更加全面的看法,藉此可以讓一部分人意識到Go語言終究並不是他們項目的最佳選擇。**備忘1**需要重點指出的是,文章裡的部分觀點(如果不是全部的話)是基於我個人的主觀想法並且跟我的編程習慣有關,它們沒有必要也不應該被描述成“最佳解法”。還有就是,我現在仍舊是一個 Go 語言的菜鳥,我接下來要說的一些東西可能是不準確或者錯誤的,對於有誤的地方請務必糾正我,這樣我才能學到新東西。:D**備忘2**在開始前我需要聲明的是:我熱愛這門語言並且我已經解釋了為什麼我覺得對於許多應用來說這是一個更佳的選擇,但是我對於 Go 和 Rust 那個更好或者 Go 和其他任何語言哪一個更好這種問題不感興趣……選擇你認為最佳的方案去完成你要做的事情:如果你認為 Rust 更好就嘗試使用它,如果你認為是你傳送到處理器的位元組碼引起了資料匯流排的錯誤,就去嘗試錯誤修正,兩種情況都是,儘管去編程,而不是浪費生命在盲目追逐所謂的流行語言上。那麼現在讓我們從最小的問題著手逐漸遞進到嚴重的問題上……## 請給我一個三元運算子在編寫很大一部分運行在終端模擬器上的應用時,我發現自己總是會列印一些系統狀態來確認自己正在調試的功能是開啟還是關閉(例如開啟或者關閉 bettercap 的其中一個模組並且報告該資訊),這意味著很多時候我需要把一個布爾類型的變數轉換成一個更容易理解的字串,在 C++ 或者其他支援這種運算子的地方它是這個樣子的:```cbool someEnabledFlagHere = false;printf("Cool module is: %s\n", someEnabledFlagHere ? "enabled" : "not enabled");```不幸的是 Go 並不支援這種寫法,這意味著你最後會寫出這樣的一堆東西:```gosomeEnabledFlagHere := falseisEnabledString := "not enabled"if someEnabledFlagHere == true {isEnabledString = "enabled"}log.Printf("Cool module is: %s\n", isEnabledString)```並且這已經很可能是你能想到的最優雅解法(而不是為了實現這個功能而去建立一個 map)。這究竟是否能算是更方便了?對我而言這種寫法很醜,並且當你的系統實現高度模組化的時候,一遍又一遍的寫這種東西會讓你的代碼變得越來越臃腫,而這僅僅是因為少了一個操作符。ˉ\\_(ツ)_/ˉ**備忘** 好吧,我知道你可以通過建立函數或者使用字串類型的別名來實現,但是完全沒有必要在評論裡把所有這些難看的替代方法都寫出來,謝謝 :)## 自動產生的這堆東西不等於文檔Go 語言的專家們,我衷心感謝你們分享的代碼以及我每天閱讀的時候學習到的這些東西,但是我不認為他們有什麼用處:```go// this function adds two integers // -put captain obvious meme here-func addTwoNumbers(a, b int) int {return a + b}```我並不認為[這樣的東西](https://godoc.org/github.com/google/gopacket)可以代替文檔,但是這看起來確實是 Go 語言使用者們給代碼添加註釋(文檔)的標準方式(當然也有一些例外的情況),即使是在一些我們所熟知的擁有數以千計貢獻者的架構中也是如此……我自己並不是很熱衷於添加詳細的文檔,如果你喜歡自己深入研究代碼的話這並不會是一個很大的問題,但是如果你是文檔的重度依賴者,那麼你恐怕要失望了。## 把 Git 倉庫作為包管理系統簡直是瘋了我幾天前在推特上有一段很有趣的對話,在那裡我解釋給某人聽為什麼 Go 導包的時候看起來很像 Github 的連結:```goimport "github.com/bettercap/bettercap"```或者是像下面這樣:```# go get github.com/bettercap/bettercap```簡單來說,在 Go 最簡單的安裝方式中,你很可能會用到(不使用 vendor 目錄並且也不覆蓋 $GOPATH 變數的情況下)所有(事實上並不是,但是為了把問題簡化我們可以這樣假設)在這個安裝目錄或者你設定的 $GOPATH 變數目錄下的東西,在我這裡這個目錄是 /home/evilsocket/gocode(是的,[確實是這樣](https://github.com/evilsocket/dotfiles/blob/master/data/go.zshrc#L2))。每當我使用 go get 命令擷取或者通過導包後使用 go get 命令[自動下載所需的包](https://github.com/bettercap/bettercap/blob/master/Makefile#L28)時,它在我的電腦上基本是下面這個樣子:```# mkdir -p $GOHOME/src# git clone https://github.com/bettercap/bettercap.git $GOHOME/src/github.com/bettercap/bettercap```如你所見,Go 事實上直接使用了 Git 倉庫來管理這些包,應用或者任何與 Go 有關的東西……從某方面來說確實很方便,但是這會引起一個很大的問題:只要你不使用其他工具或者基於這個問題做一些難看的規避方案,那麼你每次在一個新系統上編譯你的軟體時,只要缺失了某個依賴包,這個依賴包所在倉庫的主分支就會被複製下來。這意味著,**儘管你應用的代碼完全沒有修改,但是你每次在新電腦上編譯時間都很可能會產生代碼差異**(只要你任何一個依賴包在主分支上有改動)。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/amazing/mgc.gif) [via GIPHY](https://giphy.com/gifs/shia-labeouf-12NUbkX6p4xOO4)當使用者在使用源碼編譯他們自己版本的軟體時開始針對第三方庫報告問題,而你完全不清楚是哪一個提交引起的時候,請盡情享受吧 ^\_^是的沒錯, 你可以使用像 [Glide](https://github.com/Masterminds/glide) 或者其它類似的工具來將你的依賴“固定”到某些特定的提交或者標籤,並且為他們建立一個特定的目錄……這確實是由於一個糟糕的設計而不得不採取的措施,我們都知道這確實行得通,但是這看起來很噁心。直接使用 [URL 重新導向](http://labix.org/gopkg.in)來匯入特定版本的包看起來也跟上面差不多……這是可行的,但是同樣很難看,而且有些人可能會擔心這會引起一些安全方面的問題……誰來控制這些重新導向?當你在自己的電腦上使用 root 使用者或者 sudo 進行導包或者編譯這些東西時,這樣一個機制能讓你安心工作嗎?我想應該不會。## 反射?我覺得不算是……當我第一次聽說 Go 裡面有反射時,根據以往在其他語言(例如 Python,Ruby,Java,C# 和其他語言)上面反射的概念,我想到了它的許多用途(或者說,我認為的 Go 的反射的用處),像是自動枚舉 802.11 協議各層的類型,並且依據 WiFi 自動化模糊測試或者其它近似的方式自動產生對應的資料包……事實證明,對於 Go 語言來說反射是一個很大的概念 :D舉個例子,在一個不透明的介面對象中,你可以擷取到它的原始類型並且你也可以列出某個特定對象的域,但是你沒辦法簡單地枚舉一個特定的包裡定義的對象(包括結構體和基本類型),這看起來好像並不重要,但是沒有這種特性你完成不了下面這些功能:1. 構造一個外掛程式系統,它會從給定的包裡自動載入內容,而不需要明確地聲明(需要載入哪些東西)。2. 基本上所有你在 Python 裡可以用 dir 命令做到的所有事情3. 構建我想到的 802.11 協議的漏洞檢查工具(fuzzer)由此看出,(Go裡面的)反射跟別的語言比起來確實有點有限了……我不清楚你會怎麼想,但是這確實讓我有點煩……## 泛型?沒有大部分從物件導向編程的語言(轉向 Go 開發時)會抱怨 Go 裡缺少泛型,就我個人而言這並不算是一個大問題,因為我自己並不是很熱衷於不計代價的面相對象編程。相反,我認為 Go 的物件模型(確切的說並不能算是物件模型)很簡潔,我認為這種設計跟泛型會引起的複雜性相衝突了。**備忘**我並不是想說“泛型==面相對象編程(OOP)”,但是大部分開發人員希望(Go 語言支援)泛型是因為他們用 Go 來替代 C++ 並且希望有類似的模板,或者 Java 泛型……我們確實可以討論從其它具有泛型或者類似東西的功能語言轉型的一小部分(開發人員),但是就我個人經驗來說這部分人並不影響統計。從另一個方面來看,這種(看起來跟直接使用 C 語言裡的功能和結構體很相似的)簡化物件模型,會讓其他一些事情變得沒有其他語言來說那麼簡單和直接。假設你正在開發一個包含了許多模組的軟體(我喜歡把軟體模組化來保證代碼足夠簡潔明了 :D),它們全部都是從同一個基類上派生出來的(這樣你就會希望有一個特定的介面並且可以透明地處理它們),並且需要有一些已經實現了的預設功能來在各個派生的模組間共用(這些是所有派生的模組都需要使用的方法,所以為了方便起見它們會在基類中直接被實現)。好吧,在其他語言裡你會有一些抽象類別,或者一些已經實現了部分功能(子類共用的方法)其它部分聲明為介面(純虛函數)的類:```cclass BaseObject {protected: void commonMethod() { cout << "I'm available to all derived objects!" << endl; } // while this needs to be implemented by every derived object virtual interfaceMethod() = 0;};```碰巧 Go 語言就是不支援這種寫法,一個類可以是一個介面類或者是一個基礎結構體(對象),但是它並不能同時是這兩者,所以我們需要把這個例子按照這種方式進行“分離”:```gotype BaseObjectForMethods struct { }func (o BaseObjectForMethods) commonMethod() {log.Printf("I'm available to all derived objects!\n")}type BaseInterface interface {interfaceMethod()}type Derived struct {// I just swallowed my base object and got its methodsBaseObjectForMethods} // and here we implement the interface method insteadfunc (d Derived) interfaceMethod() {// whatever, i'm a depressed object model anyway ... :/}```最終你派生出來的對象會實現裡面的介面並且繼承基礎結構體……儘管這看起來可能一樣或者說這是一個尚且算是優雅的解耦方式,但是當你嘗試去再稍稍擴充一下 Go 語言的多態性的時候就會發現這很快就會變得一團糟([這是一個更加實際的例子](https://github.com/bettercap/bettercap/blob/master/session/module.go))## Go 很容易編譯,但是 CGO 就如地獄一般編譯(和交叉編譯)Go 應用非常的簡單,不管你是在那種平台編譯或者運行。使用同一個 Go 安裝包你可以為 Windows,macOS,或者 Android 或者其他基於 GNU/Linux 的 MIPS 裝置編譯同一個應用,不需要工具鏈,不需要外部編譯器,不需要為作業系統設立特定標記,也沒有那些從來都不會按照我們設想來啟動並執行古怪的配置指令碼……這簡直不要太棒好嗎?!(如果你是從 C/C++ 的世界中過來的,並且經常需要交叉編譯工程,你就會知道這意味著什麼了……或者假設你是一個安全顧問,而你現在需要儘快交叉編譯軟體,來同時解決你昨天被(病毒)感染的 Windows 網域名稱控制器和 MIPS IP 網路攝影機)。好吧,如果你正在使用一些 Go 語言沒有原生支援的本地庫,你就會發現事情沒有那麼簡單了,除非你僅僅是為了用 Go 來寫一個 “hello world”。讓我們假設你的 Go 項目正在使用 libsqlite3,或者 libmysql,或者其他的第三方庫,由於那些實現了(你正在使用的 Go API 裡的 )這整套對象-關係映射的人,並沒有把 Go 語言裡定義的資料庫協議都重寫,而僅僅重寫了其中一些通過 CGO 模組封裝的,經曆了完善測試的系統庫——迄今為止,所有的語言都有自己的封裝機制來處理本地庫——並且,如果你僅僅只是為了你的主機編譯工程的話,這完全沒有問題,因為你需要的所有庫(libsqlite3.so,libmysql.so 或者其他的庫)都可以通過 apt-get install 命令安裝。但是如果你需要進行交叉編譯呢?比如說需要為 Android 進行編譯?如果目標系統裡沒有預設的庫檔案呢?當然了,這樣你就系統通過對應系統的 C/C++ 工具鏈來自己編譯庫檔案,或者是找方法把編譯器直接安裝到系統裡然後編譯出所有東西(用你的 Android 平板來直接作為編譯主機)。那你請好好享受。無需多言,如果你想要(或者需要)支援多架構跨平台(為什麼你不應該認為 Go 最大的優點之一——如我們所說的——恰恰正是這個?),這會讓你的編譯複雜度大增,進而讓你的 Go 項目在交叉編譯時間至少會和一個 C/C++ 項目一樣複雜(諷刺的是,有時甚至會更複雜)。在[我的一些項目](https://github.com/evilsocket/arc)的某個時刻,我將項目裡的所有 sqlite 資料庫都替換成了 JSON 檔案,這讓我擺脫了本地依賴從而構建了一個 100%(基於)Go (編寫的)應用。這樣依賴交叉編譯又重新變得簡單了(如果你不能避免使用本地依賴,那麼這是你不得不解決的難題……對此我感到十分抱歉 :/)。如果現在你“聰明的內心“正在尖叫著說“全部使用靜態編譯!”(靜態編譯庫檔案來讓它們至少被打包進二進位檔案裡),不要這樣做。如果你用一個特定版本的 glibc(c運行庫)來對所有代碼進行靜態編譯,那麼編譯出來的二進位檔案在使用其他版本 glibc 的系統上是無法啟動並執行。如果你“更聰明的內心“正在尖叫著說“使用 docker 來區分編譯版本!”,請找出一個方法來正確的配置所有的平台和所有的(cpu)架構後發郵件告訴我這個方法 :)如果你的“對 go 語言有點瞭解的內心”正打算建議一些外部的 glibc 替代品,請參照上一條的需求(如何區分所有配置):D## ASLR? 沒有!(嘲諷臉)接下來這個稍微有點爭議,[Go 應用的二進位檔案沒有 ASLR(針對緩衝區溢位的安全保護技術)](https://rain-1.github.io/golang-aslr.html)。但是,根據 Go 的記憶體管理方式(最重要的是,[它並沒有指標演算法](https://golang.org/doc/faq#no_pointer_arithmetic)),這並不會成為一個安全問題——除非你使用了有漏洞的本地庫檔案——這樣的情況下 Go 缺少的 ASLR 機制[會讓開發變得更容易](http://blog.securitymouse.com/2014/07/bla-bla-lz4-bla-bla-golang-or-whatever.html)。現在,我有點瞭解 Go 語言開發人員的觀點了,但是卻不太認同:為什麼要在運行時增加複雜度,僅僅用來保證(程式)運行時不會因為某些根本不會被輕易攻擊的東西而出問題?……一旦考慮到你最終(在項目裡)會使用到的第三方本地庫的頻率(上文中有對此進行過討論 :P),我認為直接無視這個問題不是一個明智的選擇。## 總結還有許多其他關於 Go 的小問題是我不喜歡的,但是那確實也是我瞭解的其他語言上共有的,所以我僅僅關注了主要的問題而跳過了一些這樣的問題:比如我在主觀上不喜歡這個文法 X(順便說一句,我確實喜歡 Go 的文法)。我看到許多的人,盲目的去投身一門新的語言,僅僅是因為在 GitHub 上很流行……從一個方面說,如果許多的開發人員都決定使用它,那麼這肯定有很充分的理由(或者他們僅僅是“把所有東西都編譯成 JavaScript”來追趕時髦的人),但是沒有一門完美的語言可以說是所有應用的最佳選擇(但是,我對於 nipples 和 default injection 仍舊抱有希望 U.U),在選擇之前最好再三比對它們的優缺點。願世界和平----------------![](https://blockchain.info/Resources/buttons/donate_64.png)保持聯絡![關注 @evilsocket](https://twitter.com/evilsocket)
via: https://www.evilsocket.net/2018/03/14/Go-is-amazing-so-here-s-what-i-don-t-like-about-it/
作者:Simone 譯者:keon-lam 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
614 次點擊