19Go语言——包和包管理工具

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/randcrypto/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就能看到了。

前提是你自己设置了自己的GOPATHGOROOT ,它会查找这两个路径下的代码生成文档。可以看到有标准库的所有包、文档注释、示例。还有自己写的代码。

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 buildgo vetgo getgo 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(官方)、GodepGovendor等等

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.lockGopkg.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 会先将第三方依赖包下载到gopath/pkg/dep/下,然后,拷贝一份到当前项目vendor下。

三个之间的关系:

在这里插入图片描述

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.lockvendor/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中定义的依赖规则。比如以下三种;

Nnon-go
Uunused-packages
Tgo-tests

revision :修正版本

version :版本号

对于整个[[projects]] ,有5个是必须有的。

属性是否必须?
nameY
packagesY
sourceN
revisionY
versionN
branchN
pruneoptsY
digestY
小结:
  • dep check将快速报告您的项目不同步的任何方式。
  • dep ensure -update 是更新依赖项的首选方法,但对于不发布semver版本的项目效率较低。
  • dep ensure -add通常是引入新依赖项的最简单方法,但您也可以添加新import语句然后运行dep ensure
  • 如果您进行手动更改Gopkg.toml,最好运行dep ensure以确保所有内容都保持同步。
  • dep ensure几乎从来不会运行错误;如果你不确定发生了什么,运行它会让你回到安全的版本,或者失败。

此外,:

  • 在Go工具链中,通常在您自己的项目中避免符号链接。dep可以容忍这一点,但是就像Go工具链本身一样,它通常不太支持符号链接。
  • 切勿直接编辑任何内容vendor/; dep将无条件地覆盖此类更改。如果您需要修改依赖项,fork它并正确执行它。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值