4 Go 語言批評

已發表: 2022-03-11

Go(又名 Golang)是人們最感興趣的語言之一。截至 2018 年 4 月,它在 TIOBE 指數中排名第 19 位。 越來越多的人從 PHP、Node.js 和其他語言切換到 Go 並在生產中使用它。 許多很酷的軟件(如 Kubernetes、Docker 和 Heroku CLI)都是使用 Go 編寫的。

那麼,Go 成功的關鍵是什麼? 該語言中有很多東西使它非常酷。 但是,正如其創建者之一 Rob Pike 所指出的那樣,Go 如此受歡迎的主要原因之一是它的簡單性。

簡單很酷:您不需要學習很多關鍵字。 它使語言學習變得非常容易和快速。 但是,另一方面,有時開發人員缺少其他語言所具有的某些功能,因此,從長遠來看,他們需要編寫變通方法或編寫更多代碼。 不幸的是,Go 在設計上缺乏很多功能,有時真的很煩人。

Golang 旨在加快開發速度,但在很多情況下,您編寫的代碼比使用其他編程語言編寫的代碼要多。 我將在下面的 Go 語言批評中描述一些此類情況。

4 Go 語言批評

1. 缺少函數重載和參數默認值

我將在這裡發布一個真實的代碼示例。 當我在處理 Golang 的 Selenium 綁定時,我需要編寫一個具有三個參數的函數。 其中兩個是可選的。 這是實施後的樣子:

 func (wd *remoteWD) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error { // the actual implementation was here } func (wd *remoteWD) WaitWithTimeout(condition Condition, timeout time.Duration) error { return wd.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval) } func (wd *remoteWD) Wait(condition Condition) error { return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) }

我必須實現三個不同的函數,因為我不能只是重載函數或傳遞默認值——Go 沒有按設計提供它。 想像一下,如果我不小心打錯了會發生什麼? 這是一個例子:

我會得到一堆“未定義”

我不得不承認,有時函數重載會導致代碼混亂。 另一方面,正因為如此,程序員需要編寫更多的代碼。

如何改進?

這是 JavaScript 中相同(嗯,幾乎相同)的示例:

 function Wait (condition, timeout = DefaultWaitTimeout, interval = DefaultWaitInterval) { // actual implementation here }

如您所見,它看起來更清晰。

我也喜歡 Elixir 的方法。 下面是它在 Elixir 中的樣子(我知道我可以使用默認值,就像上面的例子一樣——我只是把它作為一種可以完成的方式來展示):

 defmodule Waiter do @default_interval 1 @default_timeout 10 def wait(condition, timeout, interval) do // implementation here end def wait(condition, timeout), do: wait(condition, timeout, @default_interval) def wait(condition), do: wait(condition, @default_timeout, @default_interval) end Waiter.wait("condition", 2, 20) Waiter.wait("condition", 2) Waiter.wait("condition")

2. 缺乏泛型

這可以說是 Go 用戶最需要的功能。

想像一下,您要編寫一個 map 函數,在其中傳遞整數數組和函數,該函數將應用於其所有元素。 聽起來很容易,對吧?

讓我們為整數做:

 package main import "fmt" func mapArray(arr []int, callback func (int) (int)) []int { newArray := make([]int, len(arr)) for index, value := range arr { newArray[index] = callback(value) } return newArray; } func main() { square := func(x int) int { return x * x } fmt.Println(mapArray([]int{1,2,3,4,5}, square)) // prints [1 4 9 16 25] }

看起來不錯,對吧?

好吧,想像一下你還需要為字符串做這件事。 您需要編寫另一個實現,除了簽名之外完全相同。 這個函數需要一個不同的名字,因為 Golang 不支持函數重載。 結果,您將擁有一堆具有不同名稱的類似函數,它們看起來像這樣:

 func mapArrayOfInts(arr []int, callback func (int) (int)) []int { // implementation } func mapArrayOfFloats(arr []float64, callback func (float64) (float64)) []float64 { // implementation } func mapArrayOfStrings(arr []string, callback func (string) (string)) []string { // implementation }

這肯定違反了 DRY(不要重複自己)原則,該原則指出您需要編寫盡可能少的複制/粘貼代碼,而是將其移動到函數中並重用它們。

缺乏泛型意味著數百種變體功能

另一種方法是使用帶有interface{}作為參數的單個實現,但這可能會導致運行時錯誤,因為運行時類型檢查更容易出錯。 而且它會更慢,所以沒有簡單的方法來實現這些功能。

如何改進?

有很多好的語言都包含泛型支持。 例如,下面是 Rust 中的相同代碼(我使用vec而不是array以使其更簡單):

 fn map<T>(vec:Vec<T>, callback:fn(T) -> T) -> Vec<T> { let mut new_vec = vec![]; for value in vec { new_vec.push(callback(value)); } return new_vec; } fn square (val:i32) -> i32 { return val * val; } fn underscorify(val:String) -> String { return format!("_{}_", val); } fn main() { let int_vec = vec![1, 2, 3, 4, 5]; println!("{:?}", map::<i32>(int_vec, square)); // prints [1, 4, 9, 16, 25] let string_vec = vec![ "hello".to_string(), "this".to_string(), "is".to_string(), "a".to_string(), "vec".to_string() ]; println!("{:?}", map::<String>(string_vec, underscorify)); // prints ["_hello_", "_this_", "_is_", "_a_", "_vec_"] }

請注意, map函數只有一個實現,它可以用於您需要的任何類型,甚至是自定義類型。

3. 依賴管理

任何有 Go 經驗的人都可以說依賴管理真的很難。 Go 工具允許用戶通過運行go get <library repo>來安裝不同的庫。 這裡的問題是版本管理。 如果庫維護者進行了一些向後不兼容的更改並將其上傳到 GitHub,那麼在此之後嘗試使用您的程序的任何人都會收到錯誤,因為go get只會將您的存儲git clone到庫文件夾中。 此外,如果未安裝該庫,則程序將因此無法編譯。

您可以通過使用 Dep 管理依賴項(https://github.com/golang/dep)做得更好,但這裡的問題是您將所有依賴項存儲在您的存儲庫中(這不好,因為您的存儲庫將不僅包含您的代碼,還包含成千上萬行依賴項代碼),或者只存儲包列表(但同樣,如果依賴項的維護者進行了向後不兼容的更改,它將全部崩潰)。

如何改進?

我認為這裡的完美示例是 Node.js(我想一般是 JavaScript)和 NPM。 NPM 是一個包存儲庫。 它存儲不同版本的包,所以如果你需要一個特定版本的包,沒問題——你可以從那裡得到它。 此外,任何 Node.js/JavaScript 應用程序中的一件事就是package.json文件。 在這裡,列出了所有依賴項及其版本,因此您可以使用npm install將它們全部安裝(並獲取絕對適用於您的代碼的版本)。

此外,包管理的優秀示例是 RubyGems/Bundler(用於 Ruby 包)和 Crates.io/Cargo(用於 Rust 庫)。

4. 錯誤處理

Go 中的錯誤處理非常簡單。 在 Go 中,基本上你可以從函數返回多個值,而函數可以返回錯誤。 像這樣的東西:

 err, value := someFunction(); if err != nil { // handle it somehow }

現在假設您需要編寫一個函數來執行三個返回錯誤的操作。 它看起來像這樣:

 func doSomething() (err, int) { err, value1 := someFunction(); if err != nil { return err, nil } err, value2 := someFunction2(value1); if err != nil { return err, nil } err, value3 := someFunction3(value2); if err != nil { return err, nil } return value3; }

這裡有很多可重複的代碼,這不好。 而有了大功能,情況可能會更糟! 為此,您可能需要鍵盤上的一個鍵:

鍵盤上錯誤處理代碼的幽默形象

如何改進?

我喜歡 JavaScript 在這方面的方法。 該函數可能會引發錯誤,您可以捕獲它。 考慮這個例子:

 function doStuff() { const value1 = someFunction(); const value2 = someFunction2(value1); const value3 = someFunction3(value2); return value3; } try { const value = doStuff(); // do something with it } catch (err) { // handle the error }

它更加清晰,並且不包含用於錯誤處理的可重複代碼。

圍棋中的好東西

儘管 Go 在設計上有很多缺陷,但它也有一些非常酷的特性。

1. 協程

異步編程在 Go 中變得非常簡單。 雖然多線程編程在其他語言中通常很困難,但生成一個新線程並在其中運行函數以便它不會阻塞當前線程非常簡單:

 func doSomeCalculations() { // do some CPU intensive/long running tasks } func main() { go doSomeCalculations(); // This will run in another thread; }

2. Go 捆綁的工具

雖然在其他編程語言中,您需要為不同的任務(例如測試、靜態代碼格式化等)安裝不同的庫/工具,但 Go 默認已包含許多很酷的工具,例如:

  • gofmt - 靜態代碼分析工具。 與 JavaScript 相比,您需要安裝額外的依賴項,例如eslintjshint ,這裡默認包含它。 如果你不編寫 Go 風格的代碼(不使用聲明的變量、導入未使用的包等),程序甚至不會編譯。
  • go test - 一個測試框架。 同樣,與 JavaScript 相比,您需要安裝額外的依賴項進行測試(Jest、Mocha、AVA 等)。 在這裡,它默認包含在內。 默認情況下,它允許您做很多很酷的事情,例如基準測試、將文檔中的代碼轉換為測試等。
  • godoc - 一個文檔工具。 很高興將它包含在默認工具中。
  • 編譯器本身。 與其他編譯語言相比,它的速度非常快!

3.推遲

我認為這是該語言中最好的功能之一。 想像一下,您需要編寫一個打開三個文件的函數。 如果出現問題,您將需要關閉現有打開的文件。 如果有很多這樣的結構,它看起來會一團糟。 考慮這個偽代碼示例:

 function openManyFiles() { let file1, file2, file3; try { file1 = open('path-to-file1'); } catch (err) { return; } try { file2 = open('path-to-file2'); } catch (err) { // we need to close first file, remember? close(file1); return; } try { file3 = open('path-to-file3'); } catch (err) { // and now we need to close both first and second file close(file1); close(file2); return; } // do some stuff with files // closing files after successfully processing them close(file1); close(file2); close(file3); return; }

看起來很複雜。 這就是 Go 的defer出現的地方:

 package main import ( "fmt" ) func openFiles() { // Pretending we're opening files fmt.Printf("Opening file 1\n"); defer fmt.Printf("Closing file 1\n"); fmt.Printf("Opening file 2\n"); defer fmt.Printf("Closing file 2\n"); fmt.Printf("Opening file 3\n"); // Pretend we've got an error on file opening // In real products, an error will be returned here. return; } func main() { openFiles() /* Prints: Opening file 1 Opening file 2 Opening file 3 Closing file 2 Closing file 1 */ }

如您所見,如果我們在打開第三個文件時出現錯誤,其他文件將自動關閉,因為defer語句在 return 之前以相反的順序執行。 此外,在同一個地方而不是函數的不同部分打開和關閉文件也很好。

結論

我沒有提到圍棋中所有的好和壞,只是我認為最好和最壞的事情。

Go 確實是當前使用的有趣的編程語言之一,它確實具有潛力。 它為我們提供了非常酷的工具和功能。 但是,那裡有很多可以改進的地方。

如果我們,作為 Go 開發者,將實現這些更改,它將使我們的社區受益匪淺,因為它將使使用 Go 進行編程變得更加愉快。

同時,如果您嘗試使用 Go 改進測試,請嘗試測試您的 Go 應用程序:Toptaler Gabriel Aszalos 的正確入門方法

相關:結構良好的邏輯:Golang OOP 教程