
产品经理核心能力训练营
来自 网络整理·免费领取学习资源
这份资料来自网络整理2026年的付费VIP课,内容以视频为主,对应《产品经理核心能力训练营》。官网需要单独购买,有集上可以免费领取。
大部分编程语言都有将代码组织到命名空间和库的系统,Go也不例外。在学习其它特性时我们看到了,Go对这些老思想引入了新方法。本章中,读者会学习到如何通过包和模块组织代码、如何导入、如何使用第三方库以及如何创建自有库
仓库、模块和包
Go语言的库管理有三个基础概念:仓库、模块和包。所有开发者对仓库都很熟悉了。它是存储项目源码的版本控制系统。模块是按独立单元分发和打版本的Go源码包。模块存储于仓库中。模块由一个或多个包组成,也就是源代码的目录。包进行模块的组织和结构化
注: 虽然可以在仓库中存储多个模块,不建议这么做。模块中的所有内容都在一起进行版本管理。在一个仓库中维护两个模块会要在单个仓库中分别追踪两个模块
在使用标准库之外包的代码前,需要确保模块创建正常。每个模块都有一个全局唯一标识符。这不只限于Go语言。Java中使用com.companyname.projectname.library这样的全局唯一 包声明
在Go语言中,这称之为模块路径。通常是以模块所存储的仓库为基础。例如,作者用Go语言所写的简化关系数据库访问工具Proteus,模块路径为github.com/jonbodner/proteus
注:在Go开发环境配置一章中,我们创建了名为hello_world的模块,显然它不是全局唯一的。如果创建的模块只在本地使用这完全没有问题。如果将非唯一名称模块添加到源代码仓库中的话,就无法由另一个模块导入
go.mod
在Go源代码目录中有有效go.mod文件时就成为了模块。我们不用手动创建这一文件,可以使用go mod的子命令管理模块。go mod init *MODULE_PATH命令会让当前目录变成模块的根目录。MODULE_PATH*是用于标识模块的全局唯一名称。模块路径区分大小写。为减少困扰,请不要使用大写字母
我们来看看go.mod文件中的内容:
module github.com/learning-go-book-2e/money
go 1.21
require (
github.com/learning-go-book-2e/formatter v0.0.0-20220918024742-18...
github.com/shopspring/decimal v1.3.1
)
require (
github.com/fatih/color v1.13.0 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
)
每个go.mod 文件都以指令开头,它由单词module和模块唯一路径构成。接着go.mod文件使用go指令指定了最小兼容Go版本。模块中的所有源代码必须与指定版本相兼容。例如,如果指定了比较老的版本1.12,编译器不会允许在数字字面量中使用下划线,因为这一特性在Go 1.13中才添加的。Go版本还控制着Go构建工具的特性。本系列文章中工具行为与写作时有最新版本Go 1.21相匹配。如果需要使用指定了更老版本的模块,可以会存在些微差别
go.mod中的下一个版块是require指令。只有在模块有依赖时才存在require指令。其中列出模块所依赖的模块及每个模块的最低版本。第一个require版块列举是模块的直接依赖。第二个是这些依赖模块的依赖模块。该版块中每一行依赖的后面会接一条// indirect注释。标记为间接的依赖与没标记的在功能上并没有差别,只是供查看go.mod文件的人查看使用。有一种场景会将直接依赖标记为间接,我们会在讨论go get的不同使用方式时讲解。在导入第三方代码一节中,我们会学习到更多有关添加和管理模块依赖的知识
在go.mod文件中,module、go和require是最常用的一些指令,但还有其它的指令。我们会在依赖重载一章讲到replace和exclude指令,还会在撤销模块指定版本中讲到retract指令
构建包
我们已经学习了如何将目录转化为模块,下面可以开始使用包来组织代码了。我们先了解import的原理,接着创建和组织包,然后Go包好的和不好的特性
导入和导出
示例程序中一直在使用import语句,但我们还没讨论Go语言中的导入与其它语言有什么区别。Go的import语句允许访问其它包导出的常量、变量、函数及类型。包的导出标识符(标识符是变量、常量、类型、函数、方式或结构体中字段的名称)必须使用import语句才能在其它包中使用
这就带来了一个问题:如何在Go中导出标识符呢?Go中并没有使用特殊的关键字,而是使用首字母大写来决定声明的包级标识符是否对其它包可见。以大写字母开头的标识符即为导出。相对应地,以小写字母或下划线开头的标识符仅能在所声明的包内使用
所有导出的内容都是包API的一部分。在导出标识符前,请确保意在对客户端开放。对所有导出标识符添加文档,并保持身后兼容,除非是真的要做大版本修改(参见对模块添加版本了解更多信息)
创建和访问包
在Go中创建包很容易。我们通过一个小程序进行演示。代码可参见GitHub。在package_example中,可以看到两个目录:math和do-format。math中有一个名为math.go的文件,包含如下内容:
package math
func Double(a int) int {
return a * 2
}
该文件的第一行称为package语句。包含关键字package和包名。package语句是Go源文件的第一个非空非注释行
在do-format目录中,有一个包含如下内容的formatter.go 文件:
package format
import "fmt"
func Number(num int) string {
return fmt.Sprintf("The number is %d", num)
}
注意这里包声明语句使用了名称format,但目录为do-format。如何使用包一会儿会讲解
最后,在根目录的main.go文件中有如下内容:
package main
import (
"fmt"
"github.com/learning-go-book-2e/package_example/do-format"
"github.com/learning-go-book-2e/package_example/math"
)
func main {
num := math.Double(2)
output := format.Number(num)
fmt.Println(output)
}
文件的第一行读者已经不陌生了。在本文之前我们代码的第一行都是package main。我们一会儿会讨论其含义
接下来为导入区。导入了三个包。第一个是标准库的fmt。我们在前面文章已经使用过。接下来两个导入指向程序内的包。在导入标准库以外的包时要指定导入路径。导入路径为包路径接模块路径
导入包却未使用其所导出的任一标识符会报编译时错误。这可以保障Go编译器所生成的二进制文件只包含程序中实际使用的代码
警告:虽然可以使用相对路径导入同一模块的依赖包,请不要这么做。绝对导入路径让导入变得清晰,也让代码重构变简单。使用相对路径在将导入移到另一个包时需要进行修改,而如果将文件移到另一个模块的话,则必须让导入引用变成绝对路径
运行这段程序,会得到如下输出:
$ go build
$ ./package_example
The number is 4
main函数通过在函数名前加包名调用了math包中的Double函数。在前面文章中调用标准库中的函数已经使用过了。我们还调用了format包中的Number函数。读者可能会想format包是从哪来的呢?因为导入中用的是github.com/learning-go-book-2e/package_example/do-format
同一目录中的所有Go文件必须使用相同的包声明。(存在特例情况,比如目录中包含测试文件)。我们通过路径github.com/learning-go-book-2e/package_example/do-format导入的是format包。这是因为包名由包的声明决定,而不是导入路径
通常应当让包名与目录名一致。如两者不一致就很难知道包名是什么。但是,有一些场景会要求使用的包名和目录名不一样
第一种我们一直在做,可能都没意识到。那就是声明Go语言入口的包,包名为main。因为不能导入main包,这并不会产生导入语句的困扰
包名与目录名不一致的另一个原因不那么常见。如果目录名称中包含的字符不是合法的Go标识符,那么就需要选择一个与目录名不同的包名。本例中,format不是有效的标识名,所以使用format替换。最好避免创建不是有效标识符的目录名
最后一个原因是使用目录来支持版本。我们会在对模块添加版本一节中进一步讨论
包名位于文件作用域中。如在同一个包的不同文件中使用相同包名,则必须导入这些文件的包
贡献者