7

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

 8 months ago
source link: https://www.jdon.com/71401.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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 调用中添加恢复。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK