Go语言——包和包管理工具
1、包简介
1.1 工作空间
go语言的工作空间必须由 bin、pkg、src三个目录组成
workspace
|
+--- bin // go install 安装目录。
| |
| +--- learn
|
|
+--- pkg // go build ⽣成静态库 (.a) 存放目录。
| |
| +--- darwin_amd64
| |
| +--- mylib.a
| |
| +--- mylib
| |
| +--- sublib.a
|
+--- src // 项目源码目录。
|
+--- learn
| |
| +--- main.go
|
|
+--- mylib
|
+--- mylib.go
|
+--- sublib
|
+--- sublib.g
可以在GOPATH环境变量中添加多个工作空间,但不能和GOROOT相同。通常go get使用第一个工作空间保存下载的第三方库
1.2 源文件
- 编码:源码⽂件必须是 UTF-8 格式,否则会导致编译器出错。
- 结束:语句以 ";" 结束,多数时候可以省略。
- 注释:⽀持 "//"、 "/**/" 两种注释⽅式,不能嵌套。
- 命名:采⽤ camelCasing ⻛格,不建议使⽤下划线。
1.3 包命名和声明
包命名惯例 :
- 给包命名的惯例是使用包所在目录的名字。这让用户在导入包的时候,就能清晰地知道包名。
- 尽量使用简介明了的名字,但要避免冲突
- 包名一般使用单数的形式,但是避免冲突的用的复数比如bytes,errors,strings等
- 要避免包名有其他含义。比如 temp这种
- 命名时考虑包名和成员如何配合,尽量减少包名和成员有重复
包声明:
每个go源文件开头都必须有包声明语句,比如 package main
。 包声明的目的是确定当前包被其他包导入时默认的标识符(也就是包名),就是你不写明,人家咋用。
通常来说,默认的包名是包导入路径名的最后一段,所以即使两个包的导入路径不同,他们依然可能有一个相同的包名。比如math/rand
和crypto/rand
。 这个时候可以用 包的别名。
重要:
每个包都在一个单独的目录里。不能把多个包放到同一个目录中,也不能把同一个包的文件分拆到多个不同目录中。这意味着,同一个目录下的所有.go 文件必须声明同一个包名。
1.4 main 包
- 所有用 Go 语言编译的可执行程序都必须有一个名叫 main 的包。 go语言的编译器会将这种名字的包编译为二进制可执行文件。
- main包下肯定会有名为main()的函数,main()是程序的入口。
- 编译完会使用声明 main 包的代码所在的目录的目录名作为二进制可执行文件的文件名
题外:
命令和包: Go 文档里经常使用命令(command)这个词来指代可执行程序,如命令行应用程序。
这会让新手在阅读文档时产生困惑。记住,在 Go 语言里,命令是指任何可执行程序。作为对比,
包更常用来指语义上可导入的功能单元。
2、导包
关键字 Import ,进行导包。
2.1 两种方式
import a import b,...多次导入,以及import(a b c) 批量导入,如果导入的包不使用会报错。
import "fmt"
//或者
import(
"fmt"
"time"
)
2.2 包的别名
import(
io "fmt"
)
io.Println("hello world") //别名可以直接用,在包重名时很有用
2.3 简洁模式
import (
. "fmt" //但是为了别人好看,一般还是不用这种
)
func main(){
Println("hello")
}
2.4非导入模式(匿名导入)
import _ "test" //非导入模式:仅让该包执行初始化函数
即导入一个包并不使用它。如果不加_,就会出现编译错误。 在这里用下划线 _ 重命名导入的包。只导入,不使用。
但是这个包它进行了初始化,一般在init函数调用,这样做的好处是,有些包我们不显示使用它,但是有可能用到它,或者由用户选择使用哪个。比如 对特定图像驱动包的初始化,在我们格式化转换图片用到。还有 database/sql
包,可以先都初始化,让用户选择不同的数据库驱动。
2.5 导包的路径
一般情况下是包的相对路径。比如:
import "learn/test"
标准库中的包会在安装 Go 的位置找到,即GOROOT。 Go 开发者创建的包会在 GOPATH 环境变量指定的目录里查找。GOPATH 指定的这些目录就是开发者的个人工作空间。
如果 Go 安装在/usr/local/go,并且环境变量 GOPATH 设置为/home/myproject:/home/mylibraries,编译器就会按照下面的顺序查找 net/http 包:
/usr/local/go/src/pkg/net/http
/home/myproject/src/net/http
/home/mylibraries/src/net/http
一旦编译器找到满足import的包,就停止进一步查找。 如果查遍也没有找到对应宝,run 或build就会出错。可用go get命令来进行修正。
2.6 远程导入
目前的大势所趋是,使用分布式版本控制系统(Distributed Version Control Systems, DVCS)
来分享代码,如 GitHub、 Launchpad 还有 Bitbucket。 Go 语言的工具链本身就支持从这些网站及
类似网站获取源代码。 Go 工具链会使用导入路径确定需要获取的代码在网络的什么地方。 比如:
import "github.com/xxxx/xxx"
用导入路径编译程序时, go build
命令会使用 GOPATH
的设置,在磁盘上搜索这个包。事实上,
这个导入路径代表一个 URL
,指向 GitHub
上的代码库。如果路径包含 URL,可以使用 Go 工具链从
DVCS 获取包,并把包的源代码保存在 GOPATH
指向的路径里与 URL 匹配的目录里。这个获取过程
使用 go get
命令完成。 go get
将获取任意指定的 URL 的包,或者一个已经导入的包所依赖的其
他包。由于 go get
的这种递归特性,这个命令会扫描某个包的源码树,获取能找到的所有依赖包。
注意:
未使用的导入包,会编译错误。
3、初始化 init
每个包可以包含任意多个 init 函数,这些函数都会在程序执行开始的时候被调用。所有被编译器发现的 init 函数都会安排在 main 函数之前执行。
init 函数用在设置包、初始化变量或者其他要在程序运行前优先完成的引导工作。
比如:
package postgres
import (
"database/sql"
)
func init() { //初始化函数
//这里省略。。
sql.Register("postgres", new(PostgresDriver))
}
比如这段代码是在postgres包中定义的,先不管代码内容是什么,可以看到有个init()函数来初始化驱动。如果我们引入这个包,当程序启动的时候会首先调用init()函数,在main()之前执行。
正如上面说的匿名导入,import _ xx/postgres
这种,避免了未使用的包编译出错,还在程序启动时进行了包的初始化。 然后就可以直接使用这个驱动包了。
小结:
- 每个源⽂件都可以定义⼀个或多个初始化函数。
- 编译器不保证多个初始化函数执行次序。
- 初始化函数在单⼀线程被调用,仅执行⼀次。
- 初始化函数在包所有全局变量初始化后执行。
- 在所有初始化函数结束后才执行 main.main。
- 无法调用初始化函数。
4、文档
godoc
命令可以生成本地文档,go doc
可以查看但个包的信息。比如; go doc fmt
可以在命令行查看fmt信息,还可以查看单个函数 go doc fmt.Println
。但是这种不方便查看。
生成本地浏览器站点在线查看 godoc -http=:8080
然后打开浏览器输入localhost:8080
就能看到了。
前提是你自己设置了自己的GOPATH
和GOROOT
,它会查找这两个路径下的代码生成文档。可以看到有标准库的所有包、文档注释、示例。还有自己写的代码。
4.1 生成文档规范
我们要生成文档给别人看,就要遵守规范。
- 实例代码不能声明为main包,因为无法显示package main的成员文档
- 必须添加注释,对包、函数、类型和全局变量都适用,可以是
//
或/**/
- 仅和成员相邻(中间没有空行)的注释被当做帮助信息
- 相邻行会合并成同一段落,用空行分隔段落
- 会自动转换URL为链接
- 自动合并多个源码文件中的同一package 文档
4.2 给文档添加示例函数
除了我们编写单元测试、基准测试的,我们还可以编写测试实例,作为我们文档的示例生成。
根据我自己测试,必须满足一下规则
- 同样不能声明main包
- 文件必须以_test.go 结尾
- 示例代码必须单独放在一个文件中,比如example_test.go,尽量见名知意的命名
- 示例函数必须使用ExampleXXX 命名,以Example开头,并且后面跟要添加示例的函数名。这样会在相应函数文档下生成示例代码
- 如果只是写个Example() 函数,会在包顶部的注释下生成示例文档
- 示例的输出采用注释的方式,以
//Output:
开头,然后换行,每行输出占一行
例如:
Example_test.go
func ExampleNew() {
fmt.Println("你愁啥New")
fmt.Println("我是你爸爸New")
//Output:
//这个是输出结果
}
然后执行godoc -http=:8000
浏览器输入localhost:8000
找到你的包。
5、包管理工具
除了go工具链自带的工具比如,go build
、go vet
、go get
、 go doc
等等,还有包依赖管理工具。比如 dep
等等,go 1.11 1.12 还添加了 go modules
。
一直依赖go语言被人吐槽的就是包依赖管理 和 错误处理方式。 社区出现了一批包依赖管理工具。
5.1 依赖管理快速了解
两个概念:GOROOT 和GOPATH
GOROOT: 系统环境变量,就是我们存放下载的go语言源码的地方(go的源码,不是我们写的)。
GOPATH: 环境变量,我们的工作空间,包括bin、pkg、src。是存放我们写的代码以及下载的第三方代码。
说到依赖,分为内部依赖和外部依赖。
内部依赖:
GOPATH和GOROOT,GOROOT并不是必须要设置的,但是GOPATH必须要设置,但并不是固定不变的。本项目内部依赖就会在GOPATH 所配置的路径下去寻找,编译器如果找不到会报错。总的来说内部依赖不需要太操心。
外部依赖包:
当我们要实现一些功能的时候,不可避免的需要一些第三方包,也统称为外部依赖包。go1.5之前只支持使用GOPATH来管理外部依赖包的,对比java的maven 和gradle等 简直不太方便。
使用GOPATH来管理外部依赖go1.5release之前:
go允许import不同代码库的代码,例如github.com, k8s.io, golang.org等等;对于需要import的代码,可以使用 go get 命令取下来放到GOPATH对应的目录中去。例如go get github.com/silenceshell/hcache
,会下载到$GOPATH/src/github.com/silenceshell/hcache
中去,当其他项目在import github.com/silenceshell/hcache
的时候也就能找到对应的代码了。
看到这里也就明白了,对于go来说,其实并不care你的代码是内部还是外部的,总之都在GOPATH里,任何import包的路径都是从GOPATH开始的;唯一的区别,就是内部依赖的包是开发者自己写的,外部依赖的包是go get下来的。
后来,引入了Vendor 机制,也就是下面讲的。 社区出现了一批Vendor机制的库,官方也除了一个dep。
再到go 1.11 1.12 ,出现了和dep完全不同的方式,go modules
,这个单独说。
5.2 Vendor 机制引入
在go1.5release之前,我们要管理多个依赖包版本时,只能通过设置多个GOPATH
,拷贝代码来解决。比如,如果两个工程都依赖了Beego,一个1.5,一个1.8,那么必须设置俩GOPATH,并且还要记得切换。
go语言原生包缺陷:
- 能拉取源码的平台很有限,绝大多数依赖的是 github.com
- 不能区分版本,以至于令开发者以最后一项包名作为版本划分
- 依赖 列表/关系 无法持久化到本地,需要找出所有依赖包然后一个个 go get
- 只能依赖本地全局仓库(GOPATH/GOROOT),无法将库放置于局部仓库($PROJECT_HOME/vendor)
2015年,官方是在看不下去了,引入了除了GOPATH
之外的方式,vendor
机制来进行管理。 这个vendor属性(默认关闭,需要设置go环境变量GO15VENDOREXPERIMENT=1),但在1.6版本中默认开启。Go 1.7将此设置设置为标准特性,并删除了对该标志的支持
到底这个vendor是个什么呢?
简单说,就是在你项目中多了一个vendor文件夹,go会把它默认作为GOPATH。让go编译时,优先从项目源码树根目录下的vendor目录查找代码(可以理解为切了一次GOPATH),如果vendor中有,则不再去GOPATH中去查找。
但是有有了其他问题:
- 嵌套的vendor目录问题:vendor目录下面的项目里面的vendor目录怎么办?
- vendor机制本身没有版本概念,不同版本间类型不兼容问题依旧存在。
- 与其他 GOPATH 下的包init函数冲突问题:出现了相同的包,重复的init() 函数又怎么办?
社区支持vendor的包管理库有很多,官方推荐的就有15种。
用的比较多的有 dep
(官方)、Godep
、Govendor
等等
go官方的包管理工具是dep,目前来看也是用的最多的,是官方建议使用的。
官方wiki各种对比: https://github.com/golang/go/wiki/PackageManagementTools
5.2.1 官方dep 工具的使用
官方地址:https://golang.github.io/dep/
github: https://github.com/golang/dep
下载安装:
windows下:
官方推荐使用二进制文件安装,下载地址:https://github.com/golang/dep/releases
在这个地址下载的二进制文件,直接改名dep.exe ,然后扔进GOPATH/bin下,或者你自己设置的GOBIN目录下。前提是这些都需要在环境变量 PATH中添加才可以。
接下来 打开命令行,输入dep version
查看是否成功。
还可以直接使用脚本安装:
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
这个脚本在其他平台也试用,最终目的都是下载、然后配个环境变量就可以使用了。
C:\Users\ANSWER>dep version
dep:
version : v0.5.3
build date : 2019-05-13
git hash : 192eb44
go version : go1.12.5
go compiler : gc
platform : windows/amd64
features : ImportDuringSolve=false
初始化工程
比如,我在我的GOPATH/src下 创建文件夹,helloworld, 然后命令行进入helloworld执行 dep init
。 执行不报错就会在文件夹下生成一个文件夹vendor
和两个文件 Gopkg.lock
、Gopkg.toml
-
Gopkg.lock
是生成的文件,不要手工修改 Gopkg.lock 官方文档。 -
Gopkg.toml
是依赖管理的核心文件,可以生成也可以手动修改,一般情况下Gopkg.toml
里面只定义直接依赖项,而Gopkg.lock
里面除了包含Gopkg.toml
中的所有项之外,还包含传递依赖项。比如我们的项目依赖项目A, 而项目A又依赖B、C,那么只有A会包含在Gopkg.toml
中,而A、B、C都会定义在Gopkg.lock
中。所以Gopkg.lock
定义了所有依赖的项目的详细信息(commit ID和packages),使得每次build我们自己的项目时,始终基于确定不变的依赖项。Gopkg.toml 官方文档。 -
vendor
目录是 golang1.5 以后依赖管理目录,这个目录的依赖代码是优先加载的,类似 node 的 node_module 目录。
三个之间的关系:
dep的日常使用
我们最常使用的也就是两个命令:
-
dep ensure
这个是我们主要的命令,管理依赖,唯一一个更改磁盘状态的命令 -
dep status
查看项目状态,依赖状态
有四个地方可能用到 dep ensure
,即添加新依赖、更新依赖,删除导入,根据Gopkg.toml改变依赖
依赖管理—添加依赖:
// 依赖管理帮助
dep help ensure
// 添加一条依赖
dep ensure -add github.com/pkg/errors
// 这里 @= 参数指定的是 某个 tag
dep ensure -add github.com/pkg/errors@=1.0
// 添加后一定记住执行 确保 同步
dep ensure
// 建议使用
dep ensure -v
// 删除没有用到的 package
dep prune -v
参数-v,是为了更好的查看执行过程,建议加上。
注意:在代码添加import
代码,然后运行dep ensure
也可以添加新的依赖关系,但是这种方式并不总是完美的导入,但是有时很有用。
依赖更新:
dep ensure -update -v //更新所有依赖项(尽管通常不推荐)
dep ensure -update github.com/foo/bar //更新具体依赖项
注意些允许 dep ensure -update
更新版本,一些约束可能需要手动更新Gopkg.toml
文件
查看依赖状态
通过执行dep status命令,将列出您的应用程序中使用的版本以及开发人员发布的最新版本。
D:\gopath\src\helloworld>dep status
PROJECT CONSTRAINT VERSION REVISION LATEST PKGS USED
github.com/pkg/errors v0.8.1 v0.8.1 ba968bf v0.8.1 1
添加和删除import 语句
dep 依赖于import
代码中的语句来确定项目实际需要的依赖项。
当发生以下情况之一,需要使用dep ensure
进行项目同步
- 您已经添加了包的第一个导入,但是已经从该项目导入了其他包。
- 您已经删除了包的最后一个导入,但仍然从该项目导入了其他包。
- 您已经添加了特定项目中任何包的第一个导入。(注意:这是另一种添加方法)
- 您已经从特定项目中删除了包的最后一个导入。
简而言之,dep关心的是跨整个项目的一组惟一的导入路径,并且只关心您何时从该集合中添加或删除导入路径。dep检查将快速报告任何此类问题,这些问题将通过运行dep ensure得到解决。
规则变化Gopkg.toml
Gopkg.toml
文件包含五种基本类型的规则。详情查看Gopkg.toml
文档
简单接收:
-
required
,它基本上相当于.go文件中的import语句,只是这里可以列出一个主包 -
ignored
,忽略导入路径(以及它唯一引入的任何导入) -
[[constraint]]
,表示版本约束和一些其他规则 -
[[override]]
,与[[constraint]]
完全相同,但只有当前项目可以表示它们,并且它们在当前项目和依赖项中都替代[[constraint]] -
[prune]
,全局和每个项目的规则,这些规则控制了应该从vendor中删除什么类型的文件
对这些规则中的任何一项进行修改,都可能需要修改Gopkg.lock
和 vendor/
。dep ensure
成功运行将包含所有修改,同步项目。
dep实例练习:
通过import 导入包errors
package main
import (
"github.com/pkg/errors"
)
func main() {
//别在意代码、、、、
var err error
errors.Wrap(err, "read failed")
}
这个时候,代码是爆红的,因为引入的包本地不存在。 接下来我们使用命令行 dep ensure
,会直接去下载包到vendor。
这时候我们发现:Gopkg.toml
没有变化,但是Gopkg.lock
发生了变化 如下:
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = ["github.com/pkg/errors"]
solver-name = "gps-cdcl"
solver-version = 1
============================上面是原来的===============================
[[projects]]
digest = "1:cf31692c14422fa27c83a05292eb5cbe0fb2775972e8f1f8446a71549bd8980b"
name = "github.com/pkg/errors"
packages = ["."]
pruneopts = "UT"
revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4"
version = "v0.8.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = ["github.com/pkg/errors"]
solver-name = "gps-cdcl"
solver-version = 1
可以看到,多了一个[[projects]]
,
digest:
它是该项目的vendor/的内容的哈希摘要。通过冒号分隔的前缀对摘要进行版本控制;形式是这样的
<版本>:<hex编码的摘要>
。版本1对应的哈希算法是SHA256,在stdlib包crypto/ SHA256中实现。
name:
引入的项目路径,作为名字,也就是作为这个项目的根目录。
packages
dep确定构建所需的源目录的完整列表。
pruneopts
一种压缩编码形式。用于Gopkg.toml
中定义的依赖规则。比如以下三种;
N |
non-go |
---|---|
U |
unused-packages |
T |
go-tests |
revision :修正版本
version :版本号
对于整个[[projects]]
,有5个是必须有的。
属性 | 是否必须? |
---|---|
name |
Y |
packages |
Y |
source |
N |
revision |
Y |
version |
N |
branch |
N |
pruneopts |
Y |
digest |
Y |
小结:
-
dep check
将快速报告您的项目不同步的任何方式。 -
dep ensure -update
是更新依赖项的首选方法,但对于不发布semver版本的项目效率较低。 -
dep ensure -add
通常是引入新依赖项的最简单方法,但您也可以添加新import
语句然后运行dep ensure
。 - 如果您进行手动更改
Gopkg.toml
,最好运行dep ensure
以确保所有内容都保持同步。 -
dep ensure
几乎从来不会运行错误;如果你不确定发生了什么,运行它会让你回到安全的版本,或者失败。
此外,:
- 在Go工具链中,通常在您自己的项目中避免符号链接。dep可以容忍这一点,但是就像Go工具链本身一样,它通常不太支持符号链接。
- 切勿直接编辑任何内容
vendor/
; dep将无条件地覆盖此类更改。如果您需要修改依赖项,fork它并正确执行它。
有疑问加站长微信联系(非本文作者)