《快学 Go 语言》第 16 课 —— 包管理 GOPATH 和 Vendor

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 到目前位置我们一直在编写单文件代码,只有一个 main.go 文件。本节我们要开始朝完整的项目结构迈进,需要使用 Go 语言的模块管理功能来组织很多的代码文件。 细数 Go 语言的历史发展,模块管理经历了三个重要的阶段。

到目前位置我们一直在编写单文件代码,只有一个 main.go 文件。本节我们要开始朝完整的项目结构迈进,需要使用 Go 语言的模块管理功能来组织很多的代码文件。

细数 Go 语言的历史发展,模块管理经历了三个重要的阶段。第一阶段是通过全局的 GOPATH 来管理所有的第三方包,第二阶段是通过 Vendor 机制将项目的依赖包局部化,第三阶段是 Go 语言的最新功能 Go Module。

本节我们重点讲解前两个阶段,这两个阶段要求我们编写代码时必须在 GOPATH 下面对应的包路径目录里写。第三个阶段 Go Module 内容较新,也比较复杂需要另起一节单独讲解。

系统包路径
Go 语言有很多内置包,内置包的使用需要用户手工 import 进来。Go 语言的内置包都是已经编译好的「包对象」,使用时编译器不需要进行二次编译。可以使用下面的命令查看这些已经编译好的包对象在哪里。
image
该命令显示出来的后缀名为 .a 的文件就是已经编译好的包对象。

全局管理 GOPATH
Go 语言的 GOPATH 路径下存放了全局的第三方依赖包,当我们在代码里面 import 某个第三方包时,编译器都会到 GOPATH 路径下面来寻找。GOPATH 目录可以指定多个位置,不过用户一般很少这样做。如果你没有人工指定 GOPATH 环境变量,编译器会默认将 GOPATH 指向的路径设定为 ~/go 目录。用户可以使用下面的命令看看自己的 GOPATH 指向哪里
image
GOPATH 下有三个重要的子目录,分别是 src、pkg 和 bin 目录。src 目录存放第三方包的源代码,pkg 目录存放编译好的第三方包对象,bin 存放第三方包提供的二进制可执行文件。

image
当我们导入第三方包时,编译器优先寻找已经编译好的包对象,如果没有包对象,就会去源码目录寻找相应的源码来编译。使用包对象的编译速度会明显快于使用源码。

友好的包路径
Go 语言允许包路径带有网站域名,这样它就可以使用 go get 指令直接去相应的网站上拉去包代码。最常用的要数 github.com、gopkg.in、golang.org 这三个网址。
image
Go 语言不存在官方维护的集中包仓库,它将包的选择分散到开源社区网站。使用量最大的要数 github.com,我们平时使用的大部分第三方包都是来源于此。也可以使用自己公司提供的代码仓库,路径名用上公司代码仓库的域名即可。默认会使用 https 协议下载代码仓库 ,可以使用 -insecure 参数切换到 http 协议。

模块的标准结构
了解模块结构的最好办法就是看看别人的模块是怎么写的,这里我们来观察一下 mongo 包。使用下面的命令将 redis 的包下载本 GOPATH 目录下
image
进入到 GOPATH 目录下面的 src 子目录寻找刚刚下载的 mongo 包,你会发现目录层级和 go get 指令的包路径正好一一对应起来,目录下面还有更深的子目录。

image
打开代码中的任意一个文件你可以发现代码中的 package 声明的包名是 mgo,这个和当前的目录名称可以不一样,不过当前目录下所有的文件都是这同一个包名 mgo。同时我们还注意到即使是包内代码引用,还是使用了全路径来导入而不是相对导入,比如下图的 bson,虽然同属一个项目,但是它们好像根本就互不相识,要使用对方的的路径全称来打招呼。
image
当其它项目导入这个包时,import 语句后面的路径是 mongo 包的目录路径,而使用的包名却是这个目录下面代码中 package 语句声明的包名 mgo。
image

很不幸,例子中这个项目已经停止维护了,下面是它的文档中停止维护的声明。
image
它已经由另一个社区项目接手。如果你要使用 mongo 的包,请使用
image
编写第一个模块
下面我们尝试编写第一个模块,这个模块是一个算法模块,提供两个方法,一个是计算斐波那契数,一个用来计算阶乘。我们要将这个包放到 github.com 上,需要读者在 github.com 上申请自己的账户,然后创建自己的项目名叫 mathy。我的 github id 是 pyloque,于是这个项目的包名就是 github.com/pyloque/mathy。第一步在 GOPATH 里创建这个包目录
image
好,现在我们进入了包的目录下,开始编写代码吧,首先创建 mathy.go 文件,将下面的代码贴进去
image
现在这个包的功能都齐全了,下面来编写 main 函数使用它。我们可以去其它的任意空目录下编写下面的 main.go 文件,但是不可以在当前目录编写,因为同一个目录只能有同一个包名。比如我们在 mathy 目录下面创建一个子目录 cmd,将下面的代码贴到 cmd 目录下的 main.go 文件里。执行 go run cmd/main.go 运行观察结果
image
现在将代码提交到 github.com 上去吧,你最好已经比较熟悉 git 指令

$ git init
Initialized empty Git repository in /Users/qianwp/go/src/github.com/pyloque/mathy/.git/

$ git add --all

$ git commit -a -m 'first commit'
[master (root-commit) 7da8809] first commit
 2 files changed, 37 insertions(+)
 create mode 100644 cmd/main.go
 create mode 100644 mathy.go

$ git remote add origin https://github.com/pyloque/mathy.git

$ git push origin master
Counting objects: 5, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (5/5), 555 bytes | 555.00 KiB/s, done.
Total 5 (delta 0), reused 0 (delta 0)
remote:
remote: Create a pull request for 'master' on GitHub by visiting:
remote:      https://github.com/pyloque/mathy/pull/new/master
remote:
To https://github.com/pyloque/mathy.git
 * [new branch]      master -> master
AI 代码解读

打开你的 github 项目页看一看你刚刚提交的成果吧
image
这个项目提交到了 github.com 意味着全球的人都可以使用你的代码了,前提是人们愿意使用。
现在你可以将本地的 mathy 文件夹删除,然后执行一下 go get

$ go get github.com/pyloque/mathy
AI 代码解读

你会发现刚才删掉的 mathy 目录又出现了,因为 go get 指令会自动去 github.com 网站上拉取你刚才提交的项目代码。

Go 语言支持使用 . 和 .. 符号相对导入,但是不推荐使用。官方表示相对导入只应该用于本地测试,如果要正式发布一定需要修改为绝对导入。相对导入可以不必将代码放在 GOPATH 里面编写,所以会方便本地测试。但是将代码放到 GOPATH 里面写又能产生多大障碍呢?总之就是不推荐使用相对导入。

两个包的包名一样怎么办?
如果你的代码需要使用两个包,这两个包的路径最后一个单词是一样的,那该如何分清使用的是那个包呢?为了解决这个问题,Go 语言支持导入语句名称替换功能

import pmathy "github.com/pyloque/mathy"
import omathy "github.com/other/mathy"
AI 代码解读

无名导入
Go 语言还支持一种罕见的导入语法可以将其它包的所有类型变量都导入到当前的文件中,在使用相关类型变量时可以省去包名前缀。

package main

import "fmt"
import . "github.com/pyloque/mathy"

func main() {
  fmt.Println(Fib(10))
  fmt.Println(Fact(10))
}
AI 代码解读

但是这种用法很少见,而且非常不推荐使用,读者可以当着没看见完全不知道。

匿名导入
Go 语言还支持匿名导入,就是说你导入了某个第三方包,但是不需要显示使用它,这时就可以使用匿名导入。什么时候需要导入某个包而不使用呢?这是因为 Go 语言的代码文件中可以存在一个特殊的 init() 函数,它会在包文件第一次被导入的时候运行。
image
当我们使用数据库驱动的时候就会经常遇到匿名导入,第三方驱动包会在 init() 函数中将当前驱动注册到全局的驱动列表中,这样通过特定的 URI 就可以识别并找到相应的驱动来使用。

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
)
AI 代码解读

当我们使用 Go 语言自带的图像处理包时也会遇到匿名导入,在对图像进行编码解码的时候需要根据不同的图像编码选择不同的逻辑。

import (  
    "image"
    _ "image/gif"
    _ "image/png"
    _ "image/jpeg"
)
AI 代码解读

包名和目录名不一样
Go 语言允许包名和当前的目录名成不一样,在导入包的时候使用的是目录路径,但是在使用的时候应该使用目录下的包名。所以你会看到导入的路径尾部和真正使用时的包名前缀不一样。

import "github.com/json-iterator/go"

var json = jsoniter.ConfigCompatibleWithStandardLibrary
json.Marshal(&data)
AI 代码解读

为什么 json-iterator 会使用这样奇怪的包路径呢,因为它要支持多种语言的,直接将最后的目录名改成语言的名称更加易于辨识。

go get vs go build vs go install
Go 提供了三个比较的常用的指令用来进行全局的包管理。

go build: 仅编译。如果当前包里有 main 包,就会生成二进制文件。如果没有 main 包,build 指令仅仅用来检查编译是否可以通过,编译完成后会丢弃编译时生成的所有临时包对象。这些临时包包括自身的包对象以及所有第三方依赖包的包对象。如果指定 -i 参数,会将编译成功的第三方依赖包对象安装到 GOPATH 的 pkg 目录。

go install:先编译,再安装。将编译成的包对象安装到 GOPATH 的 pkg 目录中,将编译成的可执行文件安装到 GOPATH 的 bin 目录中。如果指定 -i 参数,还会安装编译成功的第三方依赖包对象。

go get:下载代码、编译和安装。安装内容包括包对象和可执行文件,但是不包括依赖包。

$ go get github.com/go-redis/redis
AI 代码解读

image
注意编译过程中第三方包的 main 包是不可能被编译的,安装的对象也就不可能包括第三方依赖包的可执行文件。

当我们使用 go run 指令来测试运行正在开发的程序时,如果发现启动了很久,这时候可以考虑先执行 go build -i 指令,将编译成功的依赖包都安装到 GOPATH 的 pkg 目录下,这样再次运行 go run 指令就会快很多。

$ go build -i
$ go run main.go
AI 代码解读

当我们使用的第三方包已经比较陈旧,可以使用 go get -u 指令拉取最新的依赖包。

$ go get -u github.com/go-redis/redis
AI 代码解读

局部管理 Vendor
当我们在本地要开发多个项目时,如果不同的项目需要依赖某个第三方包的不同版本,这时候仅仅通过全局的 GOPATH 来存放第三方包是无解的。解决方法有一个,那就是需要在不同的项目里设置不同的 GOPATH 变量来解决冲突问题。但是这还是不能解决一个重要的问题,那就是当我们的项目依赖了两个第三方包,这两个第三方包又同时依赖了另一个包的两个不同版本,这时候就会再次发生冲突。这种多版本依赖有一个专业的名称叫「钻石型」依赖。

image
为了解决这个问题,Go 1.6 引入了 vendor 机制。这个机制非常简单,就是在你自己项目的目录下增加一个名字为 vendor 子目录,将自己项目依赖的所有第三方包放到 vendor 目录里。这样当你导入第三方包的时候,优先去 vendor 目录里找你需要的第三方包,如果没有,再去 GOPATH 全局路径下找。

image
然后每个第三方项目都会有自己的 vendor 子目录,如此递归下去,可以想象,一个大型项目将会有一颗很深的依赖树。不过实际上这颗依赖数没你想象的那么深,因为 Go 的第三方开源包普遍比较轻量级,依赖不是很多。毕竟 Go 语言已经将很多互联网常用的工具包都内置了。

使用 vendor 有一个限制,那就是你不能将 vendor 里面依赖的类型暴露到外面去,vendor 里面的依赖包提供的功能仅限于当前项目使用,这就是 vendor 的「隔离沙箱」。正是因为这个沙箱才使得项目里可以存在因为依赖传递导致的同一个依赖包的多个版本。同时这也意味着项目里可能存在多份同一个依赖包,即使它们是同一个版本。比如你的包在 vendor 里引入了某个第三方包 A,然后别人的项目在 vendor 里引入你的包,同时它也引入第三方包 A。这就会导致生成的二进制文件变大,也会导致运行时内存变大,不过也无需担心,这点代价对于服务端程序来说基本可以忽略不计。

讲到这里还有一个很重要的问题没有解决,github 上有很多开源项目,这些项目都有多个版本号,我如何引入具体某一个版本呢?如果使用 go get 指令,它总是引入 master 分支的最新代码,它往往不是稳定的可靠代码。这就需要 Go 语言的依赖管理工具的支持了,它就好比 java 语言的 maven 工具,python 语言的 pip 工具。

Dep

Go 语言没有内置 vendor 包管理工具,它需要第三方工具的支持。这样的工具很多,目前最流行的要数 golang/dep 项目了,它差一点就被官方收纳为内置工具了,很可惜!上图是它的 Logo,图中叠起来的箱子就是 dep 正在管理的各种第三方依赖包。使用它之前我们需要将 dep 工具安装到 GOPATH 下面
image
同时需要将 ~/go/bin 目录加入到环境变量 PATH 中,因为 dep 可执行文件默认会安装到 ~/go/bin 中。但是令人意外的是 dep 居然表示不能直接解决「钻石型」依赖,这让我感受到了它的危机,在 dep 中依赖包是扁平化的,vendor 不允许嵌套。如果出现了版本冲突,需要使用某种特殊手段来解决。

配置文件
dep 管理的项目会有两个配置文件,分别是 Godep.toml 和 Godep.lock。Godep.toml 用于配置具体的依赖规则,里面包含项目的具体版本号信息。通过 toml 配置文件,你即可以使用远程的依赖包(github),也可以直接使用本地的依赖包(GOPATH)。还可以为依赖包指定别名,这样就可以在代码里使用和真实路径不一样的导入路径。当你需要切换依赖包的不同版本时,可以在 toml 配置文件里修改依赖的版本号,然后通过 dep ensure 指令来更新依赖项。

Gopkg.lock 是基于当前的 toml 文件配置规则和项目代码来生成依赖的精确版本,它确定了 vendor 文件夹里要下载的依赖项代码的目标版本。

dep init
该指令用于初始化当前的项目,它会静态分析当前的项目代码(如有有的话),生成 Godep.toml 和 Godep.lock 依赖配置文件,将依赖的项目代码下载到当前项目的 vendor 文件夹里面。它会根据一定的策略来选择最新的依赖包版本。如果自动策略生成的版本号不是你想要的,可以再修改配置文件执行 dep ensure 来切换其它版本。

dep ensure
该指令会下载代码里用到的新依赖项、移除当前项目代码里不使用的依赖项。确保当前的依赖包代码和当前的项目代码配置处于完全一致的状态。

dep ensure -update
更新 Godep.lock 文件中的所有依赖项到最新版本。可以增加 一到多个包名参数,指定更新特定的依赖包。如果 toml 配置文件限定了依赖包的版本范围,那么更新必须遵守 toml 规则的版本限制。

dep ensure -add github.com/a/b
增加并下载一个新的项目依赖包,可以指定依赖版本号。如 dep ensure -add github.com/a/b@master 或者 github.com/a/b@1.0.0

dep status
显示当前项目的依赖状态。

Dep 在使用起来比较简单,但是其内部实现上是一个比较复杂的工具,鉴于篇幅限制,本节就不再继续深入讲解 Dep 了,以后有空再单独开启一篇来深入探讨吧。我甚至觉得理解 Dep 已经变得没有那么必要,因为它已经被 Go 语言官方抛弃了,取而代之的解决方案是 Go Module。

原文发布时间为:2018-12-27
本文作者: 老钱
本文来自云栖社区合作伙伴“ 码洞”,了解相关信息可以关注“
codehole”微信公众号

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
打赏
0
0
0
0
73529
分享
相关文章
监控局域网其他电脑:Go 语言迪杰斯特拉算法的高效应用
在信息化时代,监控局域网成为网络管理与安全防护的关键需求。本文探讨了迪杰斯特拉(Dijkstra)算法在监控局域网中的应用,通过计算最短路径优化数据传输和故障检测。文中提供了使用Go语言实现的代码例程,展示了如何高效地进行网络监控,确保局域网的稳定运行和数据安全。迪杰斯特拉算法能减少传输延迟和带宽消耗,及时发现并处理网络故障,适用于复杂网络环境下的管理和维护。
揭秘 Go 语言中空结构体的强大用法
Go 语言中的空结构体 `struct{}` 不包含任何字段,不占用内存空间。它在实际编程中有多种典型用法:1) 结合 map 实现集合(set)类型;2) 与 channel 搭配用于信号通知;3) 申请超大容量的 Slice 和 Array 以节省内存;4) 作为接口实现时明确表示不关注值。此外,需要注意的是,空结构体作为字段时可能会因内存对齐原因占用额外空间。建议将空结构体放在外层结构体的第一个字段以优化内存使用。
|
21天前
|
Go 语言入门指南:切片
Golang中的切片(Slice)是基于数组的动态序列,支持变长操作。它由指针、长度和容量三部分组成,底层引用一个连续的数组片段。切片提供灵活的增减元素功能,语法形式为`[]T`,其中T为元素类型。相比固定长度的数组,切片更常用,允许动态调整大小,并且多个切片可以共享同一底层数组。通过内置的`make`函数可创建指定长度和容量的切片。需要注意的是,切片不能直接比较,只能与`nil`比较,且空切片的长度为0。
Go 语言入门指南:切片
eino — 基于go语言的大模型应用开发框架(二)
本文介绍了如何使用Eino框架实现一个基本的LLM(大语言模型)应用。Eino中的`ChatModel`接口提供了与不同大模型服务(如OpenAI、Ollama等)交互的统一方式,支持生成完整响应、流式响应和绑定工具等功能。`Generate`方法用于生成完整的模型响应,`Stream`方法以流式方式返回结果,`BindTools`方法为模型绑定工具。此外,还介绍了通过`Option`模式配置模型参数及模板功能,支持基于前端和用户自定义的角色及Prompt。目前主要聚焦于`ChatModel`的`Generate`方法,后续将继续深入学习。
172 7
|
16天前
|
企业监控软件中 Go 语言哈希表算法的应用研究与分析
在数字化时代,企业监控软件对企业的稳定运营至关重要。哈希表(散列表)作为高效的数据结构,广泛应用于企业监控中,如设备状态管理、数据分类和缓存机制。Go 语言中的 map 实现了哈希表,能快速处理海量监控数据,确保实时准确反映设备状态,提升系统性能,助力企业实现智能化管理。
29 3
Go 语言中的 Sync.Map 详解:并发安全的 Map 实现
`sync.Map` 是 Go 语言中用于并发安全操作的 Map 实现,适用于读多写少的场景。它通过两个底层 Map(`read` 和 `dirty`)实现读写分离,提供高效的读性能。主要方法包括 `Store`、`Load`、`Delete` 等。在大量写入时性能可能下降,需谨慎选择使用场景。
eino — 基于go语言的大模型应用开发框架(一)
Eino 是一个受开源社区优秀LLM应用开发框架(如LangChain和LlamaIndex)启发的Go语言框架,强调简洁性、可扩展性和可靠性。它提供了易于复用的组件、强大的编排框架、简洁明了的API、最佳实践集合及实用的DevOps工具,支持快速构建和部署LLM应用。Eino不仅兼容多种模型库(如OpenAI、Ollama、Ark),还提供详细的官方文档和活跃的社区支持,便于开发者上手使用。
130 8
Go语言实战:错误处理和panic_recover之自定义错误类型
本文深入探讨了Go语言中的错误处理和panic/recover机制,涵盖错误处理的基本概念、自定义错误类型的定义、panic和recover的工作原理及应用场景。通过具体代码示例介绍了如何定义自定义错误类型、检查和处理错误值,并使用panic和recover处理运行时错误。文章还讨论了错误处理在实际开发中的应用,如网络编程、文件操作和并发编程,并推荐了一些学习资源。最后展望了未来Go语言在错误处理方面的优化方向。
阿里双十一背后的Go语言实践:百万QPS网关的设计与实现
解析阿里核心网关如何利用Go协程池、RingBuffer、零拷贝技术支撑亿级流量。 重点分享: ① 如何用gRPC拦截器实现熔断限流; ② Sync.Map在高并发读写中的取舍。
|
18天前
|
基于 Go 语言的公司内网管理软件哈希表算法深度解析与研究
在数字化办公中,公司内网管理软件通过哈希表算法保障信息安全与高效管理。哈希表基于键值对存储和查找,如用户登录验证、设备信息管理和文件权限控制等场景,Go语言实现的哈希表能快速验证用户信息,提升管理效率,确保网络稳定运行。
27 0
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等