题为“发现 Go 的阴暗面:为什么这种流行语言可能很糟糕”的文章讨论了对 Go 编程语言的几种批评,尽管它因其简单性和性能而广受欢迎。
两个重点:
1、为什么 Go 选择显式错误处理这条路?
Go 的错误处理就像开车时走风景优美的路线。当然,它不是最快的,但它迫使你注意沿途的每一个细节。与 Java 或 Python 等使用 try-catch 块提供结构化异常处理的语言不同,Go 使用简单类型保持低调error。这意味着错误只是值,你必须明确处理它们。
package main
import (
"errors" "fmt" )
func divide (a, b float64 ) ( float64 , error ) {
if b == 0 {
return 0 , errors.New( "不允许除以零" )
}
return a / b, nil
}
func main () {
result, err := divide( 10 , 0 )
if err != nil {
fmt.Println( "错误:" , err)
} else {
fmt.Println( "结果:" , result)
}
}
此代码片段显示了如何检查错误并妥善处理。但尽管这看起来很干净,但它也有缺点:
- 当代码的每一行似乎都需要检查错误时,您可能会开始觉得自己在玩打地鼠游戏。
- 这和Java代码每一行都要检查空null值一样。
冗长
想象一下,当你读一本小说时,每一页都以免责声明结尾。这就是 Go 的错误处理方式——冗长而重复。你需要不断编写样板代码来检查和处理错误,这可能会掩盖实际的业务逻辑。
打个比方:将 Go 中的错误处理想象成一次公路旅行,每行驶一英里,您都需要停下来检查轮胎压力。虽然这能保证安全,但并不会让驾驶过程变得刺激。
缺乏控制流中断
在具有结构化异常处理的语言中,您可以抛出异常并让其冒泡到调用函数,从而为控制流提供自然中断。另一方面,Go 要求您手动将错误传播到调用堆栈,从而导致所谓的“错误意大利面条”。
考虑一下这种场景:你调用一系列函数,每个函数都返回一个错误。你必须手动将错误向上传递,这感觉就像在玩一场看不到尽头的“传递包裹”游戏。
为什么 Go 选择显式错误处理这条路?
尽管存在这些挫折,但 Go 的设计者也有自己的理由。通过明确错误处理,Go 鼓励开发人员仔细考虑每种潜在的错误情况,从而开发出更强大的软件。这有点像强迫自己每天锻炼:虽然乏味,但最终有益。
但是,这种繁重工作可以交给编译器,类似Rust那样编译器,因为你的代码无法编译,强迫你考虑每个潜在的问题。
接受 Go 的方式。接受 Go 中的错误处理虽然冗长但有意图。利用 Go 的惯用模式(例如在检测到错误时从函数中尽早返回)来保持代码的简洁和可读性。
总体而言,虽然 Go 的错误处理系统简单直接,但对于习惯于基于异常的错误处理的开发人员来说,它也可能令人沮丧。Go 的错误处理系统缺乏上下文、冗长且容易出错,这使得编写和维护复杂系统变得更加困难。
错误处理的核心是上下文为王,但是这个概念普通程序员没有上下文感知意识,抛出的错误非常简单明了,但是背后隐藏着陷阱。
正因为Go特别强调错误处理,好像是一个面向错误处理的语言,因此,适合面向错误处理的运维场景,这也是Go适合运维、平台产品的原因之一。
2、为什么 Go 并发模型比普通 POSIX 线程更差
Go 的并发模型是围绕goroutines和channels构建的。Goroutines 是与其他函数并发运行的函数。它们就像 Go 运行时管理的轻量级线程。你可以把它们看作你最好的伙伴,随时准备介入并提供帮助,而无需进行太多设置。
你所要做的就是go在函数调用前添加关键字,然后你就有了一个 goroutine。
另一方面,通道是 Go 实现 goroutine 之间通信的方式。它们就像对讲机,允许 goroutine 相互发送和接收消息。这允许程序的不同部分之间进行协调和数据交换。
Go 并发模型的优势
- 易于使用: Goroutines 的创建和管理非常简单。您无需担心手动创建或销毁线程。
- 高效利用资源: Goroutines 是轻量级的,并且 Go 运行时可以高效地处理它们的调度。
- 内置同步:通道提供了一种强大且安全的方式来在 goroutine 之间同步和共享数据。
为什么 Go 的并发模型很糟糕
尽管 Go 有其优势,但它的并发模型也有其缺点,尤其是与 POSIX 线程相比时。
A. 缺乏控制
Go 的并发模型抽象了并发编程中涉及的大部分复杂性,但这可能会导致缺乏控制。使用 POSIX 线程,开发人员可以更精细地控制线程创建、调度和同步。这种控制对于某些高性能或低级系统编程任务至关重要。
假设场景:假设您正在开发一个实时交易系统,其中每一微秒都至关重要。使用 POSIX 线程,您可以微调线程优先级和调度策略以满足您的性能要求。在 Go 中,您只能听天由命,而 Go 调度程序可能并不总是能为您的用例做出最佳决策。
B.垃圾收集开销
Go 使用垃圾收集来管理内存,这可能会导致应用程序出现不可预测的暂停。尽管 Go 的垃圾收集器多年来一直在改进,但它仍然会导致延迟峰值,这在某些情况下是不可接受的,例如高频交易系统或实时视频处理。
类比:将 Go 的垃圾收集器想象成一位勤奋的管家,偶尔会停下一切来收拾东西。虽然这可以保持干净,但它可能会打断程序的流程,导致延迟。
C.非确定性调度
Go 调度程序决定何时以及如何调度 goroutine,这可能导致不确定的行为。这使得调试和测试并发程序更具挑战性,因为竞争条件和死锁可能难以捉摸且难以重现。
说人话:这就像大海捞针,只不过每次你寻找时,针都会不停地移动。
尽管 Go 的并发模型提供了简单性和效率,但在控制、可预测性和性能至关重要的场景中,它可能不够好。POSIX 线程虽然更复杂,但提供了某些高性能应用程序所需的粒度和确定性。了解每种方法的优缺点可以帮助开发人员根据其特定需求和约束做出明智的决策。
总之
对 Go 的主要批评
- 简单与复杂:
- Go 因其简单的语法而受到称赞,易于学习。然而,这种简单性可能会有局限性。该语言缺乏继承和泛型等高级功能,这会导致在构建复杂应用程序时代码冗长且重复。开发人员可能会发现自己编写的样板代码可以在具有泛型的语言中更优雅地处理。
-
错误处理:
- Go 的错误处理方法是另一个令人沮丧的地方。与具有结构化异常处理的语言不同,Go 使用必须显式检查的简单错误类型。这会导致代码冗长,并可能掩盖主要业务逻辑,使得管理错误变得繁琐,尤其是在复杂系统中。
-
缺乏泛型:
- 直到最近,Go 中才引入了泛型,这是一个重大缺点。虽然 Go 1.18 引入了泛型,但与其他语言相比,其实现被认为不太灵活。泛型的语法可能很冗长,类型推断能力有限,通常需要显式类型参数。
-
并发模型:
- Go 的并发模型基于 goroutines 和通道,简化了并发编程,但缺乏 POSIX 线程提供的控制。这在需要微调线程管理的性能关键型应用程序中可能会出现问题。此外,Go 的垃圾收集可能会带来延迟,其非确定性调度使调试和测试变得复杂。