今天,我遇到了一個 Go FAQ。首先,作為一個小小的 Go 語言測驗,看看您是否在 Go playground 中執行該程式之前就能推斷出它應該列印出的內容(我已經將程式放在側邊欄中,以防它在 Go playground 上消失)。該程式的關鍵程式碼是:
type fake struct { io.Writer }
func fred (logger io.Writer) {
if logger != nil {
logger.Write([]byte("..."))
}
}
func main() {
var lp *fake
fred(nil)
fred(lp)
}
由於 Go 語言中的變數是使用它們的零值顯式建立的,在指標的情況下,例如 lp
將會是 nil
,您可能期待上述程式碼會正常執行(即不執行任何操作)。實際上,它會在對 fred()
的第二次呼叫時崩潰。原因是,在 Go 語言中,有時以 nil
為值的變數,如果直接列印的話,它雖然看起來像 nil
,但實際上並不是真的 nil
。簡而言之,Go 語言區別對待 nil
介面值和轉換為介面的值為 nil
的具體型別。只有前者確實為 nil
,因此與字面上的 ni l
相等,就像 fred()
在這裡做的一樣。
(因此,可以使用 nil f
呼叫 (f *fake)
上的具體方法。它也許是一個 nil
指標,但是它是型別化的 nil
指標,所以可以擁有有方法。甚至在介面轉換後依然可以擁有方法,正如上述的例子。)
對於這裡的情況,其解決方法是更改初始化的過程。實際的程式條件性地設定了 fake
,類似於下面的程式碼:
var l *sLogger
if smtplog != nil {
l = &sLogger
l.prefix = logpref
l.writer = bufio.NewWriterSize(smtplog, 4096)
}
convo = smtpd.NewConvo(conn, l)
這會將具體型別為 *sLogger
的 nil
傳遞給期望引數為 io.Writer
的物件,從而導致介面轉換並掩蓋了 nil
。為了解決這個問題,我們可以新增一個必須顯式設定的中間變數 io.Writer
:
var l2 io.Writer
if smtplog != nil {
l := &sLogger
l.prefix = logpref
l.writer = ....
l2 = l
}
convo = smtpd.NewConvo(conn, l2)
如果我們不初始化這個特殊的日誌記錄器 sLogger
,則 l2
會是一個真正的 io.Writer nil
,並會在 smtpd
包中被檢測到。
(您可以將類似的初始化操作封裝進一個返回型別為 io.Writer
的函式中,並在沒有提供日誌記錄器的情況下顯式返回 nil
,通過這樣的技巧來達到類似的效果。需要強調的一點是,函式必須返回介面型別,如果返回型別為 *sLogger
,那麼您將再次遇到相同的問題。)
在 sLogger
的方法中保留對零值的防護程式碼,這是一個個人喜好問題。然而,我不想這麼做,如果將來我在程式碼中遇到類似的初始化錯誤,我希望它崩潰,以便對其進行修復。
我從這件事中學到的另一個教訓是,如果是出於除錯的目的而進行的列印,我不會再使用 %v
作為格式說明符,而會使用 %#v
。因為前者將會為介面 nil
和具體型別的 nil
同樣列印一個普通且具有誤導性的 ,而 `%#v` 將為前者列印出
,為後者列印 (*main.fake)(nil)
。
邊注欄: 測試程式
package main
import (
"fmt"
"io"
)
type fake struct {
io.Writer
}
func fred(logger io.Writer) {
if logger != nil {
logger.Write([]byte("a test\n"))
}
}
func main() {
// 這裡的 t 的值是 nil
var t *fake
fred(nil)
fmt.Printf("passed 1\n")
fred(t)
fmt.Printf("passed 2\n")
}
via: https://utcc.utoronto.ca/~cks/space/blog/programming/GoNilNotNil
作者:ChrisSiebenmann 譯者:anxk 校對:polaris1119