之前写了一片文章《Go语言如何修复十亿美金的错误(Null)》。在该文中,我谈到了在Go中有三种方案来解决该问题,但是都不完美。因为在写该文时,Go尚不支持范型。现在Go已经支持了范型,所以再来审视该问题。
备注:本文中的所有代码均为golang代码
让我们看看如下的代码:
package main
import "fmt"
var (
playerList = make([]*Player, 0, 16)
)
type Player struct {
Id int
Name string
}
func init() {
for i := 0; i < 16; i++ {
playerList = append(playerList, &Player{
Id: i + 1,
Name: fmt.Sprintf("Player_%d", i+1),
})
}
}
func getPlayerById(id int) *Player {
for _, v := range playerList {
if v.Id == id {
return v
}
}
return nil
}
我们在代码中定义了一个方法getPlayerById,让我们写几行代码来调用该方法;
func main() {
id := 100
playerPtr := getPlayerById(id)
if playerPtr == nil {
fmt.Printf("Player with id: %d doesn't exist\n", id)
return
}
fmt.Printf("Player name is: %s with id: %d\n", playerPtr.Name, id)
}
我们传入了一个并不存在的id,结果当然找不到数据;所以我们对返回值进行了是否为nil的判断。看起来一切正常。但是如果我们忘记做判断了,后果就很严重了。
func main() {
id := 100
playerPtr := getPlayerById(id)
fmt.Printf("Player name is: %s with id: %d\n", playerPtr.Name, id)
}
当我们运行该程序,进程panic并退出。
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x48278c]
goroutine 1 [running]:
main.main()
/home/jordan/Documents/GoProject/one_billion_dollar_mistake/main.go:36 +0x4c
exit status 2
你可能会说,怎么会忘记判断呢?但是我们不要高估了所有程序员的编程能力。我在项目中见到过各种忘记判断,或者判断错误的代码,每每看着都让我哭笑不得。经典的墨菲定律说:如果有可能出错,那么就一定会出错。用在编程中也同样适用。当一个方法的返回值可能为空时,就一定有程序员忘记做判断,从而导致程序panic。
那怎么解决这个问题呢?我在Go语言如何修复十亿美金的错误(Null)提到过三种方案,但是由于当时Go不支持范型,所以都不完美。现在Go已经支持范型了,那新的方案是什么呢?
简而言之:在调用方确保数据可用之前不能获得数据。
常规的方法调用,无论数据是否可用,都会返回给调用方一个对应类型的值。如下两种方式所示:
- 方式一:
playerPtr := getPlayerById(100)
- 方式二:
playerPtr, exists := getPlayerById(100)
在方式一中,如果忘记判断playerPtr是否为nil,可能导致panic;而在方案二中,如果忘记判断exists是否为true,也可能导致panic。
那么如果做到在调用方确保数据可用之前不能获得数据呢?
解决方案
NULL 变得如此普遍以至于很多人认为它是有必要的。NULL 在很多低级和高级语言中已经出现很久了,它似乎是必不可少的,像整数运算或者 I/O 一样。 不是这样的!你可以拥有一个不带 NULL 的完整的程序语言。NULL 的问题是一个非数值的值、一个哨兵、一个集中到其它一切的特例。 相反,我们需要一个实体来包含一些信息,这些信息是关于(1)它是否包含一个值和(2)已包含的值,如果存在已包含的值的话。并且这个实体应该可以“包含”任意类型。这是 Haskell 的 Maybe、Java 的 Optional、Swift 的 Optional 等的思想。 例如,在 Scala 中,Some[T]
保存一个T
类型的值。None
没有值。这两个都是Option[T]
的子类型,这两个子类型可能保存了一个值,也可能没有值。
我用golang实现了一个完整的版本。
option.go
import "fmt"
type Option[T any] struct {
// none and data are mutual exclusive
none bool
data T
}
func NewNoneOption[T any]() Option[T] {
return Option[T]{
none: true,
}
}
func NewDataOption[T any](data T) Option[T] {
return Option[T]{
data: data,
}
}
func (this Option[T]) HasNone() bool {
return this.none
}
func (this Option[T]) HasData() bool {
return !this.none
}
// Data returns the underlying data.
// Panic if there is no data.
func (this Option[T]) Data() T {
if this.none {
panic(fmt.Errorf("check validity first"))
}
return this.data
}
// DataOrDefault returns underlying data.
// It returns defaultData when there is no data.
func (this Option[T]) DataOrDefault(defaultData T) T {
if this.none {
return defaultData
}
return this.data
}
有了新的类型Option,我们就可以改造之前的代码,如下所示:
func getPlayerById(id int) Option[*Player] {
for _, v := range playerList {
if v.Id == id {
return NewDataOption(v)
}
}
return NewNoneOption[*Player]()
}
func main() {
id := 100
playerOption := getPlayerById(id)
if playerOption.HasData() {
playerPtr := playerOption.Data()
fmt.Printf("Player name is: %s with id: %d\n", playerPtr.Name, id)
}
}
由于getPlayerById方法返回的是Option类型,程序员再也无法错误地使用该对象了。如果我们将代码写成如下所示:
func main() {
id := 100
playerPtr := getPlayerById(id)
fmt.Printf("Player name is: %s with id: %d\n", playerPtr.Name, id)
}
代码将无法通过编译,错误信息如下:
# command-line-arguments
./main.go:46:59: playerPtr.Name undefined (type Option[*Player] has no field or method Name)
我们有三种方式来明确地表达我们已经对获取的数据的可用性有信心。
- 方式一:判断数据不存在后直接返回;否则调用Data()方法获取真实的数据
id := 100
playerOption := getPlayerById(id)
if !playerOption.HasNone() {
return
}
playerPtr := playerOption.Data()
- 方式二:判断数据存在后调用Data()方法获取真实的数据
id := 100
playerOption := getPlayerById(id)
if playerOption.HasData() {
playerPtr := playerOption.Data()
}
- 方式三:当我们确定能够获得非空值时,可以直接调用Data()方法获取真实的数据;比如:我们存入数据后立即访问;
id := 1
playerPtr := getPlayerById(id).Data()
通过引入Option类型,我们可以大胆地说,在golang中我们已经修复了一个10亿美金的错误!