Golang 中字典的 Comma Ok 是如何實現的
眾所周知,Golang 中函式的返回值的數量是固定的,而不是像 Python 中那樣,函式的返回值數量是不固定的。
如果我們把 Golang 中對 map 的取值看作是一個函式的話,那麼直接取值和用 comma ok 方式取值的實現就變得很意思。
Golang 中 map 的取值方式
v1, ok := m["test"]
v2 := m2["test"]
先看看彙編是如何實現的。
package main
import "log"
func main() {
m1 := make(map[string]string)
v1, ok := m1["test"]
v2 := m1["test"]
log.Println(v1, v2, ok)
}
儲存上述檔案為 map_test.go,執行go tool compile -S map_test.go
,擷取關鍵部分
...
0x00a9 00169 (map_test.go:7) CALL runtime.mapaccess2_faststr(SB)
...
0x00f8 00248 (map_test.go:8) CALL runtime.mapaccess1_faststr(SB)
...
可以看到,雖然都是 m1["test"]
,但是卻呼叫了 runtime 中不同的方法。 可以在 go/src/runtime/map_faststr.go
檔案中看到
func mapaccess2_faststr(t *maptype, h *hmap, ky string) (unsafe.Pointer, bool) {}
func mapaccess1_faststr(t *maptype, h *hmap, ky string) unsafe.Pointer {}
這樣明顯就對上了,但是 Golang 又是如何實現把 m["test"]
替換為 mapaccess2_faststr
或者 mapaccess1_faststr
的呢?
這就涉及 Golang 的編譯過程了。檢視官方文件,我們知道編譯的過程包括:
- Parsing,包括詞法分析,語法分析,抽象語法樹的生成。
- Type-checking and AST transformations,包括型別檢查,抽象語法樹轉換。
- Generic SSA,中間程式碼生成
- Generating machine code,生成機器碼
現在我們就一步一步的看一看,m["test"]
是如何變成mapaccess2_faststr
的。(mapaccess1_faststr
同理,故不贅述)
詞法分析
詞法分析,Golang 中的詞法分析主要是通過go/src/cmd/compile/internal/syntax/scanner.go
(簡稱 scanner.go) 與 go/src/cmd/compile/internal/syntax/tokens.go
(簡稱 tokens.go) 完成的,其中,tokens.go 中定義各種字元會被轉化成什麼樣。 例如: tokens.go 中分別定義了 [
與 ]
_Lbrack // [
_Rbrack // ]
會被怎樣處理。
而在 scanner.go 中,通過一個大的 switch 處理各種字元。處理 [
與 ]
的部分程式碼如下:
switch c {
// 略過
case '[':
s.tok = _Lbrack
case ']':
s.nlsemi = true
s.tok = _Rbrack
// 略過
}
語法分析
語法分析階段會將詞法分析階段生成的轉換成各種 Expr(表示式),表示式的定義在go/src/cmd/compile/internal/syntax/nodes.go
(簡稱 nodes.go)。而 map 取值的表示式定義如下:
// X[Index]
IndexExpr struct {
X Expr
Index Expr
expr
}
之後再通過go/src/cmd/compile/internal/syntax/parser.go
(簡稱 parser.go)中的 pexpr
函式將詞法分析階段的 token 轉化為表示式。關鍵部分如下:
switch p.tok {
// 略
case _Lbrack: // 遇到一個左方括號
p.next()
p.xnest++
var i Expr
if p.tok != _Colon { // 遇到一個右方括號
i = p.expr()
if p.got(_Rbrack) {
// x[i]
t := new(IndexExpr) // 生成一個 Index表示式
t.pos = pos
t.X = x
t.Index = i
x = t
p.xnest--
break
}
}
//略
}
至此,已經將 m["key"]
轉化為一個 IndexExpr
了。
抽象語法樹生成
之後,在go/src/cmd/compile/internal/gc/noder.go
檔案中,再將 IndexExpr
轉化成一個OINDEX
型別的 node,關鍵程式碼如下:
switch expr := expr.(type) {
// 略
case *syntax.IndexExpr:
return p.nod(expr, OINDEX, p.expr(expr.X), p.expr(expr.Index))
// 略
}
其中各種操作型別的定義,如上述的OINDEX
在檔案go/src/cmd/compile/internal/gc/syntax.go
(簡稱為 syntax.go) 中,如下
OINDEX // Left[Right] (index of array or slice)
型別檢查
對於上文獲得的最後一個 OINDEX
型別的 node,他取值的物件即可能是字典,也可能是陣列、字串等。所以要對他們進行區分,而型別檢查部分就是做這方面工作的。跟本文相關的函式是go/src/cmd/compile/internal/gc/typecheck.go
(簡稱為 typecheck.go)檔案中的typecheck1
函式。其中關鍵程式碼如下:
func typecheck1(n *Node, top int) (res *Node) {
// 略
switch n.Op {
case OINDEX: // 處理 OINDEX 型別的節點
// 略過部分檢查程式碼
// 獲取 Left[Right] 中的 Left的型別
l := n.Left
t := l.Type
switch t.Etype {
default:
yyerror("invalid operation: %v (type %v does not support indexing)", n, t)
n.Type = nil
return n
case TSTRING, TARRAY, TSLICE:
// 處理 Left 是字串、陣列、切片的情況
// 略
case TMAP:
// 如果 Left 是 MAP,則把該 node 的操作變成 OINDEXMAP
n.Right = defaultlit(n.Right, t.Key())
if n.Right.Type != nil {
n.Right = assignconv(n.Right, t.Key(), "map index")
}
n.Type = t.Elem()
n.Op = OINDEXMAP
n.ResetAux()
}
}
}
繼續對操作為OINDEXMAP
(OINDEXMAP
也定義在syntax.go
中)的 node 節點進行分析。可以看到,在typecheck.go
的typecheckas2
函式中,繼續對OINDEXMAP
的節點進行分析。其中關鍵程式碼如下:
func typecheckas2(n *Node) {
// 略
cl := n.List.Len()
cr := n.Rlist.Len()
// 略
// x, ok = y
// 引數左邊是兩個,右邊是一個
if cl == 2 && cr == 1 {
switch r.Op {
case OINDEXMAP, ORECV, ODOTTYPE:
switch r.Op {
case OINDEXMAP:
// 如果操作的物件是OINDEXMAP,將其變為 OAS2MAPR
n.Op = OAS2MAPR
}
}
}
//略
}
最終,我們的v1, ok := m["test"]
的語句,變成了一個型別為OAS2MAPR
的語法樹節點。
中間程式碼生成
中間程式碼生成即將語法樹生成與機器碼無關的中間程式碼。生成中間程式碼的檔案為go/src/cmd/compile/internal/gc/walk.go
(簡稱 walk.go),與本文相關的為walk.go
檔案中的walkexpr
函式。關鍵程式碼如下:
func walkexpr(n *Node, init *Nodes) *Node {
switch n.Op {
// a,b = m[i]
case OAS2MAPR:
// 略
// from:
// a,b = m[i]
// to:
// var,b = mapaccess2*(t, m, i)
// a = *var
a := n.List.First()
// 根據 map 中 key 值型別不同以及值的長度進行優化
if w := t.Elem().Width; w <= 1024 { // 1024 must match runtime/map.go:maxZero
fn := mapfn(mapaccess2[fast], t)
r = mkcall1(fn, fn.Type.Results(), init, typename(t), r.Left, key)
} else {
fn := mapfn("mapaccess2_fat", t)
z := zeroaddr(w)
r = mkcall1(fn, fn.Type.Results(), init, typename(t), r.Left, key, z)
}
// 略
n.Rlist.Set1(r)
n.Op = OAS2FUNC
// 略
n = typecheck(n, ctxStmt)
n = walkexpr(n, init)
}
}
從上述函式我們可以看到,語法樹中操作為OAS2MAPR
的節點,最終變成了一個型別為OAS2FUNC
的節點,而OAS2FUNC
則意味著是一個函式呼叫,最終會被編譯器替換為 runtime 中的函式。
總結
我們可以看到,雖然是簡簡單單的 map 取值,Golang 的編譯器也幫我們做了很多額外的工作。同理,其實 Golang 中的 goroutines, defer, make 等等很多函式都是通過這樣的方式去處理的
原文地址在我的部落格:https://h3l.github.io/posts/golang-comma-ok/
參考資料:
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- Golang 的 goroutine 是如何實現的?Golang
- Golang channel底層是如何實現的?(深度好文)Golang
- C#中的ThenBy是如何實現的C#
- golang 中 sync.Mutex 的實現GolangMutex
- golang 中,非對稱加密的實現Golang加密
- Dubbo中的統一契約是如何實現的?
- 如何實現不完全字典比較的 helper
- Lucene字典的實現原理
- 字典(Dictionary)的javascript實現JavaScript
- Golang是如何操作excel的?GolangExcel
- python中的字典是什麼Python
- productForm是如何實現的ORM
- golang 中,對稱加密的程式碼實現Golang加密
- 關於Golang中的依賴注入實現Golang依賴注入
- Golang 心跳的實現Golang
- golang如何實現單例Golang單例
- python中的字典賦值操作怎麼實現?Python賦值
- python-字典-如何取出字典中的所有值Python
- JVM是如何實現反射的JVM反射
- 我是如何實現限流的?
- 在Golang中實現Actor模型的原始碼 - GauravGolang模型原始碼
- 如何用 Golang 的 channel 實現訊息的批次處理Golang
- 如何用 Golang 的 channel 實現訊息的批量處理Golang
- CRM的行程支援是如何實現的?行程
- python如何使用字典實現switchPython
- Golang 學習——如何判斷 Golang 介面是否實現?Golang
- ElasticSearch是如何實現分散式的?Elasticsearch分散式
- 什麼是字典?Python字典是可變的嗎?Python
- MySQL 是如何實現資料的排序的?MySql排序
- Python 雜湊表的實現——字典Python
- 深入 Python 字典的內部實現Python
- Golang可重入鎖的實現Golang
- 求助 PHP chr 的golang 實現PHPGolang
- Golang實現的IP代理池Golang
- Redis中的字典Redis
- javascript中的字典JavaScript
- 教你如何運用golang 實現陣列的隨機排序Golang陣列隨機排序
- Flutter 系統是如何實現ExpansionPanelList的Flutter