[译]Go 如何计算 len()..?#私藏项目实操分享#

简介: [译]Go 如何计算 len()..?#私藏项目实操分享#

Go 如何计算 len()..?

这篇文章的动机是不久前关于 Gophers Slack 的一个问题。一位开发人员想知道在哪里可以找到有关 len  的更多信息。

I want to know how the len func gets called.

我想知道如何调用 len()

人们很快就给出了正确的答案:

It doesn’t. Len is compiler magic, not an actual function call.

它没有。 Len 是编译器魔术,而不是实际的函数调用。

…  all the types len works on have the same header format, the compiler  just treats the object like a header and returns the integer  representing the length of elements

.. len 处理的所有类型都具有相同的头格式,编译器只是将对象视为头并返回表示元素长度的整数

虽然这些答案在技术上是正确的,但我认为用一个简明的解释来展开构成这个 "魔法 "的层次是很好的 这也是一个很好的小练习,可以让我们更深入地了解 Go 编译器的内部工作原理。 顺便说一下,本帖中的所有链接都指向即将发布的 Go 1.17 分支

一个小插曲

一些可能有助于理解本文其余部分的背景信息。 Go 编译器由四个主要阶段组成。你可以从 这里开始阅读它们。前两者一般称为编译器“前端”,后两者也称为编译器“后端”。

  • Parsing:源文件被标记、解析,并为每个源文件构建一个语法树。
  • AST transformations and type-checking:语法树被转换为编译器的 AST 表示,并且 AST 树被类型检查。
  • Generic SSA:AST 树被转换为静态单分配 (SSA) 形式,这是一种可以实现优化的低级中间表示。
  • Generating machine code:SSA 经历另一个特定于机器的优化过程,然后传递给汇编程序以转换为机器代码并写出最终的二进制文件。

让我们重新开始吧!

入口点

Go 编译器的入口点(不出所料)是 compile/internal/gc 包中的 main() 函数。正如文档字符串所暗示的那样,该函数负责解析 Go 源文件、对解析的 Go 包进行类型检查、将所有内容编译为机器代码并编写已编译的包定义。 早期发生的事情之一是 typecheck.InitUniverse(),它定义了基本类型、内置函数和操作数。在那里,我们看到所有内置函数如何与“操作”匹配,我们可以使用 ir.OLEN 来跟踪调用len 的步骤。

var builtinFuncs = [...]struct {
    name string
    op   ir.Op
}{
    {"append", ir.OAPPEND},
    {"cap", ir.OCAP},
    {"close", ir.OCLOSE},
    {"complex", ir.OCOMPLEX},
    {"copy", ir.OCOPY},
    {"delete", ir.ODELETE},
    {"imag", ir.OIMAG},
    {"len", ir.OLEN},
    {"make", ir.OMAKE},
    {"new", ir.ONEW},
    {"panic", ir.OPANIC},
    {"print", ir.OPRINT},
    {"println", ir.OPRINTN},
    {"real", ir.OREAL},
    {"recover", ir.ORECOVER},
}

稍后在 InitUniverse 中,可以看到 okfor 数组的初始化,它定义了各种操作数的有效类型;例如, + 运算符应该允许哪些类型:

    if types.IsInt[et] || et == types.TIDEAL {
        ...
        okforadd[et] = true
        ...
    }
    if types.IsFloat[et] {
        ...
        okforadd[et] = true
        ...
        }
    if types.IsComplex[et] {
        ...
        okforadd[et] = true
        ...
    }

以同样的方式,我们可以看到所有类型都是 len() 的有效输入:

    okforlen[types.TARRAY] = true
    okforlen[types.TCHAN] = true
    okforlen[types.TMAP] = true
    okforlen[types.TSLICE] = true
    okforlen[types.TSTRING] = true

编译器‘前端’

继续编译过程中的下一个主要步骤,我们到达了从 noder.LoadPackage(flag.Args()) 开始解析和检查输入的点。在更深的几个层次上,我们可以看到每个文件都被单独 解析,然后在五个不同的阶段进行类型检查。

Phase 1: const, type, and names and types of funcs.
Phase 2: Variable assignments, interface assignments, alias declarations.
Phase 3: Type check function bodies.
Phase 4: Check external declarations.
Phase 5: Verify map keys, unused dot imports.

一旦在最后一个类型检查阶段遇到 len 语句,它就会转换为 UnaryExpr,因为它实际上最终不会成为函数调用。 编译器隐式地获取参数的地址并使用 okforlen 数组来验证参数的合法性或发出相关的错误消息。

// typecheck1 should ONLY be called from typecheck.
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)
    }
}
// tcLenCap typechecks an OLEN or OCAP node.
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()]
    }
    if !ok {
        base.Errorf("invalid argument %L for %v", l, n.Op())
        n.SetType(nil)
        return n
    }
    n.SetType(types.Types[types.TINT])
    return n
}

回到主编译器流程,在对所有内容进行类型检查后,所有函数都将 排队等待编译。 在 compileFunctions() 中,队列中的每个元素都通过 ssagen.Compile

    compile = func(fns []*ir.Func) {
        wg.Add(len(fns))
        for _, fn := range fns {
            fn := fn
            queue(func(worker int) {
                ssagen.Compile(fn, worker)
                compile(fn.Closures)
                wg.Done()
            })
        }
    }
    ...
    compile(compilequeue)

在几层深的地方,在 buildssa 和 genssa 之后,我们终于可以将 AST 树中的 len 表达式转换为 SSA。 在这一点上很容易看到每个可用类型是如何处理的!

// expr converts the expression n to ssa, adds it to s and returns the ssa result.
func (s *state) expr(n ir.Node) *ssa.Value {
    ...
    switch n.Op() {
    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())
        }
        ...
    }
    ...
}

数组

对于数组,我们仅根据输入数组的 NumElem() 方法返回一个常量整数,该方法仅访问输入数组的 Bound 字段。

// Array contains Type fields specific to array types.
type Array struct {
    Elem  *Type // element type
    Bound int64 // number of elements; <0 if unknown yet
}
func (t *Type) NumElem() int64 {
    t.wantEtype(TARRAY)
    return t.Extra.(*Array).Bound
}

切片,字符串

对于切片和字符串,我们必须看看 ssa.OpSliceLen 和 ssa.OpStringLen 是如何处理的。 当这些调用中的任何一个在后期扩展阶段和 rewriteSelect 方法中被降低时,切片和字符串将被递归遍历以使用诸如 offset+x.ptrSize 之类的指针算法来找出它们的大小

func (x *expandState) rewriteSelect(leaf *Value, selector *Value, offset int64, regOffset Abi1RO) []*LocalSlot {
    switch selector.Op {
    ...
    case OpStringLen, OpSliceLen:
        ls := x.rewriteSelect(leaf, selector.Args[0], offset+x.ptrSize, regOffset+RO_slice_len)
        locs = x.splitSlots(ls, ".len", x.ptrSize, leafType)
    ...
    }
    return locs

映射、通道

最后,对于映射和通道,我们使用  referenceTypeBuiltin 辅助工具。它的内部工作原理有点神奇,但它最终做的是获取 map/chan  参数的地址,并以零偏移量引用其结构布局,就像 unsafe.Pointer(uintptr(unsafe.Pointer(s))  一样,最终返回第一个结构体的值。

// referenceTypeBuiltin generates code for the len/cap builtins for maps and channels.
func (s *state) referenceTypeBuiltin(n *ir.UnaryExpr, x *ssa.Value) *ssa.Value {
    if !n.X.Type().IsMap() && !n.X.Type().IsChan() {
        s.Fatalf("node must be a map or a channel")
    }
    // if n == nil {
    //   return 0
    // } else {
    //   // len
    //   return *((*int)n)
    //   // cap
    //   return *(((*int)n)+1)
    // }
    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:
        // length is stored in the first word for map/chan
        s.vars[n] = s.load(lenType, x)
    ...
    return s.variable(n, lenType)
}

hmap 和 hchan 结构的定义表明,它们的第一个字段确实包含我们需要的 len,即分别是实时地图单元和通道队列数据。

type hmap struct {
    count     int // # live cells == size of map.  Must be first (used by len() builtin)
    flags     uint8
    B         uint8
    noverflow uint16
    hash0     uint32
    buckets    unsafe.Pointer
    oldbuckets unsafe.Pointer
    nevacuate  uintptr
    extra *mapextra
}
type hchan struct {
    qcount   uint           // total data in the queue
    dataqsiz uint
    buf      unsafe.Pointer
    elemsize uint16
    closed   uint32
    elemtype *_type
    sendx    uint
    recvx    uint
    recvq    waitq
    sendq    waitq
    lock mutex
}

最后的话

Aaaand 就是这样!这篇文章没有我想象的那么长;我只是希望它对你也很有趣。 我对 Go  编译器的内部工作没有什么经验,所以有些东西可能是不对的。另外,很多东西在不久的将来都会发生变化,特别是泛型和新的类型系统会在接下来的几个 Go 版本中出现,但至少我希望我提供了一种方法,你可以用它来开始自己的挖掘工作。 在任何情况下,请不要犹豫,就新帖子发表评论、建议、想法或简单地谈论 Go! 下次再见,再见!

相关文章
|
4天前
|
搜索推荐 Go 开发者
Go模块与依赖管理:构建稳定、可维护的项目生态
【2月更文挑战第9天】Go模块是Go语言从1.11版本开始引入的一个新的依赖管理工具,它改变了以往通过GOPATH管理项目依赖的方式,为Go开发者带来了更加灵活、高效的依赖管理方式。本文将深入探讨Go模块与依赖管理的概念、使用方法和最佳实践,帮助读者更好地理解和应用Go模块,构建稳定、可维护的项目生态。
|
7月前
|
Linux Go Windows
Go 项目使用 Makefile
Go 项目使用 Makefile
22 0
|
7月前
|
负载均衡 Go 数据库
Go 语言基于 Go kit 开发 Web 项目
Go 语言基于 Go kit 开发 Web 项目
48 0
|
7月前
|
前端开发 关系型数据库 Go
Go语言学习路线 - 5.基础篇:从一个web项目来谈Go语言的技能点
经过了 入门篇 的学习,大家已经初步了解Go语言的语法,也能写常见的代码了。接下来,我们就从一个Web项目入手,看看一些常见的技能与知识吧。 我们先简单地聊一下这个Web项目的背景:我们要做的是一个简单的web系统 ,有前端同学负责界面的开发,后端不会考虑高并发等复杂情况。
68 0
|
8月前
|
前端开发 应用服务中间件 持续交付
Dokcer + nginx + Gitee Go 实现一键化部署你的项目(保姆级别)
Dokcer + nginx + Gitee Go 实现一键化部署你的项目(保姆级别)
147 0
|
4天前
|
Go 开发者
Golang深入浅出之-Go语言项目构建工具:Makefile与go build
【4月更文挑战第27天】本文探讨了Go语言项目的构建方法,包括`go build`基本命令行工具和更灵活的`Makefile`自动化脚本。`go build`适合简单项目,能直接编译Go源码,但依赖管理可能混乱。通过设置`GOOS`和`GOARCH`可进行跨平台编译。`Makefile`适用于复杂构建流程,能定义多步骤任务,但编写较复杂。在选择构建方式时,应根据项目需求权衡,从`go build`起步,逐渐过渡到Makefile以实现更高效自动化。
29 2
|
4天前
|
运维 关系型数据库 MySQL
Serverless 应用引擎产品使用之在阿里函数计算中,部署Go项目可以区分环境如何解决
阿里云Serverless 应用引擎(SAE)提供了完整的微服务应用生命周期管理能力,包括应用部署、服务治理、开发运维、资源管理等功能,并通过扩展功能支持多环境管理、API Gateway、事件驱动等高级应用场景,帮助企业快速构建、部署、运维和扩展微服务架构,实现Serverless化的应用部署与运维模式。以下是对SAE产品使用合集的概述,包括应用管理、服务治理、开发运维、资源管理等方面。
19 0
|
4天前
|
数据采集 监控 算法
Go语言性能监控:深入理解与实操
【2月更文挑战第18天】本文旨在深入探讨Go语言性能监控的核心概念、方法和实操技巧。我们将从性能监控的重要性出发,介绍Go语言性能监控的常用工具和技术,并结合实际案例,详细阐述如何进行性能监控和数据分析,为开发者提供一套完整的性能监控解决方案。
|
7月前
|
Linux Go Docker
Go 语言怎么使用 Docker 部署项目?
Go 语言怎么使用 Docker 部署项目?
160 0
|
4天前
|
设计模式 测试技术 Go
Go 项目必备:Wire 依赖注入工具的深度解析与实战应用
在现代软件开发中,依赖注入(Dependency Injection,简称 DI)已经成为一种广泛采用的设计模式。它的核心思想是通过外部定义的方式,将组件之间的依赖关系解耦,从而提高代码的可维护性、可扩展性和可测试性。然而,随着项目规模的增长,手动管理复杂的依赖关系变得日益困难。这时,依赖注入代码生成工具就显得尤为重要。在众多工具中,Wire 以其简洁、强大和易用性脱颖而出,成为 Go 语言项目中的宠儿。本文将带你深入了解 Wire 的安装、基本使用、核心概念以及高级用法,并通过一个实际的 web 博客项目示例,展示如何利用 Wire 简化依赖注入的实现。准备好了吗?让我们开始这场代码解耦的奇