最近编写并部署了我的第一个Go程序。
在我30年的职业生涯中,我用C、C++、Visual Basic、Objective-C、TypeScript、Python、Tcl、PHP、Java、C#、Rust以及还有其他一些我已经忘记的语言编写过应用程序。
我刚刚写并部署了我的第一个 Go 应用。它是一个小小的网络服务。它只有大约1000行代码,不算复杂:将传入的 HTTP 请求转换成命令行工具的参数,然后将命令行工具的输出作为 HTTP 响应返回。这正适合用 Go 来做,我也不后悔选了它。然而,这次经历让我希望 Go 能变得更好。
我将从好的方面开始,然后谈谈不好的地方。
积极的Go的标准库真的很好。 在使用其他语言比如Typescript和Python时,我的应用有几个直接的依赖库,这些直接依赖库又带来了数百个间接依赖库。我大约10%的开发时间都花在升级依赖上了,因为某个旧版本被发现有安全漏洞,或者作者决定不再支持版本2,所有人都需要迁移到版本3。我还从未见过npm audit
能给出干净的结果。
相比之下,Go 的标准库提供了一个支持 HTTP2 的 web 服务器。这意味着相比用 Rust 或 TypeScript 构建的相同应用,Go 的依赖项少得多。此外,我查找了 Go 的漏洞数据库,发现标准库中的安全漏洞很少,而且通常只是拒绝服务。这些漏洞相比我在 JavaScript 依赖项中经常看到的漏洞要少得多,也可怕得多。
Templ真的很棒。为什么所有的模板库都不能像Templ这样工作呢?我不需要布局、主干、块以及其他在其他模板库中看到的令人困惑的抽象。ASP.NET就是例子。我只需要类型安全的函数和数据。Templ的成功在于没有让解决方案比问题本身更复杂。
我爱Goroutines。从90年代开始,我就一直是轻量级线程的粉丝。真不明白为什么这些90年代以后出现的语言居然没有采用。异步Rust一团乱。异步JavaScript经常出错。我曾经写过一段Typescript代码,忘记在异步函数前加await
,结果是花了我不少时间调试。用Goroutines,代码的流程和我脑子里的逻辑一致,而且没有操作系统的线程负担。
Go 对我应用至关重要的两个优点是快速冷启动时间和内存占用低。Go 没让我失望。
不料竟是中立的没想到我竟然没有讨厌 Go 缺乏错误处理这点。是的,我的代码中差不多每五行就有一行是 if nil != err {
。但是,即便使用了异常或者 Rust 的 Result
类型,还是很难向用户提供准确且有用的错误信息。
此外,几个月后再回头看这段代码时,我可能会以不同的方式欣赏如 if nil != err {
这样的语句。在不熟悉的代码中,特别是在使用异常抛出的语言中,错误传播和处理尤其难以理解。
在开发过程中,我的应用程序出现了异常行为并崩溃。这种情况在其他编程语言中,比如 Typescript、Rust 和 C#,会在编译时被捕捉到。我可能还犯了其他尚未被发现的错误,这些错误最终会被用户发现。这对他们来说很糟糕,对我来说也是。
我被字符串形式的代码坑了。 字符串形式的代码是指源代码中实际作为执行代码存在的字符串文字。
我按照官方的 Go 文档编写了这一行代码:mux.HandleFunc("GET /form", formHandler)
,当我请求 /form
时,formHandler
并没有被执行,而是执行了默认处理器,就像请求根目录 /
一样。为什么会这样?经过大约一个小时的调试和搜索,我才发现我安装的是 Go 1.21,但我在查阅 Go 1.23 的文档。HandleFunc()
只是从 Go 1.22 开始才支持方法指定符(我在字符串中的 GET
)。
我责怪自己将 Go 编译器和文档的版本弄混了。我责怪 Go 标准库的作者们没有在编译时发现我的错误。 实际上,编译时就能轻易发现这个错误。除了增强传递给 HandleFunc()
的参数之外,他们本可以添加一个新函数,比如:mux.HandleMethodFunc("GET", "/form", formHandler)
。这样,我的错误就能在编译时被发现,因为 1.21 版本的编译器会报错说 HandleMethodFunc()
不存在。
编译器几乎无法识别作为字符串的代码中的错误,在 Go 语言中,许多地方使用字符串形式的代码,例如将结构体序列化为 JSON 格式:
type LogEntry struct {
严重程度 string `json:"severity"`
日志消息 string `json:"message"`
}
// 该结构体用于记录日志条目,包含严重程度和日志消息。
还有哪些 bug 是以代码以字符串形式隐藏的错误,直到运行时出现错误条件时才被触发?
我解引用了 nil
,Go 程序因为这而恐慌。托尼·霍尔里称[空指针](https://en.wikipedia.org/wiki/Tony_Hoare)
是他的‘十亿美元错误’。看来 Go 语言的设计者想要超越霍尔先生的错误,于是发明了 nil
。不仅我的代码多次解引用了 nil
,我甚至不知道什么时候一个值可以是 nil
。比如,看下面这个函数:
func 内部错误判断(e Http错误) bool {
...
内部错误判断:判断给定的错误是否为内部HTTP错误。
e
可以为nil
吗?我得去看下HttpError
的定义才知道。如果是struct
,则不能为nil
;如果是接口,就可以为nil
。
如果变量 p
为 nil
,我可以调用 p.foo()
吗?可能可以,也可能引发 panic,也有可能不会。唯一知道的方法是检查 foo()
的定义,。在代码维护过程中,有人可能修改了 foo()
的定义,使其在 nil
时不会 panic,并留下了数百个调用站点,所有这些站点都在做 if p == nil {
的检查。变量是否可以为 nil
这个问题可能引发的意外事故甚至比一个坏掉的红绿灯还要多。
我在没有因为 nil 恐慌的情况下也犯了错误。 在第二次看 Francesc Compoy 关于理解 nil 的演讲 之后,我这才意识到自己在 Go 语言中写了一个反模式。我曾经这样写过代码:
func OhNo() error {
var httperr *http_error = nil
return httperr
}
func TestOhNo(t *testing.T) {
if OhNo() == nil {
fmt.Println("它是nil哦。")
} else {
fmt.Println("不是哦。")
}
}
当我调用 TestOhNo()
时,它输出了“不,并不是这样。”这是一个 Go 语言中的特性,我希望 Go 程序员能迅速了解。在学习一门新语言时,我预料到自己可能会犯这样的错误。但我认为 Go 缺乏在编译时捕获这个错误的能力。这个 bug 隐藏在我的代码中,我碰巧听了 Mr. Compoy 的演讲,运气不错,才弄清楚了错误的原因。否则,我可能要等到用户报告我的应用程序表现异常时才会发现这个 bug。
顺便说一句,go vet
都没抓到我上面提到的所有问题。
我犯的错误太多了,所以不会把Go作为首选的编程语言。我已经习惯了依靠Rust和TypeScript的编译器来帮我找到错误。我的用户其实也在不知不觉中受益,因为他们从未遇到过这些问题。我享受的一个好处就是写更少的测试:如果参数永远不可能是null
,就不需要再为参数是否是null
编写测试了。
即使有了这样的经验,仍然有一些,但不多的应用程序我会选择 Go。对于几乎每一种编程语言,我都能找到一个它作为最佳语言的应用场景。
但最终,我对 Go 的最终感觉是失望的。Go 的未来看起来非常光明。我只希望 Go 能成为更好的编程语言。我希望它能在编译时捕获更多的错误,而不是让用户在运行过程中才发现它们。其他语言能做到这点;我希望我的下一种语言也能做到这点。
共同学习,写下你的评论
评论加载中...
作者其他优质文章