Go Book / 1 Go Basics / 09 Go盒子:包及包管理

09 Go盒子:包及包管理

一、Go的包设计理念

1.代码盒子

在Go程序中,包是代码模块组织的单位,为项目代码组织结构,提高代码的可读性、重用性及可维护性。它就是一个代码盒子,把可共同维护的相关功能组织在一起,为外部提供功能实现。理解包可以更好的编写高可维护代码。

2.包结构

2.1 main包与main()函数
  • main()函数是程序的唯一入口,它放置于main包中。
  • main()函数无参数无返回值
//声明包名
package main

//导入其它包
import "fmt"

//程序的入口函数
func main(){
    //使用fmt官方标准包,用于打印输出
    fmt.Println("Hello World!")
}
2.2 init()函数

init()函数基于包级别定义,主要用于:

  • 初始化那些不能被初始化表达式完成初始化的变量
  • 检查或者修复程序的状态
  • 注册
  • 仅执行一次的计算
2.3 包的初始化及调用顺序

任何导入的包在使用前都需先初始化,其执行顺序如下

  • 初始化导入的包
  • 在包级别为声明的变量计算并分配初始值
  • 执行包内的 init 函数
  • 不管包被导入多少次,都只会被初始化一次。
2.4 基于目录组织包

按照Go的惯例,包基于目录组织,其目录名就是包名,目录内部的文件名对包调用无任何影响。目录中的每个文件里其头部使用 package {packagename} 声明所属包

2.5 包的访问控制

在面向对象的语言中,一般都设有public、protected、private等访问控制修饰符。Go语言没有这种显性设置,其访问控制通过隐性的字面量大小写来控制,一个包内编写的字面量元素,如开头小写则只对包内可见,如开头大写则对其它包也可见。

  • 如果定义的常量、变量、类型、接口、结构、函数等的名称是大写字母开头表示能被其它包访问或调用(相当于public)
  • 非大写开头就只能在包内使用(相当于private,变量或常量也可以下划线开头)
package mypkg

//该变量只在包内可见
var a int = 1

//常量可供外部访问
const A int = 10

//可外部调用:mypkg.SayHello()
func SayHello(){
    sayHi()
}

//只包内调用的函数
func sayHi() {
    fmt.Println("Hi!")
}

3.自定义包

3.1 Go包查找位置

在go程序中使用包,我们需要事先了解包定义的位置,以方便go编译器识别:

/{Your Go Path}/src/{Your Project}/vendor/xxx (vendor tree)
/usr/local/Cellar/go/1.12.1/libexec/src/xxx (from $GOROOT)
/{Your Go Path}/src/xxx (from $GOPATH)

如上所见,go编译时会先对程序中引入的包进行查找:

  • 首先从项目的vendor目录查找该项目运行所依赖的包;
  • 其次在$GOROOT目录查找标准库的包;
  • 最后才在$GOPATH目录查找用户自定义或第三方包;
3.2 自定义包
  • 在你的项目目录中创建一个与包同名的目录;
  • 创建一个容易识别的文件名,文件名一般而言无限制,但建议与该文件编码中的功能相关;
  • 在文件内头部声明包名 package {packagename}

示例:

//目录结构
{$GOPATH}|
         |-src
            |-project
                |-{pkgname1}
                    |-filename.go
                    |-...
                |-{pkgname2}
                |-{pkgname3}
                |-...
                
//包内的代码编写
package pkgname1

import (
    "project/pkgname2"
    "project/pkgname3"
)

//coding....
3.3 包的导入
包的导入方式
  • 导入标准包直接使用包名即可
  • 导入三方包需要写包的全名
//方式一:
import 	"fmt"

//方式二:
import (
	"fmt"
	"io/ioutil"
	"net/http"
)

//方式三:导入三方包
import (
	"github.com/jmoiron/sqlx"
	"github.com/garyburd/redigo/redis"
)
导入并定义别名
//在包路径前设置别名
import (
	"fmt"
	ut "io/ioutil"
	ht "net/http"
)
只导入后运行包的初始化,项目中不使用该包内部成员

有些包我们只需要其初始化功能,并不使用,只需在包名前加下划线"_“即可。如数据库驱动包的导入:

import (
	_ "github.com/go-sql-driver/mysql"
	"github.com/jmoiron/sqlx"

)
导入后使用时省略包名,直接使用该包内部成员

有些热门包直接提供一些简易的工具函数,如测试包convey,在写测试用例时我们只要使用”.“号在该包之前,便可在测试用例中直接使用Convey()函数,使用时可不用写包名,就像该函数是项目中定义的函数一样。

import (	
          . "github.com/smartystreets/goconvey/convey"
)

// 使用.符号后,可直接使用Convey(),它是convey.Convey()的简写。
func TestMongoClient(t *testing.T) {
	Convey("测试使用mysql client", t, func() {
		err := RunForTesting(nil)
		So(err, ShouldBeNil)

		err = Client().Ping(context.TODO(), nil)
		So(err, ShouldBeNil)
	})

}

4.标准包

Go提供强大的标准包支持许多常用功能的实现,你可以使用标准包的功能快速编写代码。使用文档工具go doc {PackageName}可快速查阅标准包的文档。

go还提供本地浏览器查阅文档的工具。

usage: godoc -http=localhost:{port}

运行命令后在浏览器打开就可查看可视化文档。

国内Go中文网提供了大部分常用包的翻译: Golang标准库中文文档


二、Go的包管理

提到Go的包管理,这方面也是Go被人诟病最多的地方。初期的Go程序开发基本没有包管理,一切基于GOPATH的设置,在项目开发中灵活性非常差,其中包的变更、迁移、升级基本依赖手工拷贝大法。随着Go的发展,目前已经衍生出许多包管理工具,近期到的Go1.11已经官方支持go modules。以下我们来系统地探讨Go的包管理演化。

1.基于GOPATH的包管理

在《Go安装和运行》专题中已经了解到GOPATH的相关知识点。GOPATH告诉go编译器标准包之外的包去哪里找,它管理着项目内部的依赖。然而它对项目外部的依赖却无能为力,初期的Go没有类似其他语言的依赖管理工具,对外部包的管理基本没有。此阶段项目内部包和外部依赖包是混在一起的,都在GOPATH/src目录中管理

管理方式:
  • 自建项目在GOPATH/src目录
  • 外部包通过go get /git clone下载到GOPATH/src
  • 手工下载拷贝到GOPATH/src

2.基于Vendor机制的包管理

然而如此粗暴的外部包管理肯定有碍于Go社区的发展,基于GOPATH管理,每新建一个项目都需要切换一次GOPATH,想想就累觉不爱。于是Go1.5开始添加了vendor机制帮助于项目管理,暂时缓解了切换GOPATH的尴尬。

简单来说,vendor机制就是单个项目源码让go编译时,优先从项目源码根目录下的vendor目录查找相关依赖,如果vendor中没有,则再去GOPATH中去查找。

管理方式:

  • 项目根目录下创建vendor目录
  • 将项目中需要的外部包添加到vendor即可

这样解决了go源码只能在GOPATH/src编译的尴尬,但是vendor机制也只是比较粗糙的做法,此时Go官方还没完整的包依赖管理工具。

待解决缺陷:

  • 对外部依赖没有配置安装工具,类似node的npm、python的pip,php的composer,这种工具可以编写配置自动安装依赖;
  • 对外部依赖没有版本控制,对外部包的升降级问题依旧没解决

3.基于依赖管理工具的包管理

基于vendor机制,Go社区和官方都开发了相关的依赖管理工具,比较火的有godep、govendor以及官方的dep。下面分别介绍一下这些工具:

3.1 godep

https://github.com/tools/godep

godep相对来说最早也相对较成熟,使用者较多,早期不依赖与vendor,后来也基于vendor做依赖管理。

//安装成功后会在GOPATH/bin生成可执行文件,终端直接可运行。
go get -u github.com/tools/godep

//此工具的详细命令
Usage:
	godep command [arguments]

The commands are:
    save     搜索并拷贝项目中所需要的依赖包到 Godeps目录
    go       运行go tools相关工具集
    get      下载和安装特定的依赖包
    path     打印GOPATH中的依赖项代码
    restore  签出GOPATH中列出的依赖项版本
    update   升级特定的包版本
    diff     显示当前和以前保存的依赖项集之间的差异
    version  显示版本信息
使用方式:
  • godep get

下载并安装依赖包到Godeps目录

  • godep save

使用 godep save 将项目中使用到的第三方库复制到项目的Godeps目录下。godep save能否成功执行需要有两个要素: 当前或者需扫描的包均能够编译成功:因此所有依赖包事先都应该已经或go get或手工操作保存到当前GOPATH路径下。

  • godep restore

如果下载的项目中只有Godeps/Godeps.json文件,而没有包含第三方包源码则可以使用godep restore这个命令将所有的依赖库下来下来到GOPATH的src中。godep restore执行时,godep会按照Godeps/Godeps.json内列表,依次执行go get -d -v 来下载对应依赖包到GOPATH路径下,因此,原先的依赖包保存路径(GOPATH下的相对路径)必须与下载url路径一致。

  • godep go {tools}

使用godep做依赖管理后,项目的工具必须使用godep go 才能编译运行,因为go命令是直接到GOPATH目录下去找第三方库。 而使用godep下载的依赖库放到Godeps/workspace目录下。 例如:

godep go run main.go
godep go build
godep go install
godep go test
3.2 govendor

https://github.com/kardianos/govendor

govendor功能相对godep多一点,但核心功能差不多,因为都离不开vendor机制。

//安装
go get -u github.com/kardianos/govendor

//查看帮助信息
子命令:
	init     Create the "vendor" folder and the "vendor.json" file.
	list     List and filter existing dependencies and packages.
	add      Add packages from $GOPATH.
	update   Update packages from $GOPATH.
	remove   Remove packages from the vendor folder.
	status   Lists any packages missing, out-of-date, or modified locally.
	fetch    Add new or update vendor folder packages from remote repository.
	sync     Pull packages into vendor folder from remote repository with revisions
  	             from vendor.json file.
	migrate  Move packages from a legacy tool to the vendor folder with metadata.
	get      Like "go get" but copies dependencies into a "vendor" folder.
	license  List discovered licenses for the given status or import paths.
	shell    Run a "shell" to make multiple sub-commands more efficient for large
	             projects.

	go tool commands that are wrapped:
	  "+status" package selection may be used with them
	fmt, build, install, clean, test, vet, generate, tool


状态类型:
	+local    (l) packages in your project
	+external (e) referenced packages in GOPATH but not in current project
	+vendor   (v) packages in the vendor folder
	+std      (s) packages in the standard library

	+excluded (x) external packages explicitly excluded from vendoring
	+unused   (u) packages in the vendor folder, but unused
	+missing  (m) referenced packages but not found

	+program  (p) package is a main package

	+outside  +external +missing
	+all      +all packages

	Status can be referenced by their initial letters.

使用方式:

  • 使用govendor init命令初始化项目的依赖
  • 运行govendor fetch命令增加依赖
  • 或运行govendor add命令更新vendor/vendor.json,并拷贝GOPATH下的代码到vendor目录中。
  • 打开./vendor/vendor.json查看依赖的包

govendor还可以直接指定依赖包版本来获取包,解决了外部依赖的版本管理问题。

3.3 dep (官方工具)

https://github.com/golang/dep

Go作为现代语言,其开发团队也意识到在合作越来越多的项目开发中对包依赖管理的重要性,故此官方也维护一个工具。

//dep是Go官方维护的依赖管理工具
//在mac上安装,不同系统都有各自的安装方法,具体查看官方文档
brew install dep

//帮助信息
Dep is a tool for managing dependencies for Go projects

Usage: "dep [command]"

Commands:

  init     Set up a new Go project, or migrate an existing one
  status   Report the status of the project's dependencies
  ensure   Ensure a dependency is safely vendored in the project
  version  Show the dep version information
  check    Check if imports, Gopkg.toml, and Gopkg.lock are in sync

Examples:
  dep init                               set up a new project
  dep ensure                             install the project's dependencies
  dep ensure -update                     update the locked versions of all dependencies
  dep ensure -add github.com/pkg/errors  add a dependency to the project

使用方式:

  • dep ini 项目初始化
//项目根目录中会生成以下文件或目录
├── project
    ├── ...
    ├── Gopkg.lock //生成的文件,不要手工修改 
    ├── Gopkg.toml //依赖管理的核心文件,可以生成也可以手动修改
    └── vendor //依赖包的源码存放目录
  • dep ensure -add {third part package name}@={tag} 添加一个依赖包,可指定版本tag
  • dep ensure -v 确保同步,每次更改后执行
  • dep prune -v 删除没有用到的依赖
  • dep status 查看项目的依赖状态信息

4.Vendor机制的问题

经过上面的介绍,基本对go的包管理有了相对全面的了解,至此我们可以摆脱GOPATH的局限,借助工具我们还能控制依赖的版本问题。但是你有没发现vendor也是存在问题的,比如:

  • 项目中存在大量的拷贝代码;
  • 同一个第三方包在本地不能同时保存多个版本。

基于vendor的依赖管理工具其核心功能都差不多,个人更推荐官方维护的dep,随着Go版本的更新,其对包依赖管理会加强更多功能,但也不排除有其它解决方案,以下要讲的module机制就是最新的管理方案,其也更切合GO团队的设计理念,无论哪种方案,都有许多的问题还待解决。

5.基于go modules的包管理

Go1.11开始,go引入了go modules的包管理机制,1.12版本正式开始支持。它是官方推出的全新的包管理方案

5.1 什么是module机制?

不同于以往基于GOPATH和Vendor的项目构建,其主要是通过$GOPATH/pkg/mod下的缓存包来对项目进行构建。 相对于vendor、dep管理方案,module机制更加灵活。

先看官方说明

模块是相关GO包的集合。模块是源代码交换和版本控制的单元。Go命令直接支持使用模块,包括记录和解决对其他模块的依赖性。模块替换了旧的基于gopath的指定方法给定生成中使用的源文件。

go mod help
Go mod provides access to operations on modules.

Note that support for modules is built into all the go commands,
not just 'go mod'. For example, day-to-day adding, removing, upgrading,
and downgrading of dependencies should be done using 'go get'.
See 'go help modules' for an overview of module functionality.

Usage:

	go mod <command> [arguments]

The commands are:

	download    download modules to local cache
	edit        edit go.mod from tools or scripts
	graph       print module requirement graph
	init        initialize new module in current directory
	tidy        add missing and remove unused modules
	vendor      make vendored copy of dependencies
	verify      verify dependencies have expected content
	why         explain why packages or modules are needed

Use "go help mod <command>" for more information about a command.

常用相关命令

  • go mod init {module}

初始化.mod 包管理文件到当前工程。

  • go mod vendor

vendor版本的解决方案,将依赖复制到vendor下面。

  • go mod tidy

移除未用的模块,以及添加缺失的模块。

  • go mod verify

验证所有模块是否正确。

  • go mod edit

对go.mod进行编辑,如果熟悉格式的话,也可以直接改文件

5.2 go modules和dep区别
  • dep是解析所有的包引用,然后在$GOPATH/pkg/dep下进行缓存,再在项目下生成vendor,然后基于vendor来构建项目,无法脱离GOPATH,因为vendor必须在GOPATH下才能使用
  • go modules是解析所有的包引用,然后在$GOPATH/pkg/mod下进行缓存,直接基于缓存包来构建项目,所以可以脱离GOPATH
5.3 go modules目前存在的问题
  • 本地包引用被墙的问题

考虑到有些包的下载会被墙的因素,目前完全脱离vendor不太现实,当然针对这个问题也有替代方案,如使用replace指令

//针对被墙的包直接修改go.mod文件
replace golang.org/x/utils => /path/to/local

//或者使用go mod edit replace指令
go mod edit -replace=old[@v]=new[@v]
go mod edit -dropreplace=old[@v]

目前go modules机制刚推出,或许还存在不太稳定的问题,相信随着Go版本的更新,go modules机制或许会成为主流包管理方式。