[Journey with golang] 8. Project Management

本文介绍一些关于golang工程管理相关的东西。首先介绍golang一些重要的环境变量,有关golang的环境变量可以用以下命令查看: go env

  • $GOROOT:golang安装根目录。Linux下默认是/usr/lib/go。如果$GOROOT位于上述位置,则不需要显式设置该环境变量,反之需要设置。
  • $GOPATH:golang工作目录(workspace)。默认值为$HOME/go。
  • $GOBIN:是带有main函数的源程序执行 go install 时生成的可执行程序安装目录,默认是$GOPATH/bin。
  • $GOOS和$GOARCH:用于设置目标平台操作系统和目标平台CPU体系结构。这两个环境变量主要用在交叉编译中。
  • $GOPROXY:用于设置代理。部分go源码包会被墙,可以通过如下设置解决:
    # 此设置依赖go版本为1.11或以上
    # 打开.bashrc或其他shell配置文件
    export GO111MODULE=on
    export GOPROXY=https://goproxy.io

$GOPATH环境变量所指定的目录称为go的工作目录,$GOPATH可以配置多个目录,工作目录有相同的目录结构,包含src/pkg/bin三个子目录。

src是工程的源码所在目录,一般src下的第一层目录是工程根目录,工程根目录一般采用公司的域名+工程名或用户名的格式,比如常见的github上的工程源代码组织形式:

$GOPATH/src/github.com/github/
$GOPATH/src/github.com/golang/

工程根目录下才是工程各个项目的目录,项目目录下可以是其源代码文件和各种包的源码,这是一种推荐的代码组织形式。举个例子:$GOPATH/src/github.com/github/go-ost。$GOPATH/src/github.com/github/是github工程根目录,go-ost是具体的项目目录,gh-ost内是该项目的源代码和包。

$GOPATH环境变量可以配置多个目录,使用go get下载第三方的包时,默认会将包下载到$GOPATH的第一个目录里面,很多人喜欢在$GOPATH里配置两个目录,第一个专门用于下载第三方包,第二个目录用于内部工程目录,但官方推荐的做法是只配置一个目录,通过dep来管理。

下面介绍golang的交叉编译方法。golang对交叉编译有很好的支持,唯一的不足是不支持CGO。在go1.4及以前版本中,由于编译器是使用C语言写的,交叉编译比较麻烦,先要在当前平台构建一个目标平台的编译环境,然后才能通过设置$GOOS和$GOARCH进行交叉编译。golang编译工具在1.5及以后版本中完全使用golang重写,golang编译器内置交叉编译的功能,只需要设置$GOOS和$GOARCH就可以进行交叉编译。下面看一个具体示例:

package main

import (
    "fmt"
    "runtime"
)

func main() {
    fmt.Printf("OS: %s\nArchitecture: %s\n", runtime.GOOS, runtime.GOARCH)
}

打开终端,执行 CGO_ENABLED=0 GOGOS=linux GOARCH=amd64 go build ./main.go 即可编译。

下面介绍命名空间(namespace)。命名空间在编程语言中常用来表示标识符(identifier)的可见范围。编程语言借助命名空间来解决标识符不能同名的问题,命名空间实际上相当于给标识符添加了标识前缀,使标识符变得全局唯一。另外,命名空间使程序组织更加模块化,降低了程序内部的耦合性。

一个标识符可在多个命名空间中定义,它在不同命名空间中的含义的互不相干的。在一个新的命名空间中可定义任意的标识符,它们不会与位于其他命名空间上的同名标识符发生冲突,当然自定义标识符尽量不要使用语言自身的关键字,这些标识符具有全局作用域。golang继承了命名空间的概念,采用包来组织代码,包名构成go命名空间的一部分,不同的包就是一个独立的命名空间。golang除了包级别显式的命名空间,还有隐式的命名空间。函数、方法、以及if、for、switch等和“{}”一起构成一个个代码块,代码块可以嵌套,每个代码块都构成一个隐式的命名空间。不同命名空间可以声明相同的标识符,所以不同的隐式的命名空间同样允许声明相同的标识符(包括变量),这里就有变量覆盖的问题。在介绍变量覆盖之前,先介绍作用域。

在高级语言编程中,作用域(scope)是指名字与实体(可以理解为特定内存地址)的绑定保持有效的那部分程序逻辑区间。golang是静态作用域的语言。所谓静态作用域就是变量的作用域不依赖程序执行时的因素,变量作用域在编译期就能确定。

golang有三种类型作用域:

  • 全局作用域:在任何地方都可以访问的标识符,称其具有全局作用域。在golang中,全局作用域有两类:
    • golang内置的预声明标识符(包括预声明的类型名、关键字、内置函数等),它们具有全局作用域,在任意命名空间内都可见
    • golang包内以大写字母开头的标识符(包括变量、常量、函数和方法名、自定义类型、结构字段等),它们具有全局作用域,在任意命名空间内都可见
  • 包内作用域:在golang包内定义的以小写字母开头的标识符(变量、常量、函数和方法名、自定义类型、结构字段等),它们在本包可见,在其他包都是不可见的,这些标识符具有包内作用域
  • 隐式作用域:每个代码块内定义的变量称为“局部变量”,这些局部变量只在当前代码块内可见,其作用域属于当前代码块的隐式作用域

golang编译器解析变量名到引用实体采用的是从里到外的搜索模式,里层的局部变量能够覆盖外层变量,使得外层的同名变量不可见,这种现象称为变量覆盖。

golang是使用包来组织源代码,并实现命名空间的管理。任何源代码文件必须属于某个包。源码文件第一行必须是package packageName,通过该语句声明自己所在的包。golang的包借助了目录树的组织形式,一般包的名称就是其源文件所在目录的名称,虽然golang没有限制包名必须和其所在目录名同名,但还是建议同名,这样结构更清晰。包可以定义在很深的目录中,包的定义是不包括目录名称的,但是包的引用一般是全路径引用。

标准包的源码位于$GOROOT/src/下面,标准包可以直接引用。自定义的包和第三方包的源码必须放到$GOPATH/src/目录下才能被引用。包引用路径有两种写法,一种是全路径,另一种是相对路径。全路径就是“$GOROOT/src/和$GOPATH/src/”后面包的源码的全路径。相对路径只能用于引用$GOPATH下的包,标准包的引用只能使用全路径引用。

包引用有四种引用格式,以fmt标准库为例:

  • 标准引用格式: import "fmt" 。此时可以用“fmt”作为前缀引用包内可导出元素
  • 别名引用格式: import F "fmt" 。此时相当于给包“fmt”起了个别名F
  • 省略引用格式: import . "fmt" 。此时相当于把包fmt的命名空间直接合并到当前程序的命名空间中,不再需要前缀“fmt”
  • 仅执行包初始化函数init: import _ "fmt" 。使用标准格式引用包,但代码中却没有使用包,编译器会报错。如果包中有init初始化函数,则通过此种方式引用包,仅执行初始化函数。即使包没有init初始化函数,编译器也不会报错。注意:一个包可以有多个init函数,包加载会执行完所有的init函数,但不保证执行顺序,所以不建议在一个包内放入多个init函数;包不能出现环形引用,否则编译不能通过,但包的重复引用是允许的,被重复引用的包会被保证只执行一次init函数

golang包的初始化有以下特点:

  1. 包初始化程序从main函数引用的包开始,逐级查找包的引用,直到找到没有引用其他包的包,最终生成一个包引用的DAG
  2. golang编译器会将DAG转化为一棵树,然后从树的叶子节点开始逐层向上对包进行初始化
  3. 单个包的初始化过程是,先初始化常量,再初始化全局变量,最后才是init函数

golang的版本管理直到go1.5引入vendor才有。vecdor机制就是包中引入vecdor目录,将依赖的外部包复制到vecdor目录下,编译器在查找外部依赖包时,优先在vecdor目录下查找。整个查找第三方包的流程如下:

  1. 如果当前包下有vecdor目录,则从其下查找第三方包
  2. 如果当前包下没有vecdor目录,则沿当前包目录向上逐级查找vendor目录,直到找到$GOPATH/src/下的vendor目录,只要找到vendor目录就去其下查找第三方包
  3. 在$GOPARH下寻找第三方包
  4. 在$GOROOT下寻找第三方包

vecdor将原来放在$GOPATH/src/的第三方包放到当前工程的vecdor目录中进行管理。它为工程独立的管理自己依赖的第三方包提供了保证,多个工程独立地管理自己的第三方依赖包,它们之间不会相互影响。vecdor将原来包共享模式转换为每个工程独立维护的模式,vendor的另一个好处是保证了工程目录下代码的完整性,将工程代码复制到其他go编译环境,不需要再去下载第三方包,直接就能编译,这种隔离和解耦的设计思路是一大进步。

然而vendor有一个重要的问题没有解决,那就是对外部依赖的第三方包的版本管理。通常使用go get -u更新第三方包。默认的是将工程的默认分支的最新版本拉取到本地,并不能指定第三方包的版本。go官方的包依赖管理工具dep就是为了解决该问题而出现的,与此同时社区也有很多包管理工具,如godep、govendor、glide等。目前dep并不会取代go get,go get只是一个便捷的方式。

建议使用以下方式安装dep:

curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh

如果需要,请使用proxychains(笔者就是这么做的)。

使用dep init命令初始化工程,该命令可以用于新项目,也可以用于已存在的项目。该命令会在当前目录创建三个文件: Gopkg.toml Gopkg.lock vecdor 。dep通过两个元文件来管理依赖:manifest文件Gopkg.toml和lock文件Gopkg.lock。Gopkg.toml可以由用户自由配置,包括依赖的source、branch、version等。Gopkg.lock仅描述工程当前第三方包版本视图。Gopkg.toml可以通过命令产生,也可以被用户手动修改;Gopkg.lock是自动生成的,不可以手动修改;vendor目录下存放具体依赖的外部代码。

dep init会做如下的事情:

  • 检查是否有其他版本的依赖管理工具,如果有则尝试转换
  • 检查是否已经用dep管理了,如果有则退出
  • 如果本地有vendor目录,则备份之
  • 分析工程源码,分析生成外部依赖包列表
  • 下载依赖包到$GOPATH/pkg/dep/sources下,切换到每个依赖包的最高兼容版本
  • 生成Gopkg.toml和Gopkg.lock源信息文件
  • 复制最高依赖版本的代码到工程的vendor目录下

下面来看一下Gopkg.toml的几个配置项。

[[constraint]]:指定直接依赖的包的相关元信息,是用户重点维护的信息,其格式为:

[[constraint]]
   name = "github.com/user/project"
   version = "1.0.0"
[[constraint]]
   name = "github.com/user/project2"
   branch = "dev"
   source = "github.com/myfork/project2"

[[constraint]]必须指定依赖包的如下属性中的一个:version(相当于git中的tag)、branch、revision(相当于git中的commit)、source(备选仓库源)

[[override]]:强制设置包的版本元信息,既可用于直接依赖,也可用于间接依赖。通过override声明的包信息会覆盖所有constraint声明的包信息,在实际工程中尽量避免使用override管理依赖包

constraints和overrides被用户用来指定依赖包的哪些版本是需要管理的,以及从哪里获取该版本的包。required和ignored被用来控制哪些包纳入管理,哪些被忽略。

dep的整个工作流如下:

  • 首次初始化运行dep init,dep自动分析并构建Gopkg.toml和Gopkg.lock,默认的是拉取依赖包的最新版本
  • 后续工程开发中可以手动编辑Gopkg.toml来调整依赖,通过运行dep ensure更新Gopkg.lock和vendor的依赖包源码
  • 要保证Gopkg.toml、Gopkg.lock和vendor下的代码一致

相关推荐