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 教程