我在思考 nil 在 go 中的不同工作方式,以及有時某些東西可以同時為 nil 和非 nil。
這是一個可以是 nil 指標但不是 nil 介面的小例子。讓我們來看看這意味著什麼。
首先,go 有一個介面的概念,它與一些物件導向語言中的介面類似,但又不完全相同(根據大多數定義,go 不是 OOP)。在 Go 中,介面是一種類型,它定義了另一種類型必須實作才能滿足該介面的函數。這允許我們擁有多種具體類型,可以以不同的方式滿足介面。
例如,error 是一個具有單一方法的內建介面。看起來像這樣:
type error interface { Error() string }
任何想要用作錯誤的類型都必須有一個名為 Error 的方法,該方法傳回一個字串。例如,可以使用以下程式碼:
type ErrorMessage string func (em ErrorMessage) Error() string { return string(em) } func DoSomething() error { // Try to do something, and it fails. if somethingFailed { var err ErrorMessage = "This failed" return err } return nil } func main() { err := DoSomething() if err != nil { panic(err) } }
請注意,在此範例中,如果出現問題,DoSomething 將傳回錯誤。我們可以使用 ErrorMessage 類型,因為它具有 Error 函數,該函數傳回字串,因此實作了錯誤介面。
如果沒有發生錯誤,我們回傳 nil。
在go中,指標指向一個值,但它們也可以指向無值,在這種情況下指標為nil。例如:
var i *int = nil func main() { if i == nil { j := 5 i = &j } fmt.Println("i is", *i) }
在這種情況下,i 變數是指向 int 的指標。它一開始是一個 nil 指針,直到我們創建一個 int 並將其指向該指針。
由於使用者定義的類型可以附加函數(方法),因此我們也可以擁有指向類型的指標的函數。這是Go中很常見的做法。這也意味著指標也可以實作介面。透過這種方式,我們可以得到一個非 nil 介面的值,但仍然是一個 nil 指標。考慮以下程式碼:
type TruthGetter interface { IsTrue() bool } func PrintIfTrue(tg TruthGetter) { if tg == nil { fmt.Println("I can't tell if it's true") return } if tg.IsTrue() { fmt.Println("It's true") } else { fmt.Println("It's not true") } }
任何具有 IsTrue() bool 方法的型別都可以傳遞給 PrintIfTrue,但 nil 也可以。所以,我們可以執行 PrintIfTrue(nil) ,它會印出「我無法判斷它是否為真」。
我們也可以做一些簡單的事情,例如這樣:
type Truthy bool func (ty Truthy) IsTrue() bool { return bool(ty) } func main() { var ty Truthy = true PrintIfTrue(ty) }
這將列印“It's true”。
或者,我們可以做一些更複雜的事情,例如:
type TruthyNumber int func (tn TruthyNumber) IsTrue() bool { return tn > 0 } func main() { var tn TruthyNumber = -4 PrintIfTrue(tn) }
這將列印「這不是真的」。這些範例都不是指針,因此這些類型都不可能出現 nil,但請考慮這一點:
type TruthyPerson struct { FirstName string LastName string } func (tp *TruthyPerson) IsTrue() bool { return tp.FirstName != "" && tp.LastName != "" }
在這種情況下,TruthyPerson 不會實作 TruthGetter,但 *TruthyPerson 會實作。所以,這應該有效:
func main() { tp := &TruthyPerson{"Jon", "Grady"} PrintIfTrue(tp) }
這是有效的,因為 tp 是一個指向 TruthyPerson 的指標。然而,如果指針為零,我們就會感到恐慌。
func main() { var tp *TruthyPerson PrintIfTrue(tp) }
這會引起恐慌。但是,PrintIfTrue 中不會發生恐慌。您可能會認為這很好,因為 PrintIfTrue 檢查是否為 nil。但是,問題就在這裡。它正在針對 TruthGetter 檢查 nil。換句話說,它檢查的是 nil 接口,而不是 nil 指標。在 func (tp *TruthyPerson) IsTrue() bool 中,我們不檢查 nil。在 go 中,我們仍然可以在 nil 指標上呼叫方法,因此恐慌發生在那裡。修復實際上非常簡單。
func (tp *TruthyPerson) IsTrue() bool { if tp == nil { return false } return tp.FirstName != "" && tp.LastName != "" }
現在,我們檢查 PrintIfTrue 中是否有 nil 介面以及 func (tp *TruthyPerson) IsTrue() bool 中是否有 nil 指標。現在它會列印“這不是真的”。我們可以看到所有這些代碼都在這裡工作。
透過反射,我們可以對 PrintIfTrue 進行一些小更改,以便它可以檢查 nil 介面和 nil 指標。程式碼如下:
func PrintIfTrue(tg TruthGetter) { if tg == nil { fmt.Println("I can't tell if it's true") return } val := reflect.ValueOf(tg) k := val.Kind() if (k == reflect.Pointer || k == reflect.Chan || k == reflect.Func || k == reflect.Map || k == reflect.Slice) && val.IsNil() { fmt.Println("I can't tell if it's true") return } if tg.IsTrue() { fmt.Println("It's true") } else { fmt.Println("It's not true") } }
在這裡,我們像以前一樣首先檢查 nil 介面。接下來,我們使用反射來取得類型。除了指標之外,chan、func、map 和 slice 也可以為 nil,因此我們檢查該值是否是這些類型之一,如果是,則檢查它是否為 nil。如果是,我們也會回傳「我無法判斷這是否是真的」訊息。這可能是也可能不是您想要的,但它是一個選擇。透過此更改,我們可以執行以下操作:
func main() { var tp *TruthyPerson PrintIfTrue(tp) }
您有時可能會看到一些更簡單的建議,例如:
// Don't do this if tg == nil && reflect.ValueOf(tg).IsNil() { fmt.Println("I can't tell if it's true") return }
這效果不好有兩個原因。首先,使用反射時會產生效能開銷。如果您可以避免使用反射,那麼您可能應該這樣做。如果我們先檢查 nil 接口,那麼如果它是 nil 接口,我們就不必使用反射。
第二個原因是,如果值的型別不是可以為 nil 的型,reflect.Value.IsNil() 將會發生恐慌。這就是我們添加此類檢查的原因。如果我們沒有檢查 Kind,那麼我們會對 Truthy 和 TruthyNumber 類型感到恐慌。
因此,只要我們確保先檢查類型,現在就會列印“我無法判斷它是否為真”,而不是“這不是真的”。根據您的觀點,這可能是一種改進。這是進行此更改的完整程式碼。
本文原刊於 Dan's Musings
以上是golang:理解 nil 指標和 nil 介面之間的區別的詳細內容。更多資訊請關注PHP中文網其他相關文章!