golang拾遺:內置函數len的小知識

len是很經常使用的內置函數,能夠測量字符串、slice、array、channel以及map的長度/元素個數。c++

不過你真的瞭解len嗎?也許還有一些你不知道的小知識。golang

咱們來看一道GO101的題目,這題也被GO語言愛好者週刊轉載:express

package main

import "fmt"

func main() {
    var x *struct {
        s [][32]byte
    }
    
    fmt.Println(len(x.s[99]))
}

題目問你這段代碼的運行結果,選項有編譯錯誤、panic、32和0。c#

咱們分析一下,別看x的聲明定義一大長串,實際上就是定義了一個有個[][32]byte的結構體,而後x是這個結構體的指針。數組

而後咱們沒有初始化x,因此x是一個值爲nil的指針。看到這裏你也許以及有答案了,對nil指針解引用訪問它的成員s,那不就是panic嘛。即便引用x的成員合法,咱們的s也沒有初始化,訪問沒有初始化的slice也會panic。函數

然而這麼想你就錯了,代碼的實際運行結果是32!學習

爲何呢?咱們看看len的幫助文檔:優化

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.this

這句話很重要,對於結果是數組的表達式,len可能會是一個編譯期常量,並且數組類型的長度在編譯期是可知的,因此熟悉c++的朋友大概會馬上想到這樣的常量是不須要進行實際求值的,簡單類型推導便可得到。不過口說無憑,咱們看看spec裏的描述:lua

The expression len(s) is constant if s is a string constant. The expressions len(s) and cap(s) are constants if the type of s is an array or pointer to an array and the expression s does not contain channel receives or (non-constant) function calls; in this case s is not evaluated. Otherwise, invocations of len and cap are not constant and s is evaluated.
若是表達式是字符串常量那麼len(s)也是常量。若是表達式s的類型是array或者array的指針,且表達式不是channel的接收操做或是函數調用,那麼len(s)是常量,且表達式s不會被求值;不然len和cap會對s進行求值,其計算結果也不是一個常量。

其實說的很清楚了,但還有三點須要說明。

第一個是視爲常量的表達式裏爲何不能含有chan的接收操做和函數調用?

這個答案很簡單,由於這兩個操做都是使用這明確但願發生「反作用」的。特別是從chan裏接收數據,還會致使goroutine阻塞,而咱們的常量len表達式不會進行求值,這些你指望會發生的反作用便不會產生,會引起一些隱蔽的bug。

第二個是咱們注意到了函數調用前用non-constant修飾了,這是什麼意思?

按字面意思,一部分函數調用實際上是能夠在編譯期完成計算被當成常量處理的,而另外一些不能夠。

在進一步深刻以前咱們先要看看golang裏哪些東西是常量/常量表達式。

  1. 首先是各類字面量以及對字面量的類型轉換產生的值了,無需多說。
  2. 一部份內置函數:len、cap、imag、real、complex,它們在參數是常量的時候自己也是常量表達式。
  3. unsafe.Sizeof,由於類型的大小也是編譯期就能肯定的,因此它是常量表達式也很好理解。
  4. 全部的常量之間的運算(加減乘除位運算等,除了很是量表達式函數的調用)都是常量表達式。

從上面的描述裏能夠看出兩點,內置函數和unsafe.Sizeof的調用咱們能夠當作是constant function calls,全部常量表達式除了浮點數和複數表達式均可以在編譯期完成計算。而其餘函數好比用戶自定義函數的調用,雖然仍然有可能在編譯期被求值優化,但自己不屬於常量表達式。因此語言標準會加以區分。好比下面這個:

func add(x, y int) int {
    return x + y
}

func main() {
    fmt.Println(add(512, 513)) // 1025
}

若是咱們看生成的彙編,會發現求值已經完成,不須要調用add:

MOVQ    $1025, (SP)
PCDATA  $1, $0
CALL    runtime.convT64(SB)
MOVQ    8(SP), AX
XORPS   X0, X0
MOVUPS  X0, ""..autotmp_16+64(SP)
LEAQ    type.int(SB), CX
MOVQ    CX, ""..autotmp_16+64(SP)
MOVQ    AX, ""..autotmp_16+72(SP)
NOP
MOVQ    os.Stdout(SB), AX
LEAQ    go.itab.*os.File,io.Writer(SB), CX
MOVQ    CX, (SP)
MOVQ    AX, 8(SP)
LEAQ    ""..autotmp_16+64(SP), AX
MOVQ    AX, 16(SP)
MOVQ    $1, 24(SP)
MOVQ    $1, 32(SP)
NOP
CALL    fmt.Fprintln(SB)
MOVQ    80(SP), BP
ADDQ    $88, SP
RET

很明顯的,1025已經在編譯期求值了,然而add的調用不是常量表達式,因此下面的代碼會報錯:

const number = add(512, 513) // error!!!

// example.go:11:7: const initializer add(512, 513) is not a constant

spec給出的實例是調用的內置函數,內置函數也只有在參數是常量的狀況下被調用纔算作常量表達式:

const (
	c4 = len([10]float64{imag(2i)})  // imag(2i) is a constant and no function call is issued
	c5 = len([10]float64{imag(z)})   // invalid: imag(z) is a (non-constant) function call
)
var z complex128

因此len的表達式裏若是用了non-constant的函數調用,那麼就len自己不能算是常量表達式了。

這就有了最後一個疑問,題目中的x不是常量,爲何len的結果是常量呢?

標準只說表達式裏不能有chan的接收和很是量表達式的函數調用,沒說其餘的不能夠。你也能夠這麼理解,表達式都有結果值,任何值除了無類型常量(這裏顯然不是)都是要有一個肯定的類型的,只要這個類型是數組或者數組的指針,那len就能得到數組的長度,而這一切不須要s必定是常量表達式,編譯器能夠簡單推導出表達式的值的類型。不能包含non-constant function calls和chan接收是我在第一點裏解釋的,杜絕全部可能的反作用發生從而保證即便不對錶達式求值程序也是正確的,不包含這兩個操做的表達式既能夠是常量的也能夠不是,因此這裏咱們能用x.s[99]做爲len的參數。

說了這麼多,只要len的參數類型爲array或者array的指針而且符合要求,就不會進行求值,而題目裏的表達式正好知足這點,因此雖然咱們看起來是會致使panic的代碼,可是自己並未進行實際求值,所以程序能夠正常運行。另外cap也遵循一樣的規則。

最後,還有個小測驗,檢驗一下本身的學習吧:

// 如下哪些語句是正確的,哪些是錯誤的
var slice [][]*[10]int

const (
    a = len(slice[10000000000000][4]) // 1
    b = len(slice[1]) // 2
    c = len(slice) // 3
    d = len([1]int{1024}) // 4
    e = len([1]int{add(512, 512)}) // 5
    g = len([unsafe.Sizeof(slice)]int{}) // 6
    g = len([unsafe.Sizeof(slice)]int{int(unsafe.Sizeof(slice))}) // 7
)
參考

https://golang.org/ref/spec#Length_and_capacity

相關文章
相關標籤/搜索