对于你的下一个 DevOps 应用来说,Google 公司的 Go 可能是完美的语言。作为由 6 篇组成一个系列文章的第一篇,我们从 goroutines、panics 和 errors 开始深入研究 Go 语言的利与弊,因为这些利与弊涉及构建 DevOps 应用。
在这篇博客中,我们已经称赞了 Google 公司的 Go 语言用于 DevOps 应用开发的优点,而且我们也写了Go 迷你入门指南。
为了避免有人认为我们是打着 DevOps 监控平台幌子的 Google 员工(我们确实是正儿八经 DevOps 监控平台,而不是 Google 员工),我们希望深入研究 Go 语言的优缺点,特别是在涉及构建 DevOps 应用程序方面。由于我们将智能代理(软件)从 Python(构建) 转换到 Go(构建),我们有许多在 Go 语言环境下工作(开发)的经验,并且我们也有一些自认为很重要的东西(知识)来与更大的 DevOps 社区分享。
从这周开始,我们将发布由 6 篇组成一系列关于 Go 语言利和弊的文章,每一篇文章都详述了少许的内容。正如我们所做的,我们将通过链接到其它篇目来更新这篇文章:
-
Golang 对于 DevOps 之利弊第一篇:Goroutines, Channels, Panics, 和 Errors(本篇)
-
Golang 对于 DevOps 之利弊第二篇:自动接口实现,共有/私有变量
-
Golang 对于 DevOps 之利弊第三篇:速度 VS 缺少泛型
-
Golang 对于 DevOps 之利弊第四篇:打包时间与方法重载
-
Golang 对于 DevOps 之利弊第五篇:交叉编译,窗口,信号,文档和编译器
-
Golang 对于 DevOps 之利弊第六篇:Defer 语句和包依赖版本控制
如果这是你首次阅读有关 Go 的文章,并且你已经知道怎样用类 C 的语言进行编程,你应该去参考Go 语言之旅,它将大约花费你一个小时,而且介绍相当有深度。接下来的内容并不是介绍学习如何用 Go 进行编程,而是我们在用 Go 语言开发智能代理系统过程中有过的抱怨和发现的可取之处。
调用函数来引发 panic
除 0
关闭一个已经关闭的 channel
映射不存在的属性,比如 Attribute = map["This doesn’t exist"]
另一方面,error 是一个內建类型,这种类型表示能自声明为字符串类型的值。这是从 Go 源代码引用的定义:
type error interface { Error() string}
根据以上定义,这是对于为什么我们讨厌 Go 拥有 error 和 panic 的总结:
Error 是为了避免异常流,而 panic 抵消了这种作用
对于任何一种编程语言,只要拥有 error 和 panic 其中之一就足够了。至少可以说,一些编程语言兼有二者是令人沮丧的。Go 语言的开发者们不幸地跟错了潮流,错误地选择了兼有二者。
一份在流行编程语言中错误处理的抽样
Golang | panic(实际上更像 error),exceptions & segfault |
Java | exceptions |
Scala | exceptions |
Ruby | error(实际上更像 exceptions) |
Python | exceptions |
PHP | error & exceptions |
Javascript | exceptions |
C/C | error,exceptions & segfault |
Objective-C | exceptions & error |
Swift | error |
总有可能返回错误,但对语言来说可能并不必要。Go 的很多內建函数,比如访问 map 元素、从 channel 中读取数据、JSON 编码等,都需要用到错误处理。这就是为什么 Go 和 其他一些类似的语言接纳 "error" 这个设计,而像 Python 和 Scala 等语言却没有。
Go 语言官方博客再次介绍:
错误处理是重要的。该语言的设计和约定鼓励你明确地检查错误发生的地方(区别于其它语言约定中的抛出异常和有时捕获它们)。在某些情况下,这使得 Go 代码冗长,但幸运的是,你可以使用一些技术来最小化重复的错误处理。
当他们说 error 可以使 Go 代码冗长,他们没有撒谎。
所以从 Go 语言对 panic 和 error 的实现中,我们可以了解到一些什么呢?
Error 增加你的代码的大小在开始介绍之前,让我们点明我们可以对 panic 和 error 抱怨。不是我们不喜欢 error,而是我们不喜欢兼有 error 和 panic。根据语言的设计,error 可以很容易的去除,所以我们主要抱怨 error 相关的问题,而不是 panic。
在代码库大小方面,Go 应用已经很臃肿了。它的二进制文件运行速度很快,但是作为源代码,它比它需要的更加冗长。在这种冗长的基础上,还需要同时处理 panic 和 error,由此你就会明白为什么这门语言之前被称为丑陋的东西有了一个概念。在区分 panic 和 error 方面需要花费额外的努力。在那些典型的编程语言里,你只有一种处理错误的方式。那如果有 error 和 panic 呢?这让我们比在假人模特工厂里的蚊子更加抓狂!
这里是原因。
通常管理 error 的方式是 try/catch。
下面是本应该出现在我们代理软件代码中的例子:
try { this.downloadModule(moduleSettings) this.extractModule(moduleSettings) this.tidyManifestModule(moduleSettings) this.restartCommand(moduleSettings) this.cleanupModule(moduleSettings) return nil}catch e { case Exception => return e}
在这个例子中,和我们实际的代码一样,我们不关心 error 是什么或者它在哪里发生。所有我们关心的是是否有一个 error。这种方式既有意义,又能产生整洁简明的代码。
对于一个错误,你的代码的每一行都必须这么做:
if err := this.downloadModule(moduleSettings); err != nil { return err}if err := this.extractModule(moduleSettings); err != nil { return err}if err := this.tidyManifestModule(moduleSettings); err != nil { return err}if err := this.restartCommand(currentUpdate); err != nil { return err}if err := this.cleanupModule(moduleSettings); err != nil { return err}
在最糟糕的情况下,它将使你的代码库增加至三倍。三倍!不,它不是每一行 - 结构体,接口,导入库和空白行完全不受影响。所有的其他行,你知道的,有实际代码的行,都增至三倍。
在少数情况下,你想为这些做出不同。在不必要的时候,把一行代码变成三行是件很蠢的事情。没有任何理由去扩展代码库的大小。
除了 error,你还有 panic(需要应对)。如果你有引发 panic 的事物,它和在哪个函数之内发生 (panic) 没有关系.如果你有一个 panic,它将和 try/catch 做同样的事情,除了你现在需要重复代码外,你还是需要处理同样的 try/catch!
我们的代理软件中的解决方案是使用一个带有重试功能的 wrapper 函数和为 panic 和 error 而做的良好记录。然后我们在主线程和遍及代码产生的每一个 goroutine 中严格的调用它。这些代码不能在其他的任何地方运行,因为他缺失其它类库,而且它是阉割版的代码,但是它应该给你关于如何共同管理 panic 和 error 的一些启发。
package safefunc import ( "common/log" "common/timeout" "runtime/debug" "time")type RetryConfig struct { MaxTries int BaseDelay time.Duration MaxDelay time.Duration SplayFraction float64 ShutdownChannel <-chan struct{} } func DefaultRetryConfig() *RetryConfig { return &RetryConfig{ MaxTries: -1, BaseDelay: time.Second, MaxDelay: time.Minute, SplayFraction: 0.25, ShutdownChannel: nil, } } func Retry(name string, config *RetryConfig, callback func() error) { // this is stupid, but necessary. // when a function panics, that function's returns are zeros. // that's the only way to check (can't rely on a nil error during a panic) var noPanicSuccess int = 1 failedAttempts := 0 wrapped := func() (int, error) { defer func() { if err := recover(); err != nil { log.Warn.Println("Recovered panic inside", name, err) log.Debug.Println("Panic Stacktrace", string(debug.Stack())) } }() return noPanicSuccess, callback() } retryLoop: for { wrappedReturn, err := wrapped() if err != nil { log.Warn.Println("Recovered error inside", name, err) log.Debug.Println("Recovered Stacktrace", string(debug.Stack())) } else if wrappedReturn == noPanicSuccess { break retryLoop } failedAttempts if config.MaxTries > 0 && failedAttempts >= config.MaxTries { log.Trace.Println("Giving up on retrying", name, "after", failedAttempts, "attempts") break retryLoop } sleep := timeout.Delay(config.BaseDelay, failedAttempts, config.SplayFraction, config.MaxDelay) log.Trace.Println("Sleeping for", sleep, "before continuing retry loop", name) sleepChannel := time.After(sleep) select { case <-sleepChannel: case <-config.ShutdownChannel: log.Trace.Println("Shutting down retry loop", name) break retryLoop } }}
当因为 Go 有这些错误处理让你觉得代码很安全时,一个 panic 错误在运行时发生了。它不做任何检查,把各 goroutine 的所有的错误都放到堆栈里,直到最终使程序崩溃。
并非每人都讨厌 Go 中的 error 和 panic诚然,有些人可能不认为这是一个弊端。甚至在我们 Blue Matador 自己公司内部都有不同意见(联合创始人们之间有更多的意见)
有错误处理会强迫你在错误发生的时候处理它们,而不是在未来一个未知的时候。error 可以是类型检查的,所以编译器可以让你去处理它们并且警告你。Go 是为数不多支持这一特性的语言。
一些人喜欢 panic,因为它不是依靠开发者的能力来返回一个异常。try/catch 是解决这些更好的方式,并且它是在函数之外的。在异常发生时,开发人员很难跟踪到,panic 和 error 可以帮助开发者解决这个问题。
我们对 error 和 panic 的主要抱怨在 Go 中,你不得不同时处理这两者。你已经通过 try/catch 获得错误捕获的 panic,所以为什么你还要让自己必须去担心 panic 呢?
panic 不仅中断所有导致 panic 的函数调用,而且也中断线程!如果你有一个出现 panic 的线程,并且你不在那个线程捕获它,这样的话不仅那个线程停止了,而且调用它的线程也会停止,一直出现异常直到你的程序挂掉。你代码中的单个 panic 可以中断所有事情,因为它可以级联的导致整个程序执行失败。
Go 使你不得不去容忍 error 和 panic 同时存在,而你最好永远记住这个。如果你一旦忘记,你的程序可能会意外的崩溃。你可能将此归咎于那个正确写出糟糕代码的程序员,但是它真的是开发者的错误吗?当机修工拧松车上的所有螺帽,难道是司机的错误吗?
总结来说,如果一切都被 error 捕获,那太好了!如果一切都被 panic 捕获,那也很好!但是在 Go 中,必须对两者进行处理和同时容忍同时存在确实是令人沮丧的。
Golang 对于 DevOps 之利弊第二篇:自动接口实现,共有/私有变量
每隔一周,我们会发布一篇新的指南,如我们的关于 Golang 对于 DevOps 之利弊六部曲中本篇一样。下一篇是:自动接口,共有/私有变量。
作者简介Matthew 经历过标准 DevOps 监控工具需要随时待命之痛。他创立了 Blue Matador 来修复遍及 DevOps 的 Frankenstein 安装的混乱。业余时间,他喜欢开飞机,玩棋盘游戏,还有花时间陪他的妻子和三个孩子。
via: https://blog.bluematador.com/posts/golang-pros-cons-for-devops-part-1-goroutines-panics-errors/
作者:Matthew Barlocker 译者:liuxinyu123 校对:rxcai polaris1119
本文由 GCTT 原创编译,Go语言中文网 荣誉推出
本文由 GCTT 原创翻译,Go语言中文网 首发。也想加入译者行列,为开源做一些自己的贡献么?欢迎加入 GCTT!
翻译工作和译文发表仅用于学习和交流目的,翻译工作遵照 CC-BY-NC-SA 协议规定,如果我们的工作有侵犯到您的权益,请及时联系我们。
欢迎遵照 CC-BY-NC-SA 协议规定 转载,敬请在正文中标注并保留原文/译文链接和作者/译者等信息。
文章仅代表作者的知识和看法,如有不同观点,请楼下排队吐槽
,