迷惑了,Go len() 是怎么計(jì)算出來(lái)的?
本文轉(zhuǎn)載自微信公眾號(hào)「腦子進(jìn)煎魚(yú)了」,作者陳煎魚(yú)。轉(zhuǎn)載本文請(qǐng)聯(lián)系腦子進(jìn)煎魚(yú)了公眾號(hào)。
大家好,我是煎魚(yú)。
最近看到了一個(gè)很有意思的話(huà)題,我們平時(shí)常常會(huì)用 Go 的內(nèi)置函數(shù) len 去獲取各種 map、slice 的長(zhǎng)度,那他是怎么實(shí)現(xiàn)的呢?
正當(dāng)我想去看看 len 的具體實(shí)現(xiàn)時(shí),一展身手,卻發(fā)現(xiàn)竟然是個(gè)空方法:
- func len(v Type) int
看注解也沒(méi)有 link 到其他 runtime 函數(shù),那么 len 函數(shù)是如何被調(diào)用的呢?
先前也做了一些筆記,在此分享給大家,共同進(jìn)步。
謎底
今天就由煎魚(yú)帶大家一同解開(kāi)這個(gè)謎底。既然是謎底,那就一開(kāi)始就揭開(kāi)。
其實(shí) Go 語(yǔ)言中并沒(méi)有 len 函數(shù)的具體實(shí)現(xiàn)代碼,他其實(shí)是 Go 編譯器的 "魔法" ,不是實(shí)際的函數(shù)調(diào)用。
接下來(lái)將展開(kāi)這部分,我們可以更深入地了解 Go 編譯器的內(nèi)部工作原理。
編譯器
在 Go 編譯器編譯時(shí)會(huì)解析命令行參數(shù)中指定的標(biāo)志和 Go 源文件,對(duì)解析后的 Go 包進(jìn)行類(lèi)型檢查,將函數(shù)編譯為機(jī)器代碼。代碼,最后將編譯后的包定義寫(xiě)到磁盤(pán)上。
內(nèi)部定義基本類(lèi)型、內(nèi)置函數(shù)和操作函數(shù)的階段是在 types/universe.go 當(dāng)中。同時(shí)會(huì)進(jìn)行內(nèi)置函數(shù)和具體的操作符匹配,可以明確知道內(nèi)置函數(shù) len 對(duì)應(yīng)的是 OLEN:
- var builtinFuncs = [...]struct {
- name string
- op Op
- }{
- {"append", OAPPEND},
- {"cap", OCAP},
- {"close", OCLOSE},
- {"complex", OCOMPLEX},
- {"copy", OCOPY},
- {"delete", ODELETE},
- {"imag", OIMAG},
- {"len", OLEN},
- ...
- }
在編譯時(shí),上分為五個(gè)階段進(jìn)行類(lèi)型檢查:
- 第一階段:常量、類(lèi)型、以及函數(shù)的名稱(chēng)和類(lèi)型。
- 第二階段:變量賦值、接口賦值、別名聲明。
- 第三階段:類(lèi)型檢查函數(shù)體。
- 第四階段:檢查外部聲明。
- 第五階段:檢查類(lèi)型的地圖鍵,未使用的導(dǎo)入。
如果最后一個(gè)類(lèi)型檢查階段遇到 len 函數(shù),就會(huì)轉(zhuǎn)換為 UnaryExpr 類(lèi)型,一個(gè) UnaryExpr 節(jié)點(diǎn)代表一個(gè)單數(shù)表達(dá)式,也最終就是不會(huì)成為函數(shù)調(diào)用:
- func typecheck1(n ir.Node, top int) ir.Node {
- if n, ok := n.(*ir.Name); ok {
- typecheckdef(n)
- }
- switch n.Op() {
- ...
- case ir.OCAP, ir.OLEN:
- n := n.(*ir.UnaryExpr)
- return tcLenCap(n)
- }
- }
在調(diào)用 *ir.UnaryExpr 轉(zhuǎn)換完畢后,會(huì)調(diào)用 tcLenCap,也就是 typecheck,使用 okforlen 數(shù)組來(lái)驗(yàn)證參數(shù)的合法性或發(fā)出相關(guān)錯(cuò)誤信息:
- func tcLenCap(n *ir.UnaryExpr) ir.Node {
- n.X = Expr(n.X)
- n.X = DefaultLit(n.X, nil)
- n.X = implicitstar(n.X)
- ...
- var ok bool
- if n.Op() == ir.OLEN {
- ok = okforlen[t.Kind()]
- } else {
- ok = okforcap[t.Kind()]
- }
- ...
- n.SetType(types.Types[types.TINT])
- return n
- }
經(jīng)歷過(guò)上面的步驟后在對(duì)所有內(nèi)容進(jìn)行類(lèi)型檢查后,所有函數(shù)都將排隊(duì)等待編譯:
- base.Timer.Start("be", "compilefuncs")
- fcount := int64(0)
- for i := 0; i < len(typecheck.Target.Decls); i++ {
- if fn, ok := typecheck.Target.Decls[i].(*ir.Func); ok {
- enqueueFunc(fn)
- fcount++
- }
- }
- base.Timer.AddEvent(fcount, "funcs")
- compileFunctions()
在經(jīng)過(guò)在 buildssa 和 genssa 之后,再深入幾層,就會(huì)將 AST 樹(shù)中的 len 表達(dá)式轉(zhuǎn)換為 SSA。接著我們就可以看到 Go 語(yǔ)言中的每種類(lèi)型的長(zhǎng)度是怎么獲取的。
這塊的處理對(duì)應(yīng) internal/ssagen/ssa.go 的 expr 方法,如下:
- case ir.OLEN, ir.OCAP:
- n := n.(*ir.UnaryExpr)
- switch {
- case n.X.Type().IsSlice():
- op := ssa.OpSliceLen
- if n.Op() == ir.OCAP {
- op = ssa.OpSliceCap
- }
- return s.newValue1(op, types.Types[types.TINT], s.expr(n.X))
- case n.X.Type().IsString(): // string; not reachable for OCAP
- return s.newValue1(ssa.OpStringLen, types.Types[types.TINT], s.expr(n.X))
- case n.X.Type().IsMap(), n.X.Type().IsChan():
- return s.referenceTypeBuiltin(n, s.expr(n.X))
- default: // array
- return s.constInt(types.Types[types.TINT], n.X.Type().NumElem())
- }
若是數(shù)組(array)類(lèi)型,則會(huì)調(diào)用 NumElem 方法來(lái)獲取長(zhǎng)度值:
- type Array struct {
- Elem *Type
- Bound int64
- }
- func (t *Type) NumElem() int64 {
- t.wantEtype(TARRAY)
- return t.Extra.(*Array).Bound
- }
若是字典(map)類(lèi)型或通道(channel),將會(huì)調(diào)用 referenceTypeBuiltin 方法:
- func (s *state) referenceTypeBuiltin(n *ir.UnaryExpr, x *ssa.Value) *ssa.Value {
- lenType := n.Type()
- nilValue := s.constNil(types.Types[types.TUINTPTR])
- cmp := s.newValue2(ssa.OpEqPtr, types.Types[types.TBOOL], x, nilValue)
- b := s.endBlock()
- b.Kind = ssa.BlockIf
- b.SetControl(cmp)
- b.Likely = ssa.BranchUnlikely
- bThen := s.f.NewBlock(ssa.BlockPlain)
- bElse := s.f.NewBlock(ssa.BlockPlain)
- bAfter := s.f.NewBlock(ssa.BlockPlain)
- ...
- switch n.Op() {
- case ir.OLEN:
- s.vars[n] = s.load(lenType, x)
- ...
- return s.variable(n, lenType)
- }
該函數(shù)的作用是是獲取 map 或chan 的內(nèi)存地址,并以零偏移量引用其結(jié)構(gòu)布局,就像 unsafe.Pointer(uintptr(unsafe.Pointer(s)) 一樣,返回第一個(gè)字面字段的值。
那為什么要獲取結(jié)構(gòu)體的第一個(gè)字段的值呢,應(yīng)該是和 map 和 chan 的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)有關(guān):
- type hmap struct {
- count int
- ...
- }
- type hchan struct {
- qcount uint
- ...
- }
是因?yàn)?map 和 chan 的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)的第一個(gè)字段就表示長(zhǎng)度,自然也就通過(guò)計(jì)算偏移值來(lái)獲取了。
其他的數(shù)據(jù)類(lèi)型,大家可以繼續(xù)深入代碼,再細(xì)看就好了。主要還是枚舉多同類(lèi)的數(shù)據(jù)類(lèi)型,接著調(diào)用相應(yīng)的方法。
總結(jié)
每次我們看到內(nèi)置函數(shù)時(shí),總會(huì)下意識(shí)的以為是在 runtime 內(nèi)實(shí)現(xiàn)的。看不到 runtime 內(nèi)的實(shí)現(xiàn)方法,又會(huì)以為是通過(guò)注解 link 的方式來(lái)解決的。
但需要注意,其實(shí)還有像 len 內(nèi)置函數(shù)這種直接編譯器轉(zhuǎn)換的,這也是一種不錯(cuò)的優(yōu)化方式。






























