09 September 2014

目录

1. 源代码获取

Go语言代码使用Mercurial进行版本管理,所以,如果要获取最新代码,你必须先安装Mercurial。

截止2014-04-10,go版本为1.2.1。

获取最新源代码:

$ hg clone -u release https://code.google.com/p/go

并切换到默认分支:

$ hg update default

2. 源代码基础介绍

go支持的CPU有(GOARCH):

  • arm
  • amd64
  • x86

支持的OS有(GOOS):

  • linux
  • windows
  • darwin
  • plan9
  • nacl
  • netbsd
  • openbsd
  • solaris

要查看安装的go所支持的操作系统和cpu,可用go env命令:

go env 获取全部环境变量。

go env GOOS 获取当前支持的操作系统。

go env GOARCH 获取当前支持的cpu。

源代码命名风格,体系结构无关代码无特殊后缀,如zversion.go;与体系结构相关代码带有“操作系统名+CPU名”的后缀:name_GOOS_GOARCH.[go|c|s]

其中unix还可以指除了windows、plan9、nacl以外的所有操作系统。

例如:

  • Linux PC平台下启动代码:src/pkg/runtime/rt0_linux_amd64.s
  • OS X/Darwin操作系统相关代码:src/pkg/runtime/os_darwin.c

2.1. 目录结构

go
	src - 源代码
		cmd - go命令行工具
			[5|6|8]a - 汇编器
			[5|6|8]c - c编译器后端
			[5|6|8]g - go编译器后端
			[5|6|8]l - 链接器
			cc - c语言编译器前端
			cgo - cgo编译器,可编译嵌入go语言的c代码
			fix - go修复工具,将上一版本源代码修正为当前版本
			gc - go语言编译器前端
			go - go命令,命令行工具的总入口
			gofmt - go语言源代码格式化工具
			ld - 链接器
			dist - “发布工具”
			link -
			nm - 目标文件的符号查看工具
			objdump - 目标文件的反汇编器
			pack - 归档工具,将目标文件合并成.a文件
			prof -
			yacc - go语言的yacc
		pkg - 系统库
			runtime - 运行时
	test - 单元测试用例
	doc - 文档

2.2. 工具链

5,6,8分别表示ARM,AMD64,x86三种CPU。

cc,gc,ld分别为c编译器,go编译器,链接器的前端,其中的代码与体系结构无关。a/c/g/l这4个工具调用了其中的代码。

go使用了自己的汇编语言,介绍在这里http://golang.org/doc/asm,跟流行的汇编器有所不同:

  • 无论在x86还是在amd64下,寄存器名都相同,如:ax,bx,cx等。
  • 源操作数在左,目标操作数在右。

go还使用自己的c语言,与标准c有所不同,但大部分相同。

3. 编译过程

通过go源代码的编译来观察整个编译过程

源代码~/demo/main.go:

package main
 
import "fmt"
 
func main() {
	fmt.Println("Go to die!")
}

先查看编译环境:

$ go env
GOARCH="amd64"
GOBIN=""
GOCHAR="6"
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH=
GORACE=""
GOROOT="/usr/local/Cellar/go/1.2.1/libexec"
GOTOOLDIR="/usr/local/Cellar/go/1.2.1/libexec/pkg/tool/darwin_amd64"
TERM="dumb"
CC="clang"
GOGCCFLAGS="-g -O2 -fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fno-common"
CXX="clang++"
CGO_ENABLED="1"

编译源代码并查看工具链调用过程:

$ cd ~/demo
$ go build -x
WORK=/var/folders/ly/cf4crlvd603gg82rn31mdbcc0000gn/T/go-build666344221
mkdir -p $WORK/_/Users/niko/demo/_obj/
cd /Users/niko/demo
/usr/local/Cellar/go/1.2.1/libexec/pkg/tool/darwin_amd64/6g -o $WORK/_/Users/niko/demo/_obj/_go_.6 -p _/Users/niko/demo -complete -D _/Users/niko/demo -I $WORK ./main.go
/usr/local/Cellar/go/1.2.1/libexec/pkg/tool/darwin_amd64/pack grcP $WORK $WORK/_/Users/niko/demo.a $WORK/_/Users/niko/demo/_obj/_go_.6
cd .
/usr/local/Cellar/go/1.2.1/libexec/pkg/tool/darwin_amd64/6l -o demo -L $WORK $WORK/_/Users/niko/demo.a

可以看出,go build调用了三个工具执行编译、归档、链接,最后生成可执行文件:demo

再增加a.go,a_amd64.s, a_arm.s, a_amd64.c四个源文件,重新编译:

$ ls
a.go      a_amd64.c a_amd64.s a_arm.s   demo      main.go
$ go build -x
WORK=/var/folders/ly/cf4crlvd603gg82rn31mdbcc0000gn/T/go-build838843629
mkdir -p $WORK/_/Users/niko/demo/_obj/
cd /Users/niko/demo
/usr/local/Cellar/go/1.2.1/libexec/pkg/tool/darwin_amd64/6g -o $WORK/_/Users/niko/demo/_obj/_go_.6 -p _/Users/niko/demo -D _/Users/niko/demo -I $WORK ./a.go ./main.go
/usr/local/Cellar/go/1.2.1/libexec/pkg/tool/darwin_amd64/6c -F -V -w -I $WORK/_/Users/niko/demo/_obj/ -I /usr/local/Cellar/go/1.2.1/libexec/pkg/darwin_amd64 -o $WORK/_/Users/niko/demo/_obj/a_amd64.6 -D GOOS_darwin -D GOARCH_amd64 ./a_amd64.c
/usr/local/Cellar/go/1.2.1/libexec/pkg/tool/darwin_amd64/6a -I $WORK/_/Users/niko/demo/_obj/ -o $WORK/_/Users/niko/demo/_obj/a_amd64.6 -D GOOS_darwin -D GOARCH_amd64 ./a_amd64.s
/usr/local/Cellar/go/1.2.1/libexec/pkg/tool/darwin_amd64/pack grcP $WORK $WORK/_/Users/niko/demo.a $WORK/_/Users/niko/demo/_obj/_go_.6 $WORK/_/Users/niko/demo/_obj/a_amd64.6 $WORK/_/Users/niko/demo/_obj/a_amd64.6
cd .
/usr/local/Cellar/go/1.2.1/libexec/pkg/tool/darwin_amd64/6l -o demo -L $WORK $WORK/_/Users/niko/demo.a

这次处理6g还调用了6a,6c分别编译汇编、c源代码,a_arm.s没有被编译,因为_arm.s只有当GOARCH=”arm”才会被编译。

  1. [5|6|8]a,[5|6|8]g,[5|6|8]c分别编译汇编、go、c源代码,生成目标文件.[5|6|8]
  2. pack将目标文件归档为.a静态库文件。
  3. [5|6|8]g链接静态库为可执行文件。

3.1 go的语法特点

  1. 静态类型:

    意即每个变量都是有类型的,且变量类型在运行时不会改变;类似lua、js、python、ruby的动态类型语言,变量无类型,值才有类型。

  2. 强类型&类型安全:

    运行时不会出现类型错误。不允许1 + "2"之类的表达式。

  3. 不允许隐式类型转换:

    源自对c语言的反思,隐式类型转换可能导致隐藏的错误。 并非绝对不允许隐式类型转换,所有类型都能被隐式转换为interface{}类型。

  4. duck typing:

    只要方法签名与接口一致,就能被当作这个接口使用,避免繁琐的申明。

  5. 简单的支持类型推导:

    仅仅只支持变量申明的类型归一化,foo := expression语法。

    类型推导非常弱,不支持函数返回类型推导、参数类型推导;也不支持范型。

    类型归一化:已知bar, baz的类型而推导出foo的类型

     foo := bar op baz
    

    类型推导:从表达式推导出函数参数类型

     def foo(a, b) = a op b
     foo(1, 2) // => a: Int, b: Int, returnVal: Int
     foo("Hello ", "World1!") // => a: String, b: String, returnVal: String
    
  6. 为并发与I/O而专门设计了一套以goroutine为核心的语法

    goroutine本质上是用户态-轻量级-非抢占式协程。

    用户态:多个goroutine可能运行在同一个操作系统线程中,每个goroutine拥有自己的栈由runtime管理。

    非抢占式:goroutine无法抢占调度,需要显式的放弃goroutine自己的控制权才能让其他goroutine得到运行的机会。

  7. 使用函数返回值返回错误的方式

    go函数支持多返回值,一般使用内建错误类型error表示错误,使用函数的最后一个返回值返回错误。

  8. 提供defer-panic-recover类似异常的机制

    panic-recover的使用比较让人费解,panic相当于异常抛出,recover相当于异常捕捉,但recover函数必须在defer中被另一个函数调用。

     func nakedRecover() {
         defer recover() // 这样无法捕捉到panic,panic会到达最顶层,导致程序失败退出。
    	 
         panic("error")
     }
    	 
     func calleeRecover() {
         defer func() {
             // recover使用比较诡异,必须在defer中一个被调用的函数内被调用。
             recover() // 成功捕捉panic
         } ()
    	 
         panic("error")
     }
    

    go错误处理的详细讲解

  9. 使用import机制,大大加快编译速度。

    这也是现代程序设计语言的基本特征。

    源自对c语言的反思,c中使用#include预编译机制,会导致一个头文件在同一个项目中被反复打开,降低编译速度。

    go的import机制非常严格,如果import了某个库而未使用其中的任何符号,将会导致语法错误。

3.2 gc编译器实现

go目前有两个编译器,gccgo和gc,官方实现使用的是gc编译器。

gc前端源代码位于:src/cmd/gc,后端区分不同cpu实现,位于:src/cmd/[5|6|8]g

gc语法分析器使用的是yacc(bison)生成,yacc语法文件在go.y,由语法文件生成的LALR(1)语法解析器为y.tab.c,y.tab.h这两个c源文件没有可读性;如果需要研究go语法,还是以go.y为准。词法解析器没有使用自动化工具(如:lex),而是手写而成,源代码在lex.c中。这个前端实现得比较简陋,错误处理、错误信息输出都不如gcc、clang。

语法解析的结果是ast,ast节点定义在go.hstruct Node

语法解析是从函数lex.c:main开始,其中进行一些初始化后,调用yyparse开始语法解析,yyparse定义在y.tab.c中,由go.y生成。go tool 6g可以看到编译选项。

yyparse解析完成后,生成得语法树根节点保存在外部全局变量xtop中。接下来得处理分为5个阶段:

  1. 收集所有类型、方法信息,后面会依赖这些收集到得信息。
  2. 变量赋值检查,检查接口赋值,依赖阶段1。
  3. 函数体类型检查。
  4. 内联。
  5. 逃逸分析(Escape analysis)。
  6. 编译顶层函数。
  7. 检查外部定义。

由于go语法特点,不需要前向声明,因此语法解析不能流式处理,需要分析全部源代码才能获取全部的类型、函数信息;由于go是静态类型语言,需要进行类型检查,已经类型推导(类型归一化)。


哪些函数能被内联?

inl.c:caninl负责判断哪些函数能被内联; “多毛”判定,用于判断一个函数的复杂性,判定前会预先分配预算,每判定一个节点预算<0,当预算减小到小于0时,这个函数就会被判定为“多毛”而不能被内联。为内联判定分配的预算是40。

以下语法会无视预算,直接被判定为多毛:

  • 调用
    • go foo()
    • defer foo()
    • foo(args...)
    • foo()
    • self.foo()
  • “返回跳转”
    • 对尾递归的优化,将return优化为jump。
    • func foo(i int) int {
          ...
          return foo(i - 1)
      }
      
  • panic
  • recover
  • 闭包
  • range
    • for k, v := range(map)
  • for
    • for i := 0; i < N; i++
  • select
  • switch
  • proc
    •   go func () {
        ...
        } ()
      
  • defer
  • 类型声明
  • 常数声明

逃逸分析做了哪些优化?

go语言允许以下语法:

type bar struct {
    id int
    name string
}
 
func fooEsc(id int, name string) *bar {
    return &bar { id: id, name: name } // 发生逃逸
}
 
func fooNewEsc(id int, name string) *bar {
    rv := new(bar)
    return rv // 指针rv发生逃逸
}
 
func fooEcho(id int, name string) *bar {
    rv := &bar {id: id, name: name }
    fmt.Println(rv) // 指针rv未逃逸
}

go使用垃圾回收进行内存管理,如果堆上对象过多会增加垃圾回收压力。类似fooEcho的函数,使用的指针未逃逸,没有其他对象引用到它,因此可优化bar在栈上分配,当函数生存期结束时,自动释放分配的空间,减小垃圾回收的压力。

go的逃逸分析算法不仅将未逃逸对象移动到栈上,还将栈上的大对象移动到堆中。


完成上面多个阶段,再调用dumpobj生成目标文件,编译过程就结束了。

文件清单:

文件名 说明 主要内容
lex.c 词法解析文件 词法解析器,入口函数main
go.y yacc语法文件  
go.h 语法解析相关数据结构定义 struct Node
inl.c 内联算法  
typecheck.c 类型检查  
esc.c 逃逸分析算法  
walk.c 遍历并生成代码  


blog comments powered by Disqus