基础
Go程序通过包(package)进行组织,一个包可以由多个文件组成,但这些文件必须位于同一目录下。每个文件通过在首行用package语句声明所属的包,例如package math,包名不要求一定要与目录名相同(虽然通常会使用相同的)。 同一个包下定义的常量、类型、变量和函数都是互相可见的,即使位于不同的文件中。大写字母开头的元素可以导出到其它包中使用。在这种约束的工程结构组织下,编译器无需额外指令(通常是写一个Makefile文件)就清楚知道怎样构建程序。编译的时候每个包会生成一个.a文件,可在build时通过-work参数打印临时路径查看,这些.a文件再链接生成最终的可执行文件。要引入外部包,通过import语句,例如import math / import github.com/boltdb/bolt。import的参数是包路径。关于GOPATH的管理,一种做法是设置一个唯一路径,另一种做法是为每个项目单独设置,我推荐使用第一种。
如果工程的输出是可执行文件则必需有main包。对于仅设计为内部实现使用,而不是被外部引用的包可以放到internal目录中,这样位于internal目录外的包就不能引用其中的包,否则会编译错误。internal的设计可以防止内部实现细节扩散到外部。
有四种导入(import)包的方式:
1. import "path/pkg"
2. import x "path/pkg"
3. import . "path/pkg"
4. import _ "path/pkg"
假设包名和目录名一致(此处假设为pkg),使用第1种导入方式,在使用时必须显式的使用包名作为限定符(说明名字空间),第2种方式使用指定的名字x作为限定符,第3种方式导入的包不需要使用限定符(导入包和当前包在同一名称空间,要注意名字冲突问题),第4种方式不导入名字空间,只是对导入包进行初始化操作。最好不要使用第3种导入方式,否则当工程变大时将很难维护。
引入外部包的问题和解决办法
通常稍大一点规模的项目都会引入外部包(例如数据库驱动) ,而不是把每个轮子都造一遍(不必要/成本不允许/根本没造轮子的能力)。外部即意味着不在自己的控制之内,可能产生新旧版本的API不兼容问题,例如函数增加了参数/结构体字段类型修改,甚至外部包的作者直接把项目删除了!这就可能导致在你本机可以编译通过并正常运行的程序在新同事那里连编译都通不过的情况发生。其中一种解决办法是把用到的外部项目fork一份,但又会存在升级维护的问题,而且这种方式给人的直观感受就是丑陋的。我曾经遇到过mongodb驱动的不同版本支持的document最大size是不同的,导致遭遇更新驱动版本后数据库操作失败的问题。
Go工具链在1.5版本增加了实验性质的vendor机制(通过GO15VENDOREXPERIMENT环境变量开启)来解决包依赖关系的问题,从Go1.6开始默认开启,Go1.7成为标准特性。但是Go官方并没有提供相关工具,有很多的第三方实现。通过综合评估各个工具的热度(github star / 在开源项目中的使用情况)和易用性,推荐掌握govendor和godep (按先后优先级)。
govendor
govendor的基本原理就是通过vendor.json文件描述来确定工程使用的外部包。看一个简单示例:
{
"comment": "",
"ignore": "test",
"package": [
{
"checksumSHA1": "Vw77VGlwiPNNoCPc+lKVeQWcKK4=",
"path": "github.com/boltdb/bolt",
"revision": "4b1ebc1869ad66568b313d0dc410e2be72670dda",
"revisionTime": "2016-10-28T19:36:45Z"
},
{
"checksumSHA1": "Jl0BawxPBuKr2uY1FpdXGyfCzrA=",
"path": "github.com/caojunxyz/upid",
"revision": "f8f05b4acc042cfc1a81bc9dbecb5232800d974b",
"revisionTime": "2016-10-12T11:57:35Z"
}
],
"rootPath": "github.com/caojunxyz/govendortest"
}
基于vendor.json可以保证不同的构建者使用相同的外部包build工程,进而保证可重复的确定输出。通过FAQ可以快速掌握govendor的常用命令,此处不再赘述(可另起一篇介绍)。使用govendor命令可以在工程根目录下增加vendor目录,依赖的外部包可以通过命令拷贝一份,并且还可以通过命令升级维护。例如示例项目的vendor目录结构为:
vendor/
vendor.json
github.com/
boltdb/
bolt/
caojun.xyz/
upid/
任何情况都把vendor.json签入(check in)版本控制系统中,vendor目录下的外部包拷贝通常根据情况决定是否签入版本控制,main包下的vendor外部包就签入,否则不签入。这里很容易理解,这样可以防止大量的重复代码。
godep
我在使用godep的过程中遇到一个问题,目前还没有被close,以后再单独写一篇介绍。
使用gopkg.in管理github开源包
有很多被广泛使用的github开源项目通过gopkg.in进行版本管理,例如mgo、yaml。gopkg.in非侵入式的设计堪称巧妙,非常具有借鉴意义。它的设计建立在对版本号的管理约定和go get命令对http响应meta信息的处理上。
采用三段式的版本号设计:(vMAJOR[.MINOR[.PATCH]]),例如v1, v2, v2.0, v2.1.3。这里最重要的是主版本号(MAJOR)的变更,这往往意味着向后不兼容的修改。主版本号0表示不稳定版本,github上相应项目如果没有任何满足约定的tag或branch时默认为v0,对应master分支。
meta信息的格式为<meta name="go-import" content="pkg git repo">
。
- pkg: 包名,例如gopkg.in/yaml.v2
- git: 版本控制系统
- repo: 源代码地址,例如https://github.com/go-yaml/yaml.git
gopkg.in支持两种URL样式,例如:
gopkg.in/pkg.v3 → github.com/go-pkg/pkg (branch/tag v3, v3.N, or v3.N.M)
gopkg.in/user/pkg.v3 → github.com/user/pkg (branch/tag v3, v3.N, or v3.N.M)
第一种样式更精简,通常用于被广泛使用的有较大影响力的开源项目,例如gopkg.in/yaml.v2,它通过包名和user名的名字约定来精简样式。第二种样式通常用于个人项目,例如pkg.in/caojunxyz/upid.v0。
以gopkg.in/yaml.v2为例说明大致原理:
- 本地运行go get gopkg.in/yaml.v2命令,发起https://gopkg.in/go-yaml/yaml.v1?go-get=1请求
- gopkg.in服务器收到请求后解析出目标项目名yaml和目标版本号v2
- gopkg.in服务器到github服务器查询go-yaml/yaml项目是否存在,且存在名为v2的tag或branch
- gopkg.in服务器在响应go get的meta信息中包含源代码clone地址和GOPATH中的对应下载目录
- go get克隆代码到本地
这个例子中meta信息为:
<meta name="go-import" content="gopkg.in/yaml.v2 git https://github.com/go-yaml/yaml.git">
如果在浏览器中打开gopkg.in/yaml.v2,系统会自动生成一个web页面,其中包含所有可用版本。通过“Source Code”超链接可以跳转到项目的github地址,通过“API Documentation”超链接可以跳转到项目在godog.org的对应文档页面。godog.org也是一个很巧妙的设计,有点类似gopkg.in,它通过godoc命令生成项目文档。
go get使用自定义域名
有时候我们可能会有通过自定义域名引用包的需求,比如公司内部的项目。这种情况下,源代码可能通过自建仓库(例如使用GitLab, Gogs, Github企业版等)托管或者托管在第三方的私有仓库中。go get默认是不支持从自定义域名参数获取代码的,有一种做法是修改Go源码实现该功能(很容易就实现了),网上也有人是这样做的,但这种方式是侵入式的,Go官方默认不支持自有其道理。更加优雅的方式是通过类似gopkg.in的方式,直接上代码:
const domain = "caojun.xyz"
// const host = "https://github.com/caojunxyz" // 托管在github
const host = "http://caojun.xyz:3000" // Gogs自建仓库
func handler(w http.ResponseWriter, r *http.Request) {
list := strings.Split(r.URL.Path, "/")
if len(list) > 1 {
repo := strings.Join(list[1:], "/")
content := fmt.Sprintf("%s/%s", domain, repo)
meta := fmt.Sprintf(`<meta name="go-import" content="%s git %s/%s.git">`, content, host, repo)
fmt.Println("meta:", meta)
fmt.Fprint(w, meta)
}
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":80", nil)
select {}
}
这里需要注意的是,如果自定义域名已经有web应用运行(公司主页)该如何处理:
- 单独使用一个子域名例如code.caojun.xyz(推荐)
- web端检测客户端是否浏览器发起的请求,如果不是才返回go-import meta信息