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

Null 引用一直是個(gè)壞主意,從來沒發(fā)揮過什么正面作用。
2020 年是 ALGOL 60 的 60 周年誕辰。ALGOL 60 讓結(jié)構(gòu)化編程真正落地,并為 Pascal、C 語言、B 語言和 Simula 的出現(xiàn)打下了堅(jiān)實(shí)基礎(chǔ),可以稱之為是編程語言們的“祖父”。
Null 的產(chǎn)生是由于 1965 年的一個(gè)偶然事件。
托尼·霍爾(Tony Hoare)是快速排序算法的創(chuàng)造者,也是圖靈獎(jiǎng)(計(jì)算機(jī)領(lǐng)域的諾貝爾獎(jiǎng))的獲得者。他把 Null 添加到了 ALGOL 語言中,因?yàn)樗雌饋砗軐?shí)用而且容易實(shí)現(xiàn)。但幾十年后,他后悔了。
Tony 表示,1965 年把 Null 引用加進(jìn) ALGOL W 時(shí)的想法非常簡單,“就是因?yàn)檫@很容易實(shí)現(xiàn)?!?br> 但如今再次談到當(dāng)初的決定時(shí),他表示這是個(gè)價(jià)值十億美元的大麻煩:
“ 我稱之為我的十億美元錯(cuò)誤……當(dāng)時(shí),我正在設(shè)計(jì)第一個(gè)全面的類型系統(tǒng),用于面向?qū)ο笳Z言的引用。我的目標(biāo)是確保所有對(duì)引用的使用都是絕對(duì)安全的,由編譯器自動(dòng)執(zhí)行檢查。但是我無法拒絕定義一個(gè) Null 引用的誘惑,因?yàn)樗鼘?shí)在太容易實(shí)現(xiàn)了。這導(dǎo)致了無法計(jì)數(shù)的錯(cuò)誤、漏洞和系統(tǒng)崩潰。在過去的四十年里,這些問題可能已經(jīng)造成了十億美元的損失?!?/p>

這就是這個(gè)“十億美金的錯(cuò)誤”的起源?,F(xiàn)在有些語言開始解決這個(gè)問題。

解決方案
NULL 變得如此普遍以至于很多人認(rèn)為它是有必要的。NULL 在很多低級(jí)和高級(jí)語言中已經(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è)值,也可能沒有值。

那么在Go里面如何解決這個(gè)問題呢?
我想到了三種方式,當(dāng)然都不完美,但是等到Go支持范型后,第二個(gè)方案將變得很合適。

方案一

由于Go語言支持多返回值,所以我們可以通過返回形如(Value, Exists)這樣的兩個(gè)兩個(gè)返回值;而在使用Value之前,先判斷Exists的值是否為true來決定Value值是否可用。如代碼所示:

func GetItem(id int) (Type, bool) {
    return Value,  Exists
}

value, exists := GetItem(id)
if !exists {
    fmt.Printf("Id:%d doesn't exist.", id)
    return
}

// do something with value

優(yōu)點(diǎn):簡單明了。
缺點(diǎn):有時(shí)候可能會(huì)被程序員忘記判斷第二個(gè)返回值。

方案二

不直接返回所需數(shù)據(jù),而是返回一個(gè)對(duì)象。這樣,程序員更不容易忘記對(duì)數(shù)據(jù)是否存在做判斷。如代碼所示

type Response struct {
    Value *Type
    Exists bool
}

func GetItem(id int) Response {
    return Response {
        Value,
        Exists,
    }
}

response := GetItem(id)
if !response.Exists {
    fmt.Printf("Id:%d doesn't exist.", id)
    return
}

// do something with response.Value

優(yōu)點(diǎn):相比方案一,此方案會(huì)提醒程序員去判斷值是否存在。
缺點(diǎn):程序員仍然有可能忘記判斷值是否存在。

方案三

杜絕程序員直接獲得目標(biāo)值的機(jī)會(huì),取而代之的是必須先判斷數(shù)據(jù)是否存在,然后才能獲取到數(shù)據(jù)。代碼如下所示:

type Option struct {
    value   interface{}
    exists  bool
    checked bool
}

func NewOption(value interface{}, exists bool) *Option {
    return &Option{
        value:   value,
        exists:  exists,
        checked: false,
    }
}

func (this *Option) Exists() bool {
    this.checked = true
    return this.exists
}

func (this *Option) GetValue() interface{} {
    if !this.checked {
        panic("This object has not been verified.")
    }

    return this.value
}

func GetItem(id int) *Option {
    return NewOption(value, exists)
}

option := GetItem(id)
// option.GetValue() 直接調(diào)用獲取值的方法會(huì)導(dǎo)致panic,因?yàn)榇藭r(shí)Exists屬性尚未被檢查

if !option.Exists() {
    fmt.Printf("Id:%d doesn't exist.", id)
    return
}

// do something with option.GetValue().(Type)

優(yōu)點(diǎn):對(duì)數(shù)據(jù)進(jìn)行了很好的封裝,杜絕了程序員犯錯(cuò)的可能性。
缺點(diǎn):
1、在調(diào)用GetValue()方法后需要做類型的推斷,有點(diǎn)verbose;當(dāng)Go支持范型后,此問題將可以得到解決;
2、為了處理的方便,在返回Option對(duì)象的時(shí)候,我使用了引用,這將導(dǎo)致每次調(diào)用都會(huì)在heap上創(chuàng)建一個(gè)對(duì)象,從而加大了GC的負(fù)擔(dān)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容