Go语言的nil引发10万美元损失

摘要:在一个公司中,一位Go语言的忠实粉丝开始推动其他团队也使用Go,但由于一个新的订阅计划的插入错误,数据库中的某个字段为空,导致应用在后台任务中发生空指针异常并崩溃,进而导致整个服务宕机,造成了约10万美元的损失。引发了对Go语言的质疑。

Kotlin开发人员迅速指出,在Kotlin或JVM应用中不会出现这种情况,因为Kotlin中的空引用是明确的,并且在后台线程中发生空指针异常时,只有线程被终止,整个应用不会崩溃。

这个故事让作者对Go语言产生了怀疑,以前一直推荐给其他人使用,现在不再确定是否值得推荐了。

详细:
在我的工作中,我们有几十个开发团队,其中一小部分在使用 Go,其余的在使用 Kotlin 和 Spring。我是 Go 的忠实粉丝,而且老实说,一旦你了解了 Go,再使用 JVM(Java 虚拟机,Kotlin 应用程序在其上运行)对我来说就毫无意义了。因此,我在公司内部推动其他团队也开始使用 Go。

几个月后,维护订阅服务的团队上线了他们的第一个 Go 应用程序。它基本上是一个微服务,可以让你在调用用户 ID 时获取用户订阅信息。用户信息是在调用时从数据库中获取的,但由于我们只有几个订阅计划,因此它们会在启动时加载一次以保留在内存中,并每隔几小时在后台刷新一次。

又过了几周,我们即将上线一个新的订阅计划。它被加载到订阅服务数据库中时有一个可见标志(visible=false),稍后将通过将其设置为 "true"(并刷新应用程序中的缓存数据)来启用它。数据已在下午插入数据库,并进行了一些测试,一切看起来都很正常。

当天傍晚,也就是流量最大的时候,应用程序的实例一个接一个地触发后台任务,从数据库中重新加载订阅数据,然后崩溃。这些实例试图再次启动,但它们在启动过程中也从数据库加载了数据,结果再次崩溃。几分钟内,可用实例为零,用户的整个服务都瘫痪了。警报响起,人们被呼来唤去,支持团队非常困惑,因为已经几周没有代码变更了(所以没有什么可以回滚的),IT 团队被请来调试和修复问题。最后,我们的服务中断了一个多小时,估计收入损失约 10 万美元。

到底发生了什么?在将新订阅插入数据库时,某些信息不详并被设置为空。应用程序为这些可选字段使用了指针,在将数据库结构中的数据转换为 API 端点中使用的另一个结构时,发生了取消引用为空的情况(在后台任务中),应用程序惊慌失措并退出。当启动时,应用程序再次出现同样的 nil 问题,并立即慌乱退出。

当然,这里出了很多问题。一个缺乏经验的团队在生产中使用 Go 开发一个关键应用程序,而他们几乎没有任何经验;使用指针字段时没有进行 "无 "检查;将缓存数据插入数据库后没有手动刷新;没有运行手册来恢复数据插入(并将数据更改通知支持人员)。

但 Kotlin 的人很快就指出,在 Kotlin 或 JVM 应用程序中绝不会出现这种情况。首先,在 Kotlin 中,null 是显式的,因此不会意外发生 null 解除引用的情况(除非您在使用 Kotlin 代码的同时还使用了 Java 代码)。此外,当您在后台线程中遇到 NullPointerException 时,只有该线程会被杀死,而不是整个应用程序(即便如此,大多数运行后台任务的机制都内置了错误恢复功能,以 try...catch 的形式围绕整个任务)。

这让我大开眼界。我对 Go 有相当丰富的经验,以前还向大家推荐过它。现在我不再那么肯定了。您对此有什么看法?

背景知识:
在Go语言中,nil 是一个预定义的标识符,用于表示指针、切片、映射、通道、接口和函数等类型的零值或空值。nil 通常用于表示一个指针或接口不引用任何有效对象。以下是一些常见类型的 nil 表示:

1、指针(Pointers): 如果一个指针不指向任何有效的内存地址,它就是 nil。

var ptr *int
fmt.Println(ptr)  // 输出: <nil>

2、切片(Slices)、映射(Maps)、通道(Channels): 当切片、映射或通道没有被分配任何底层数据时,它们的零值是 nil。

var slice []int
fmt.Println(slice)  // 输出: []

var m map[string]int
fmt.Println(m)  // 输出: map[]

var ch chan int
fmt.Println(ch)  
// 输出: <nil>

3、接口(Interfaces): 一个接口在未被初始化时,其值是 nil。

var i io.Reader
fmt.Println(i)  // 输出: <nil>

4、函数(Functions): 一个未初始化的函数变量(function variable)的零值是 nil。

var f func(int) int
fmt.Println(f)  // 输出: <nil>

请注意,对于基本类型(如整数、浮点数、布尔值等),它们没有 nil 值的概念。 nil 主要用于表示指针和一些复合类型的零值。在使用 nil 值时,确保处理可能的空指针引用,以避免运行时错误。

网友评论:

  • 作为一个也从事编程语言设计的人,我必须同意他们的观点:(nil通常是零值)感觉像是 Go 设计中的大错误。过去 25 年里设计的所有其他语言都找到了这个问题的解决方案,所以我无法理解为什么 Go 的设计者决定做出这个选择。
  • 补救措施:https://github.com/uber-go/nilaway
  • Kotlin 将 null 安全性作为该语言的核心部分,而 Go 则不然(习惯用法不算在内)。
  • 我希望大多数 Go 爱好者在批评 Java 或其他语言时都采用相同的逻辑...任何语言都可以编写糟糕的代码。
  • Kotlin 不会终止整个进程,因为线程中有未捕获的异常。
  • java 中未处理的异常可能不会杀死整个应用程序,但它们可能会留下污染状态,在这种情况下,如果没有额外的处理,将逐渐杀死后台工作集合中的每个线程。
  • 在 main 中添加恢复也不起作用。如果一个 goroutine 发生恐慌,它会杀死所有 goroutine,不管你在主 goroutine 中是否恢复了。为了解决这个问题,你需要在每个 go 调用中添加恢复。