再談Go語言如何修復(fù)十億美金的錯誤(Null)

之前寫了一片文章《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億美金的錯誤婶芭!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市着饥,隨后出現(xiàn)的幾起案子犀农,更是在濱河造成了極大的恐慌,老刑警劉巖宰掉,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件呵哨,死亡現(xiàn)場離奇詭異,居然都是意外死亡轨奄,警方通過查閱死者的電腦和手機(jī)孟害,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來挪拟,“玉大人挨务,你說我怎么就攤上這事∮褡椋” “怎么了谎柄?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長惯雳。 經(jīng)常有香客問我朝巫,道長,這世上最難降的妖魔是什么石景? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任劈猿,我火速辦了婚禮,結(jié)果婚禮上鸵钝,老公的妹妹穿的比我還像新娘糙臼。我一直安慰自己,他們只是感情好恩商,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布变逃。 她就那樣靜靜地躺著,像睡著了一般怠堪。 火紅的嫁衣襯著肌膚如雪揽乱。 梳的紋絲不亂的頭發(fā)上名眉,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天,我揣著相機(jī)與錄音凰棉,去河邊找鬼损拢。 笑死,一個(gè)胖子當(dāng)著我的面吹牛撒犀,可吹牛的內(nèi)容都是我干的福压。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼或舞,長吁一口氣:“原來是場噩夢啊……” “哼荆姆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起映凳,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤胆筒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后诈豌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體仆救,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年矫渔,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了彤蔽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,675評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蚌斩,死狀恐怖铆惑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情送膳,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布丑蛤,位于F島的核電站叠聋,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏受裹。R本人自食惡果不足惜碌补,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望棉饶。 院中可真熱鬧厦章,春花似錦、人聲如沸照藻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽幸缕。三九已至群发,卻和暖如春晰韵,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背熟妓。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工雪猪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人起愈。 一個(gè)月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓只恨,卻偏偏與公主長得像,于是被迫代替她去往敵國和親抬虽。 傳聞我的和親對象是個(gè)殘疾皇子坤次,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容