翻译自

这个系列的博文面向那些已经熟悉go的基本语法并想要深入了解内部原理的读者。今天的博文主要介绍go源码结构和一些go编译器细节。读完这篇文章,你应该可以回答以下问题:

1. go源码的结构是什么样的?

2. go编译器是如何工作的?

3. go的node tree的基本结构是什么样的?

开始准备

当你开始学习一门新的语言时,你通常可以找到许多"helloworld"的教程、入门指导、或者关于语言概念、语法、甚至标准库的书。然而,从这些资料上无法获取到语言runtime分配的内存布局或者当你调用内部函数时生成的汇编代码。显然的,这些答案隐藏来源码之中,但是,按照我的经验,你可能花费了几个小时思考,但是却没有取得什么进展。

在这个主题,我即不会装作是一个专家,也不会尝试解释每个概念。相反,本文的目标是使你独立的识别go的源码。 开始之前,我们需要一份源码的拷贝。简单执行下面的命令。

git clone https://github.com/golang/go

注意master分支的代码已经更新了许多,因此我们使用 release-branch.go1.4 分支。

项目结构

/src

编译器

/src/cmd/gclex.cgo.y

深入GO语法

现在让我们看一下第二步。理解go编译器和语法,包含了语法的`go.y`文件是一个很好的切入点。文件的主要部分包括声明,如下:
xfndcl:
     LFUNC fndcl fnbody

fndcl:
     sym '(' oarg_type_list_ocomma ')' fnres
| '(' oarg_type_list_ocomma ')' sym '(' oarg_type_list_ocomma ')' fnres
xfndclfundclfundcksomefunction(x int, y int) int(t *SomeType) somefunction(x int, y int) intxfndclfuncLFUNCfndclfnbodynodes$$,$1,$2$$
fndcl:
      sym '(' oarg_type_list_ocomma ')' fnres
        {
          t = nod(OTFUNC, N, N);
          t->list = $3;
          t->rlist = $5;

          $$ = nod(ODCLFUNC, N, N);
          $$->nname = newname($1);
          $$->nname->ntype = t;
          declare($$->nname, PFUNC);
      }
| '(' oarg_type_list_ocomma ')' sym '(' oarg_type_list_ocomma ')' fnres
$3$5oarg_type_list_ocommafnres$$

现在你理解了语法树等基本结构。你可以实践一下。在下面的内容,我们会使用一个简单的go程序分析汇编码的生成。

深入编译器

interface
go tool 6g test.go

这会编译test.go并生成目标文件。这里6g时amd64下的编译器名字。不同的平台应该用不同的名字。有时候,我们也需要一些其他的命令行参数。例如,我们用 -W标志打印语法树的结构。 创建一个简单的go程序。 首先,创建一个简单的程序。

1  package main
  2 
  3  type I interface {
  4          DoSomeWork()
  5  }
  6 
  7  type T struct {
  8          a int
  9  }
 10 
 11  func (t *T) DoSomeWork() {
 12  }
 13 
 14  func main() {
 15          t := &T{}
 16          i := I(t)
 17          print(i)
 18  }

接着编译它

go tool 6g -W test.go
maininitinit

语法树的main函数

DCL l(15)
.   NAME-main.t u(1) a(1) g(1) l(15) x(0+0) class(PAUTO) f(1) ld(1) tc(1) used(1) PTR64-*main.T

AS l(15) colas(1) tc(1)
.   NAME-main.t u(1) a(1) g(1) l(15) x(0+0) class(PAUTO) f(1) ld(1) tc(1) used(1) PTR64-*main.T
.   PTRLIT l(15) esc(no) ld(1) tc(1) PTR64-*main.T
.   .   STRUCTLIT l(15) tc(1) main.T
.   .   .   TYPE <S> l(15) tc(1) implicit(1) type=PTR64-*main.T PTR64-*main.T

DCL l(16)
.   NAME-main.i u(1) a(1) g(2) l(16) x(0+0) class(PAUTO) f(1) ld(1) tc(1) used(1) main.I

AS l(16) tc(1)
.   NAME-main.autotmp_0000 u(1) a(1) l(16) x(0+0) class(PAUTO) esc(N) tc(1) used(1) PTR64-*main.T
.   NAME-main.t u(1) a(1) g(1) l(15) x(0+0) class(PAUTO) f(1) ld(1) tc(1) used(1) PTR64-*main.T

AS l(16) colas(1) tc(1)
.   NAME-main.i u(1) a(1) g(2) l(16) x(0+0) class(PAUTO) f(1) ld(1) tc(1) used(1) main.I
.   CONVIFACE l(16) tc(1) main.I
.   .   NAME-main.autotmp_0000 u(1) a(1) l(16) x(0+0) class(PAUTO) esc(N) tc(1) used(1) PTR64-*main.T

VARKILL l(16) tc(1)
.   NAME-main.autotmp_0000 u(1) a(1) l(16) x(0+0) class(PAUTO) esc(N) tc(1) used(1) PTR64-*main.T

PRINT l(17) tc(1)
PRINT-list
.   NAME-main.i u(1) a(1) g(2) l(16) x(0+0) class(PAUTO) f(1) ld(1) tc(1) used(1) main.I

为了简要,采取了删减版的代码。 第一个节点很简单:

DCL l(15)
.   NAME-main.t l(15) PTR64-*main.T

第一个节点是声明节点。I(15)说么这个节点在15行定义。节点引用了代表matin.t变量的节点。这个变量在main包中定义,是一个main.T类型的指针。通过15行可以看出这一点。 下一个有些复杂。

AS l(15) 
.   NAME-main.t l(15) PTR64-*main.T
.   PTRLIT l(15) PTR64-*main.T
.   .   STRUCTLIT l(15) main.T
.   .   .   TYPE l(15) type=PTR64-*main.T PTR64-*main.T
main.tmain.tmain.Tmain.Imain.i
DCL l(16)
.   NAME-main.i l(16) main.I
autotmp_0000main.t
AS l(16) tc(1)
.   NAME-main.autotmp_0000 l(16) PTR64-*main.T
.   NAME-main.t l(15) PTR64-*main.T

最后,这是关键的节点。

AS l(16) 
.   NAME-main.i l(16)main.I
.   CONVIFACE l(16) main.I
.   .   NAME-main.autotmp_0000 PTR64-*main.T
main.i
AS-init
.   AS l(16) 
.   .   NAME-main.autotmp_0003 l(16) PTR64-*uint8
.   .   NAME-go.itab.*"".T."".I l(16) PTR64-*uint8

.   IF l(16) 
.   IF-test
.   .   EQ l(16) bool
.   .   .   NAME-main.autotmp_0003 l(16) PTR64-*uint8
.   .   .   LITERAL-nil I(16) PTR64-*uint8
.   IF-body
.   .   AS l(16)
.   .   .   NAME-main.autotmp_0003 l(16) PTR64-*uint8
.   .   .   CALLFUNC l(16) PTR64-*byte
.   .   .   .   NAME-runtime.typ2Itab l(2) FUNC-funcSTRUCT-(FIELD-
.   .   .   .   .   NAME-runtime.typ·2 l(2) PTR64-*byte, FIELD-
.   .   .   .   .   NAME-runtime.typ2·3 l(2) PTR64-*byte PTR64-*byte, FIELD-
.   .   .   .   .   NAME-runtime.cache·4 l(2) PTR64-*PTR64-*byte PTR64-*PTR64-*byte) PTR64-*byte
.   .   .   CALLFUNC-list
.   .   .   .   AS l(16) 
.   .   .   .   .   INDREG-SP l(16) runtime.typ·2 G0 PTR64-*byte
.   .   .   .   .   ADDR l(16) PTR64-*uint8
.   .   .   .   .   .   NAME-type.*"".T l(11) uint8

.   .   .   .   AS l(16)
.   .   .   .   .   INDREG-SP l(16) runtime.typ2·3 G0 PTR64-*byte
.   .   .   .   .   ADDR l(16) PTR64-*uint8
.   .   .   .   .   .   NAME-type."".I l(16) uint8

.   .   .   .   AS l(16) 
.   .   .   .   .   INDREG-SP l(16) runtime.cache·4 G0 PTR64-*PTR64-*byte
.   .   .   .   .   ADDR l(16) PTR64-*PTR64-*uint8
.   .   .   .   .   .   NAME-go.itab.*"".T."".I l(16) PTR64-*uint8
AS l(16) 
.   NAME-main.i l(16) main.I
.   EFACE l(16) main.I
.   .   NAME-main.autotmp_0003 l(16) PTR64-*uint8
.   .   NAME-main.autotmp_0000 l(16) PTR64-*main.T
AS-initmain.autotmp_0003go.itab.*””.T.””.Iruntime.typ2Itabmain.Tmain.Igo.itab.*””.T.””.I

getitab函数

runtime.typ2Itab
func typ2Itab(t *_type, inter *interfacetype, cache **itab) *itab {
    tab := getitab(inter, t, false)
    atomicstorep(unsafe.Pointer(cache), unsafe.Pointer(tab))
    return tab
}

显然真正的工作是在getitab中,第二句只是一个简单的赋值。接下来分析getitab,这个函数很大,所以只截取部分。

m = 
    (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*ptrSize, 0,
    &memstats.other_sys))
    m.inter = interm._type = typ

ni := len(inter.mhdr)
nt := len(x.mhdr)
j := 0
for k := 0; k < ni; k++ {
    i := &inter.mhdr[k]
    iname := i.name
    ipkgpath := i.pkgpath
    itype := i._type
    for ; j < nt; j++ {
        t := &x.mhdr[j]
        if t.mtyp == itype && t.name == iname && t.pkgpath == ipkgpath {
            if m != nil {
                *(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*ptrSize)) = t.ifn
            }
        }
    }
}

首先分配内存,persistentalloc是go的一个内存分配器。注意,它分配的内存无法释放,会一直存在。 为什么要用它分配呢?我们看一下itab结构体。

type itab struct {
    inter  *interfacetype
    _type  *_type
    link   *itab
    bad    int32
    unused int32
    fun    [1]uintptr // variable sized
}

最后一个字段,是定义为了一个只有一个元素的数组,但其实是可变大小的。(参考gcc变长数组)。接着来看这个数组指针。这些方法和interface里的方法一样。go的作者动态分配这个内存。(当你用unsafe时分配的都是这种内存).内存的大小时结构体自身大小加上interface里的函数数量乘以指针大小

unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*ptrSize

接下来,是两个内嵌循环。首先,我们遍历所有的interface方法。对每个方法都试着找出相关的类型(在mhdr表中储存的)。这是为了检查两个方法是否匹配。 如果我们找到一个匹配的,我们吧指针储存在fun字段中。

*(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*ptrSize)) = t.ifn

一个小知识:由于方法都是按照字典序排序的,这个循环可以是O(m+n)而不是O(n*m) 最后是真正的赋值。

AS l(16) 
.   NAME-main.i l(16) main.I
.   EFACE l(16) main.I
.   .   NAME-main.autotmp_0003 l(16) PTR64-*uint8
.   .   NAME-main.autotmp_0000 l(16) PTR64-*main.T
main.imain.autotmp_0003main.autotmp_0003runtime.typ2Itabmain.i
type iface struct {
    tab  *itab
    data unsafe.Pointer
}