之前寫了一片文章《Go語言如何修復(fù)十億美金的錯誤(Null)》灸蟆。在該文中,我談到了在Go中有三種方案來解決該問題,但是都不完美雏蛮。因?yàn)樵趯懺撐臅r(shí)穷绵,Go尚不支持范型。現(xiàn)在Go已經(jīng)支持了范型墙杯,所以再來審視該問題。
備注:本文中的所有代碼均為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
}
我們在代碼中定義了一個(gè)方法getPlayerById括荡,讓我們寫幾行代碼來調(diào)用該方法高镐;
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)
}
我們傳入了一個(gè)并不存在的id,結(jié)果當(dāng)然找不到數(shù)據(jù)畸冲;所以我們對返回值進(jìn)行了是否為nil的判斷嫉髓。看起來一切正常邑闲。但是如果我們忘記做判斷了算行,后果就很嚴(yán)重了。
func main() {
id := 100
playerPtr := getPlayerById(id)
fmt.Printf("Player name is: %s with id: %d\n", playerPtr.Name, id)
}
當(dāng)我們運(yùn)行該程序苫耸,進(jìn)程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
你可能會說,怎么會忘記判斷呢褪子?但是我們不要高估了所有程序員的編程能力量淌。我在項(xiàng)目中見到過各種忘記判斷,或者判斷錯誤的代碼褐筛,每每看著都讓我哭笑不得类少。經(jīng)典的墨菲定律說:如果有可能出錯,那么就一定會出錯渔扎。用在編程中也同樣適用硫狞。當(dāng)一個(gè)方法的返回值可能為空時(shí),就一定有程序員忘記做判斷晃痴,從而導(dǎo)致程序panic残吩。
那怎么解決這個(gè)問題呢?我在Go語言如何修復(fù)十億美金的錯誤(Null)提到過三種方案倘核,但是由于當(dāng)時(shí)Go不支持范型泣侮,所以都不完美。現(xiàn)在Go已經(jīng)支持范型了紧唱,那新的方案是什么呢活尊?
簡而言之:在調(diào)用方確保數(shù)據(jù)可用之前不能獲得數(shù)據(jù)隶校。
常規(guī)的方法調(diào)用,無論數(shù)據(jù)是否可用蛹锰,都會返回給調(diào)用方一個(gè)對應(yīng)類型的值深胳。如下兩種方式所示:
- 方式一:
playerPtr := getPlayerById(100)
- 方式二:
playerPtr, exists := getPlayerById(100)
在方式一中,如果忘記判斷playerPtr是否為nil铜犬,可能導(dǎo)致panic舞终;而在方案二中,如果忘記判斷exists是否為true癣猾,也可能導(dǎo)致panic敛劝。
那么如果做到在調(diào)用方確保數(shù)據(jù)可用之前不能獲得數(shù)據(jù)呢?
解決方案
NULL 變得如此普遍以至于很多人認(rèn)為它是有必要的纷宇。NULL 在很多低級和高級語言中已經(jīng)出現(xiàn)很久了夸盟,它似乎是必不可少的,像整數(shù)運(yùn)算或者 I/O 一樣呐粘。 不是這樣的满俗!你可以擁有一個(gè)不帶 NULL 的完整的程序語言转捕。NULL 的問題是一個(gè)非數(shù)值的值作岖、一個(gè)哨兵、一個(gè)集中到其它一切的特例五芝。 相反痘儡,我們需要一個(gè)實(shí)體來包含一些信息,這些信息是關(guān)于(1)它是否包含一個(gè)值和(2)已包含的值枢步,如果存在已包含的值的話沉删。并且這個(gè)實(shí)體應(yīng)該可以“包含”任意類型。這是 Haskell 的 Maybe醉途、Java 的 Optional矾瑰、Swift 的 Optional 等的思想。 例如隘擎,在 Scala 中殴穴,Some[T]
保存一個(gè)T
類型的值。None
沒有值货葬。這兩個(gè)都是Option[T]
的子類型采幌,這兩個(gè)子類型可能保存了一個(gè)值,也可能沒有值震桶。
我用golang實(shí)現(xiàn)了一個(gè)完整的版本休傍。
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)
我們有三種方式來明確地表達(dá)我們已經(jīng)對獲取的數(shù)據(jù)的可用性有信心忙厌。
- 方式一:判斷數(shù)據(jù)不存在后直接返回顷扩;否則調(diào)用Data()方法獲取真實(shí)的數(shù)據(jù)
id := 100
playerOption := getPlayerById(id)
if !playerOption.HasNone() {
return
}
playerPtr := playerOption.Data()
- 方式二:判斷數(shù)據(jù)存在后調(diào)用Data()方法獲取真實(shí)的數(shù)據(jù)
id := 100
playerOption := getPlayerById(id)
if playerOption.HasData() {
playerPtr := playerOption.Data()
}
- 方式三:當(dāng)我們確定能夠獲得非空值時(shí),可以直接調(diào)用Data()方法獲取真實(shí)的數(shù)據(jù)慰毅;比如:我們存入數(shù)據(jù)后立即訪問隘截;
id := 1
playerPtr := getPlayerById(id).Data()
通過引入Option類型,我們可以大膽地說汹胃,在golang中我們已經(jīng)修復(fù)了一個(gè)10億美金的錯誤婶芭!