面試題解析:這么一道“簡單”的 Go 題,為什么結(jié)果出乎意料
昨天在《Go語言愛好者周刊:第62期》中貼了一道 Go101 的題,原題如下:
package?main
const?s?=?"Go101.org"
//?len(s)?==?9
//?1?<9?==?512
//?512?/?128?==?4
var?a?byte?=?1?<len(s)?/?128
var?b?byte?=?1?<len(s[:])?/?128
func?main()?{
??println(a,?b)
}
答案是 4 0。
不少人對這個結(jié)果應該很吃驚,因為從答題結(jié)果看,不到一半的人答對了。而且,如果只給 var b byte = 1 << len(s[:]) / 128,沒有 a 對比,我想答對的人會更少。因為有對比,很多人雖然直覺是 4 4,但想到一定有陷阱,所以會重新思考。
好幾個群都問,為什么結(jié)果會是 4 0,希望我解釋下。因此有了此文。
這個小題涉及到幾個知識點。
len 函數(shù)的結(jié)果
要注意,len 是一個內(nèi)置函數(shù)。在官方標準庫文檔關(guān)于 len 函數(shù)[1]有這么一句:
For some arguments, such as a string literal or a simple array expression, the result can be a constant. See the Go language specification's "Length and capacity" section for details.
明確支持,當參數(shù)是字符串字面量和簡單 array 表達式,len 函數(shù)返回值是常量,這很重要。
上題中,如果 const s = "Go101.org” 改為 var s = "Go101.org" 結(jié)果又會是什么呢?
package?main
var?s?=?"Go101.org"
var?a?byte?=?1?<len(s)?/?128
var?b?byte?=?1?<len(s[:])?/?128
func?main()?{
?println(a,?b)
}
結(jié)果是 0 0。
但改為這樣:
package?main
var?s?=?[9]byte{'G',?'o',?'1',?'0',?'1',?'.',?'o',?'r',?'g'}
var?a?byte?=?1?<len(s)?/?128
var?b?byte?=?1?<len(s[:])?/?128
func?main()?{
?println(a,?b)
}
結(jié)果又是 4 0。
接著看文檔那句話的后半句,查看 Go 語言規(guī)范中關(guān)于長度和容量的說明[2]。
內(nèi)置函數(shù) len 和 cap 獲取各種類型的實參并返回一個 int 類型結(jié)果。實現(xiàn)會保證結(jié)果總是一個 int 值。
如果 s 是一個字符串常量,那么 len(s) 是一個常量 。如果 s 類型是一個數(shù)組或到數(shù)組的指針且表達式 s 不包含 信道接收 或(非常量的) 函數(shù)調(diào)用的話, 那么表達式 len(s) 和 cap(s) 是常量;這種情況下, s 是不求值的。否則的話, len 和 cap 的調(diào)用結(jié)果不是常量且 s 會被求值。
可見題目中:
var?a?byte?=?1?<len(s)?/?128
var?b?byte?=?1?<len(s[:])?/?128
第一句的 len(s) 是常量(因為 s 是字符串常量);而第二句的 len(s[:]) 不是常量。這是這兩條語句的唯一區(qū)別:兩個 len 的返回結(jié)果數(shù)值并無差異,都是 9,但一個是常量一個不是。
關(guān)于位移操作
根據(jù)上面的分析,現(xiàn)在問題的關(guān)鍵在于位移運算這里。Go 語言規(guī)范中有這么一句[3]:
The right operand in a shift expression must have integer type or be an untyped constant representable by a value of type uint. If the left operand of a non-constant shift expression is an untyped constant, it is first implicitly converted to the type it would assume if the shift expression were replaced by its left operand alone.
大意是:在位移表達式的右側(cè)的操作數(shù)必須為整數(shù)類型,或者可以被 uint 類型的值所表示的無類型的常量。如果一個非常量位移表達式的左側(cè)的操作數(shù)是一個無類型常量,那么它會先被隱式地轉(zhuǎn)換為假如位移表達式被其左側(cè)操作數(shù)單獨替換后的類型。
這里的關(guān)鍵在于常量位移表達式。根據(jù)上文的分析,1 << len(s) 是常量位移表達式,而 1 << len(s[:]) 不是。
規(guī)范上關(guān)于常量表達式中,還有這么一句[4]:
If the left operand of a constant shift expression is an untyped constant, the result is an integer constant; otherwise it is a constant of the same type as the left operand, which must be of integer type.
大意是:如果常量 位移表達式 的左側(cè)操作數(shù)是一個無類型常量,那么其結(jié)果是一個整數(shù)常量;否則就是和左側(cè)操作數(shù)同一類型的常量(必須是 整數(shù)類型 )
因此對于 var a byte = 1 << len(s) / 128,因為 1 << len(s) 是一個常量位移表達式,因此它的結(jié)果也是一個整數(shù)常量,所以是 512,最后除以 128,最終結(jié)果就是 4。
而對于 var b byte = 1 << len(s[:]) / 128,因為 1 << len(s[:]) 不是一個常量位移表達式,而做操作數(shù)是 1,一個無類型常量,根據(jù)規(guī)范定義它是 byte 類型(根據(jù):如果一個非常量位移表達式的左側(cè)的操作數(shù)是一個無類號常量,那么它會先被隱式地轉(zhuǎn)換為假如位移表達式被其左側(cè)操作數(shù)單獨替換后的類型)。
為什么是 byte 類型,大家可能還是有點暈。這要回到關(guān)于常量的說明上。
常量
常量是在編譯的時候進行計算的。在 Go 語言中,常量分兩種:無類型和有類型。Go 規(guī)范上說,字面值常量, true , false , iota 以及一些僅包含無類型的恒定操作數(shù)的 常量表達式 是無類型的。
那有類型常量是怎么來的呢?一般有兩種:顯示聲明或隱式得到。比如:
const?a?int32?=?23
const?b?float32?=?0.1
無類型常量都有一個默認類型(無類型常量的默認類型分別是 bool , rune , int , float64 , complex128 或 string)。當在上下文中需要請求該常量為一個帶類型的值時,這個 默認類型 便指向該常量隱式轉(zhuǎn)換后的類型。
所以 var b byte = 1 << len(s[:]) / 128 中,根據(jù)規(guī)范定義,1 會隱式轉(zhuǎn)換為 byte 類型,因此 1 << len(s[:]) 的結(jié)果也是 byte 類型,而 byte 類型最大只能表示 255,很顯然 512 溢出了,結(jié)果為 0,因此最后 b 的結(jié)果也是 0。
小結(jié)
一道很具迷惑性的題目引出這么多小知識點??赡苡腥艘獓姡河懻撨@些有什么用?這也太細節(jié)了。我想說的是,Go 語言規(guī)范,細節(jié)點很多,能多掌握一些沒壞處,說不定將來實際工作就遇到了類似的問題呢?!以上的知識點,很細節(jié),但我認為也是挺有價值的。
當然了,你怎么說都行,你都是對的,你開心就好!
參考資料
關(guān)于 len 函數(shù): https://docs.studygolang.com/pkg/builtin/#len
[2]關(guān)于長度和容量的說明: https://hao.studygolang.com/golang_spec.html#id221
[3]這么一句: https://docs.studygolang.com/ref/spec#Operators
[4]這么一句: https://docs.studygolang.com/ref/spec#Constant_expressions
推薦閱讀

