内容简介:h3l · 2020-02-29 14:41:14众所周知,Golang 中函数的返回值的数量是固定的,而不是像 Python 中那样,函数的返回值数量是不固定的。如果我们把 Golang 中对 map 的取值看作是一个函数的话,那么直接取值和用 comma ok 方式取值的实现就变得很意思。
h3l · 2020-02-29 14:41:14
众所周知,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 等等很多函数都是通过这样的方式去处理的
参考资料:
以上所述就是小编给大家介绍的《Golang 中字典的 Comma Ok 是如何实现的》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Python实现创建字典
- 字典哈希表的实现原理
- Python编程实现从字典中提取子集的方法分析
- 字典与哈希表 | 自己实现Redis源代码(3)
- Go 实现字符串全排列字典序排列详解
- UWeb v1.5.4 专业版发布,完善字典组件,实现动态获取
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
JavaScript权威指南
弗拉纳根 / 东南大学出版社 / 2007-6 / 99.00元
《JavaScript权威指南(影印版)(第5版)》已经经过全面地修订和扩展,涵盖了构建当今Web2.0应用程序所需的JavaScript技术。《JavaScript权威指南(影印版)(第5版)》不仅是一本实例驱动的程序员指南,同时也是一本可以摆在桌边随时查阅的参考手册,它以全新的章节阐述了有效使用Javascript脚本所需要知道的一切,包括: 脚本化的HTTP和Ajax;XML处理;使用......一起来看看 《JavaScript权威指南》 这本书的介绍吧!