1. 程式人生 > 其它 >go 模板字串_Go 實在是令人驚歎,但是我想說說我不喜歡它的地方

go 模板字串_Go 實在是令人驚歎,但是我想說說我不喜歡它的地方

技術標籤:go 模板字串

通過我的上一篇文章以及最近幾個月期間對於Go程式語言的間接推廣,我與許多開始對這門語言感興趣的人們進行了交流,所以現在我打算轉而去寫一些我對這門語言的不滿,依據我目前積累的經驗來提供一種更加全面的看法,藉此可以讓一部分人意識到Go語言終究並不是他們專案的最佳選擇。

備註1

需要重點指出的是,文章裡的部分觀點(如果不是全部的話)是基於我個人的主觀想法並且跟我的程式設計習慣有關,它們沒有必要也不應該被描述成“最佳解法”。還有就是,我現在仍舊是一個 Go 語言的菜鳥,我接下來要說的一些東西可能是不準確或者錯誤的,對於有誤的地方請務必糾正我,這樣我才能學到新東西。

0a2e7832c52283a373441753977d6dd9.png

備註2

在開始前我需要宣告的是:我熱愛這門語言並且我已經解釋了為什麼我覺得對於許多應用來說這是一個更佳的選擇,但是我對於 Go 和 Rust 那個更好或者 Go 和其他任何語言哪一個更好這種問題不感興趣……選擇你認為最佳的方案去完成你要做的事情:如果你認為 Rust 更好就嘗試使用它,如果你認為是你傳送到處理器的位元組碼引起了資料匯流排的錯誤,就去嘗試糾錯,兩種情況都是,儘管去程式設計,而不是浪費生命在盲目追逐所謂的流行語言上。

那麼現在讓我們從最小的問題著手逐漸遞進到嚴重的問題上……

請給我一個三元運算子

在編寫很大一部分執行在終端模擬器上的應用時,我發現自己總是會列印一些系統狀態來確認自己正在除錯的功能是開啟還是關閉(例如開啟或者關閉 bettercap 的其中一個模組並且報告該資訊),這意味著很多時候我需要把一個布林型別的變數轉換成一個更容易理解的字串,在 C++ 或者其他支援這種運算子的地方它是這個樣子的:

bool someEnabledFlagHere = false;
printf("Cool module is: %sn", someEnabledFlagHere ? "enabled" : "not enabled");

不幸的是 Go 並不支援這種寫法,這意味著你最後會寫出這樣的一堆東西:

someEnabledFlagHere := false
isEnabledString := "not enabled"
if someEnabledFlagHere == true {
    isEnabledString = "enabled"
}
log.Printf("Cool module is: %sn", isEnabledString)

並且這已經很可能是你能想到的最優雅解法(而不是為了實現這個功能而去建立一個 map)。這究竟是否能算是更方便了?對我而言這種寫法很醜,並且當你的系統實現高度模組化的時候,一遍又一遍的寫這種東西會讓你的程式碼變得越來越臃腫,而這僅僅是因為少了一個操作符。ˉ*(ツ)*/ˉ

備註 好吧,我知道你可以通過建立函式或者使用字串型別的別名來實現,但是完全沒有必要在評論裡把所有這些難看的替代方法都寫出來,謝謝

e849e5703eb35edcd3d849fbd2a01121.png

自動生成的這堆東西不等於文件

Go 語言的專家們,我衷心感謝你們分享的程式碼以及我每天閱讀的時候學習到的這些東西,但是我不認為他們有什麼用處:

// this function adds two integers 
// -put captain obvious meme here-
func addTwoNumbers(a, b int) int {
    return a + b
}

我並不認為這樣的東西可以代替文件,但是這看起來確實是 Go 語言使用者們給程式碼添加註釋(文件)的標準方式(當然也有一些例外的情況),即使是在一些我們所熟知的擁有數以千計貢獻者的框架中也是如此……我自己並不是很熱衷於新增詳細的文件,如果你喜歡自己深入研究程式碼的話這並不會是一個很大的問題,但是如果你是文件的重度依賴者,那麼你恐怕要失望了。

把 Git 倉庫作為包管理系統簡直是瘋了

我幾天前在推特上有一段很有趣的對話,在那裡我解釋給某人聽為什麼 Go 導包的時候看起來很像 Github 的連結:

import "github.com/bettercap/bettercap"

或者是像下面這樣:

# go get github.com/bettercap/bettercap

簡單來說,在 Go 最簡單的安裝方式中,你很可能會用到(不使用 vendor 目錄並且也不覆蓋

GOPATH 變數目錄下的東西,在我這裡這個目錄是 /home/evilsocket/gocode(是的, 確實是這樣)。每當我使用 go get 命令獲取或者通過導包後使用 go get 命令 自動下載所需的包時,它在我的電腦上基本是下面這個樣子:
# mkdir -p $GOHOME/src
# git clone https://github.com/bettercap/bettercap.git $GOHOME/src/github.com/bettercap/bettercap

如你所見,Go 事實上直接使用了 Git 倉庫來管理這些包,應用或者任何與 Go 有關的東西……從某方面來說確實很方便,但是這會引起一個很大的問題:只要你不使用其他工具或者基於這個問題做一些難看的規避方案,那麼你每次在一個新系統上編譯你的軟體時,只要缺失了某個依賴包,這個依賴包所在倉庫的主分支就會被克隆下來。這意味著,儘管你應用的程式碼完全沒有修改,但是你每次在新電腦上編譯時都很可能會產生程式碼差異(只要你任何一個依賴包在主分支上有改動)。

via GIPHY

當用戶在使用原始碼編譯他們自己版本的軟體時開始針對第三方庫報告問題,而你完全不清楚是哪一個提交引起的時候,請盡情享受吧 ^_^

是的沒錯, 你可以使用像 Glide 或者其它類似的工具來將你的依賴“固定”到某些特定的提交或者標籤,並且為他們建立一個特定的目錄……這確實是由於一個糟糕的設計而不得不採取的措施,我們都知道這確實行得通,但是這看起來很噁心。

直接使用 URL 重定向來匯入特定版本的包看起來也跟上面差不多……這是可行的,但是同樣很難看,而且有些人可能會擔心這會引起一些安全方面的問題……誰來控制這些重定向?當你在自己的電腦上使用 root 使用者或者 sudo 進行導包或者編譯這些東西時,這樣一個機制能讓你安心工作嗎?我想應該不會。

反射?我覺得不算是……

當我第一次聽說 Go 裡面有反射時,根據以往在其他語言(例如 Python,Ruby,Java,C# 和其他語言)上面反射的概念,我想到了它的許多用途(或者說,我認為的 Go 的反射的用處),像是自動列舉 802.11 協議各層的型別,並且依據 WiFi 自動化模糊測試或者其它近似的方式自動生成對應的資料包……事實證明,對於 Go 語言來說反射是一個很大的概念

0a2e7832c52283a373441753977d6dd9.png

舉個例子,在一個不透明的介面物件中,你可以獲取到它的原始型別並且你也可以列出某個特定物件的域,但是你沒辦法簡單地列舉一個特定的包裡定義的物件(包括結構體和基本型別),這看起來好像並不重要,但是沒有這種特性你完成不了下面這些功能:

  1. 構造一個外掛系統,它會從給定的包裡自動載入內容,而不需要明確地宣告(需要載入哪些東西)。
  2. 基本上所有你在 Python 裡可以用 dir 命令做到的所有事情
  3. 構建我想到的 802.11 協議的漏洞檢查工具(fuzzer)

由此看出,(Go裡面的)反射跟別的語言比起來確實有點有限了……我不清楚你會怎麼想,但是這確實讓我有點煩……

泛型?沒有

大部分從面向物件程式設計的語言(轉向 Go 開發時)會抱怨 Go 裡缺少泛型,就我個人而言這並不算是一個大問題,因為我自己並不是很熱衷於不計代價的面相物件程式設計。相反,我認為 Go 的物件模型(確切的說並不能算是物件模型)很簡潔,我認為這種設計跟泛型會引起的複雜性相沖突了。

備註

我並不是想說“泛型==面相物件程式設計(OOP)”,但是大部分開發者希望(Go 語言支援)泛型是因為他們用 Go 來替代 C++ 並且希望有類似的模板,或者 Java 泛型……我們確實可以討論從其它具有泛型或者類似東西的功能語言轉型的一小部分(開發者),但是就我個人經驗來說這部分人並不影響統計。

從另一個方面來看,這種(看起來跟直接使用 C 語言裡的功能和結構體很相似的)簡化物件模型,會讓其他一些事情變得沒有其他語言來說那麼簡單和直接。

假設你正在開發一個包含了許多模組的軟體(我喜歡把軟體模組化來保證程式碼足夠簡潔明瞭

0a2e7832c52283a373441753977d6dd9.png

),它們全部都是從同一個基類上派生出來的(這樣你就會希望有一個特定的介面並且可以透明地處理它們),並且需要有一些已經實現了的預設功能來在各個派生的模組間共享(這些是所有派生的模組都需要使用的方法,所以為了方便起見它們會在基類中直接被實現)。

好吧,在其他語言裡你會有一些抽象類,或者一些已經實現了部分功能(子類共享的方法)其它部分宣告為介面(純虛擬函式)的類:

class 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 語言就是不支援這種寫法,一個類可以是一個介面類或者是一個基礎結構體(物件),但是它並不能同時是這兩者,所以我們需要把這個例子按照這種方式進行“分離”:

type 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 methods
    BaseObjectForMethods
}   
// and here we implement the interface method instead
func (d Derived) interfaceMethod() {
    // whatever, i'm a depressed object model anyway ... :/
}

最終你派生出來的物件會實現裡面的介面並且繼承基礎結構體……儘管這看起來可能一樣或者說這是一個尚且算是優雅的解耦方式,但是當你嘗試去再稍稍擴充套件一下 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++ 專案一樣複雜(諷刺的是,有時甚至會更復雜)。

我的一些專案的某個時刻,我將專案裡的所有 sqlite 資料庫都替換成了 JSON 檔案,這讓我擺脫了本地依賴從而構建了一個 100%(基於)Go (編寫的)應用。這樣依賴交叉編譯又重新變得簡單了(如果你不能避免使用本地依賴,那麼這是你不得不解決的難題……對此我感到十分抱歉

01709650deb199ae50dbb2652725ea07.png

)。

如果現在你“聰明的內心“正在尖叫著說“全部使用靜態編譯!”(靜態編譯庫檔案來讓它們至少被打包進二進位制檔案裡),不要這樣做。如果你用一個特定版本的 glibc(c執行庫)來對所有程式碼進行靜態編譯,那麼編譯出來的二進位制檔案在使用其他版本 glibc 的系統上是無法執行的。

如果你“更聰明的內心“正在尖叫著說“使用 docker 來區分編譯版本!”,請找出一個方法來正確的配置所有的平臺和所有的(cpu)架構後發郵件告訴我這個方法

e849e5703eb35edcd3d849fbd2a01121.png

如果你的“對 go 語言有點了解的內心”正打算建議一些外部的 glibc 替代品,請參照上一條的需求(如何區分所有配置)

0a2e7832c52283a373441753977d6dd9.png

ASLR? 沒有!(嘲諷臉)

接下來這個稍微有點爭議,Go 應用的二進位制檔案沒有 ASLR(針對緩衝區溢位的安全保護技術)。但是,根據 Go 的記憶體管理方式(最重要的是,它並沒有指標演算法),這並不會成為一個安全問題——除非你使用了有漏洞的本地庫檔案——這樣的情況下 Go 缺少的 ASLR 機制會讓開發變得更容易

現在,我有點了解 Go 語言開發者的觀點了,但是卻不太認同:為什麼要在執行時增加複雜度,僅僅用來保證(程式)執行時不會因為某些根本不會被輕易攻擊的東西而出問題?……一旦考慮到你最終(在專案裡)會使用到的第三方本地庫的頻率(上文中有對此進行過討論

008733113a6f6ba40d7c29466bb4b581.png

),我認為直接無視這個問題不是一個明智的選擇。

總結

還有許多其他關於 Go 的小問題是我不喜歡的,但是那確實也是我瞭解的其他語言上共有的,所以我僅僅關注了主要的問題而跳過了一些這樣的問題:比如我在主觀上不喜歡這個語法 X(順便說一句,我確實喜歡 Go 的語法)。我看到許多的人,盲目的去投身一門新的語言,僅僅是因為在 GitHub 上很流行……從一個方面說,如果許多的開發者都決定使用它,那麼這肯定有很充分的理由(或者他們僅僅是“把所有東西都編譯成 JavaScript”來追趕時髦的人),但是沒有一門完美的語言可以說是所有應用的最佳選擇(但是,我對於 nipples 和 default injection 仍舊抱有希望 U.U),在選擇之前最好再三比對它們的優缺點。

願世界和平


7a56b40a2b58721f9a9a43dfe70066c1.png