rust golang_从Rust借来解决Go中的十亿美元错误

rust golang

panic: runtime error: invalid memory address or nil pointer dereference

如果您曾经使用过Go,则可能至少会看到一次此错误。 在某个地方,将nil指针或nil接口传递给了不处理nil的函数。 在所有情况下,这都是编程错误,函数要么应该处理nil,要么调用者不应该将nil传递给函数。 这份Go体验报告将尝试说明通常不需要nil的情况,并且被迫使用nil-able的指针和接口可能会导致生产混乱。 我们还将简要讨论Rust如何解决此问题以及如何将其解决方案应用于Go。

零可能有用

让我们首先显示为什么允许将值设为nil会有用。 nil的主要用例是指示值“缺失”。 一个很好的例子就是一些解析JSON并需要知道是否提供字段的代码。 通过使用指向int的指针,您可以区分缺少的键和值为0的值:

package main

import (
"encoding/json"
"fmt"
)

type Number struct {
N int
}

type NilableNumber struct {
N *int
}

func main() {
zeroJSON := []byte(`{"N": 0}`)
emptyJSON := []byte(`{}`)

var zeroNumber Number
json.Unmarshal(zeroJSON, &zeroNumber)
var emptyNumber Number
json.Unmarshal(emptyJSON, &emptyNumber)
fmt.Println(zeroNumber.N, emptyNumber.N) // output: 0 0

var zeroNilable NilableNumber
json.Unmarshal(zeroJSON, &zeroNilable)
var emptyNilable NilableNumber
json.Unmarshal(emptyJSON, &emptyNilable)
fmt.Println(*zeroNilable.N, emptyNilable.N) // output: 0
}

但是它有缺点

但是,尽管零可能是一个有用的概念,但它也有很多缺点。 “空引用”的发明者托尼·霍尔(Tony Hoare)甚至称其为十亿美元的错误:

空引用创建于1964年-价格多少? 少于还是超过十亿美元? 虽然我们不知道,但数额可能约为(美国)十亿美元-超过十亿分之一,不到一百亿美元。
资料来源: 托尼·霍尔(Tony Hoare)—空引用:十亿美元的错误

Go中的主要问题是,不可能有一个类型的变量来指定该变量永远不会丢失,但仍然让它成为指针或接口。

创建这样的变量会很好,因为指针和接口显然都具有其他用例,而不是编码缺少的值。 指针允许就地修改变量,而接口则允许指定抽象。 有时您需要这些用例之一,但又不想缺少一个值。 由于无法在类型系统中对此进行编码,因此您需要使用可以为nil的指针或接口类型。 然后,这会导致一个问题:代码阅读器如何知道是否允许变量为nil?

值的不同期望行为以及可用于实现它们的Go类型

最后,在所有您都不希望指针或接口为零的情况下,还有另一个问题。 该类型的零值突然变得毫无用处,因为只有在出现程序员错误时它才为零。 这样一来,就无法遵循其中一种Go谚语

使零值有用。
来源: Rob Pike — Gopherfest — 2015年11月18日

问题的例子

我创建了以下小示例来演示实际问题。 所有这些示例代码中,您永远都不希望类型为nil。 例如,当您创建一个接受接口的函数时,通常需要调用该接口在变量上定义的方法:

type Named interface {
Name() string
}

func greeting(thing Named) string {
return "Hello " + thing.Name()
}

这段代码看起来不错,但是如果您用nil来打招呼,则代码可以正常编译:

func main() {
greeting(nil)
}

但是,您将在运行时收到我们众所周知的“零指针取消引用”错误:

panic: runtime error: invalid memory address or nil pointer dereference

当使用指向用于就地修改结构的类型的指针时,也是如此。 您期望在编写如下函数时实际获得该结构的实例:

type myNumber struct {
n int
}

func plusOne(number *myNumber) {
number.n++
}

但是再次用nil调用它会编译良好,但在运行时会出错:

func main() {
var number *myNumber
plusOne(number)
}

在测试和代码审查期间,很容易找到这两个示例。 但是,零指针取消引用是导致生产中几乎所有恐慌的原因。 它们通常发生在一些很少使用的代码路径中或由于意外的输入而发生。 举一个具体的例子:我们有一个恐慌,我们想记录一个可恢复的错误,并让日志包含一个指向结构指针的字段。 但是,在执行此操作之前,我们忘记了检查指针是否为nil。 这导致了通常可以从升级到崩溃的错误。 在这种情况下,我们的代码覆盖范围也无济于事,因为测试中覆盖了代码,只是没有使用nil作为输入。

解决方法

解决此问题的一种方法是简单地记录您不应该将nil传递给函数。 一个很好的例子是Go的标准库中的`context`包。 它在文档中指出以下内容:

即使函数允许,也不要传递nil Context。 如果不确定使用哪个上下文,请传递context.TODO。
资料来源: https : //golang.org/pkg/context/

显然,这并不是一个可靠的解决方案。

另一个解决方法可能是使用静态分析解决此问题,该静态分析会在您使用之前未经检查为nil的指针或接口时警告您。 尽管理论上可以构建这样的工具,但目前尚不存在。 此外,我认为这是不可取的。 主要是因为它基本上需要成为go类型检查器的扩展。

到目前为止,我只发现了一种实际上是可靠的解决方案。 这是要在实际使用之前手动检查该值是否为nil。 这需要在所有代码中完成,而不仅仅是在边缘函数中完成,这使得在某些需要的地方容易忘记。 除此之外,它还带来另一个问题:如果为零,您会怎么做? 通常,您可能想返回一个错误,但这会使函数签名更加复杂,即使只是针对错误使用函数的极端情况。

func greeting(thing Named) (string, error) {
if thing == nil {
return "", errors.New("thing cannot be nil")
}

return "Hello " + thing.Name()
}

解?

为了解决这个问题,需要更改语言。 我不会讨论该问题的所有可能解决方案。 一方面是因为经验报告应该主要针对该问题,另一方面是因为最佳解决方案很大程度上取决于Go 2讨论的其他功能,例如泛型和求和类型。

我将展示一种可能的解决方案。 我认为这不是最好的解决方案,但是无论如何我都想展示它,因为它可以用最少的新语言功能实现,并且可以逐步集成到现有的Go代码中。 但是,它只是nil指针的解决方案,而不是nil接口。 这个想法真的很简单,Rust和C ++也使用它:添加一个永远不能为零的指针类型。 下面是一些示例代码,其中我使用`&`字符来定义一个不可空的指针,而plusOne函数现在看起来像这样:

func plusOne(number &myNumber) {
number.n++
}

然后,您将具有以下行为:

func TestNil() {
var number *myNumber
plusOne(number) // compile error: cannot use *myNumber as &myNumber, *myNumber can be nil
}

func TestPointer() {
var number *myNumber = &myNumber{n: 5}
plusOne(number) // compile error: cannot use *myNumber as &myNumber, *myNumber can be nil
}

func TestNonNilablePointer() {
var number &myNumber = &myNumber{n: 5}
plusOne(number)
fmt.Println(number.n) // output: 6
}

如果您有一个指针,则可以使用常规类型转换来获取一个不可空的指针:

func plusOnePointer(numberPointer *myNumber) error {
if numberPointer == nil {
return errors.New("number shouldn't be nil")
}

number := numberPointer.(*myNumber)
plusOne(number)

}

func TestCastedPointer() {
var number *myNumber = &myNumber{n: 5}

plusOnePointer(number) // should handle error here
fmt.Println(number.n) // output: 6
}

如果您对Rust如何解决这个问题感兴趣,那么这就是Rust中以前的代码的样子。 我认为可以从下面的Rust代码中获取其他一些想法,以使上述Go解决方案更好,但是就像我之前所说的那样,这需要进行更多更改:

结论

在某些情况下,您不希望指针或接口为零。 在这些情况下,很容易忘记检查nil,这可能导致生产代码出现混乱。 解决方法不是很健壮,或者很难严格应用。 由于所有这些,如果Go可以为从未为零的指针和接口获得语言级别的支持,那就太好了。

总结思想

Go中有更多类型可以为零。 如果某些类型在为零时正常使用,则它们仍然可以正常工作,例如切片和通道。 诸如指针之类的其他变量,如果为零,则在使用时会惊慌,就像指针和接口一样。 关于这些类型的讨论不在本文之内,这是为了使其简短,也因为我们没有遇到因使用这些类型的nil而导致生产崩溃的情况。 但是,在设计解决方案时也要牢记这些类型。

该帖子最初由 GetStream.io的 软件工程师 Jelte Fennema 撰写 原始帖子可以在 https://getstream.io/blog/fixing-the-billion-dollar-mistake-in-go-by-borrowing-from-rust/中 找到

翻译自: https://hackernoon.com/fixing-the-billion-dollar-mistake-in-go-by-borrowing-from-rust-66fab3ea715e

rust golang

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值