15.包管理
15.1 模块化
用任何语言开发,如果软件规模扩大,会编写大量的函数、结构体、接口等。而这些代码不可能全部写在同一个文件中,因此就会产生大量的文件。如果这些文件是杂乱无章,则会造成名称冲突、重复定义、难以检索、无法引用、共享不便、版本管理、代码如何复用等一系列问题。因此需要有方案来解决类似问题,Go里面的方案是采用模块化管理来解决前述问题。
15.2 包
模块,有时也称为包,它是某个功能或某个框架的基本单位。任何编程语言都会有一些内置包,它将一些基本功能封装为包形式提供给开发者使用,Go语言内置包可以在安装目录src文件夹查看。其主要特点如下所示:
- 包由多个文件和目录组成
- 使用关键字
package 包名来定义包名 - 包名一般使用小写且符合标识符要求
- 当前目录名和
package 包名中的包名不需要一致,但根据约定俗成原则,则建议保持一致 - 同级文件归属一个包,即每个包目录的当前目录中,只能统一使用同一个包名,否则则会编译出错
一般来说开发项目时,可以把功能相关的代码集中放置一个包里面。同一个目录就是同一个包,该包里面的变量、函数、结构体均互相可见,也可以直接使用。而跨目录则是跨包,使用时需要导入相应的包。
15.3 包管理
15.3.1 GOPATH
Go 1.11版本之前,项目依赖包存于GOPATH。GOPATH是一个环境变量,它指向一个目录,用于存放项目依赖包的源码。其默认值为$HOME/go。开发的代码在GOPATH/src目录中,编译这个目录的代码,生成的二进制文件放置GOPATH/bin目录中。但存在以下问题:
- GOPATH不区分项目,代码中任何导入的路径均从GOPATH作为根目录开始。如果存在多个项目,不同项目依赖不同的库的不同版本,则无法解决
- 所有项目的依赖都放在GOPATH中,则很难知道当前项目的依赖项有哪些
15.3.2 GOPATH+vendor机制
Go 1.5 引入Vendor机制。Vendor是将项目依赖包复制到项目中的vendor目录,在编译时使用项目中vendor目录的包进行编译,但依然不能解决不同项目依赖不同包版本问题。其中包的搜索顺序如下所示:
- 在当前包vendor目录中查找
- 向上级目录查找,直到
GOPATH/src/vendor目录 - 在
GOPATH目录查找 - 在GOROOT目录查找标准库
15.3.3 Go Modules
Go Modules 是从Go 1.11版本之后引入,到1.13版本之后已经成熟,因此Go Modules已经成为官方的依赖包管理解决方案。其优势如下所示:
- 不受GOPATH的限制,代码可放在任意目录
- 自动管理和下载依赖,且可以限制使用版本
- 不允许使用相对导入
我们通过GO111MODULE配置来控制Go Module模式是否开启,有以下三个选项
- on:开启Go Module功能,Go会忽略GOPATH和vendor目录,仅根据go.mod下载依赖项,在
GOPATH/pkg/mod目录中搜索依赖包
Go 1.13版本之后,默认开启。
- off:关闭Go Moudule功能,Go会从GOPATH和vendor目录中搜索依赖包
- auto:在
GOPATH/src外面构建项目且时,若根目录中存在go.mod文件时,则开启支持Go Module功能,否则使用GOPATH和vendor机制
15.4 常用内置包
Go语言为我们提供了很多内置包,以下为一些日常开发常用的包,如下所示:
fmt包实现了格式化的标准输入输出,与C语言的printf和scanf类似。其中fmt.Printf()和fmt.Println()是开发中使用最频繁的函数。io包提供了原始的I/O操作,定义了4个基本接口Reader、Writer、Closer、Seeker用于表示二进制流的读、写、关闭和寻址操作。这些接口封装底层操作,如果没有特殊说明,接口不能被视为线程安全。bufio包通过对io包的封装提供了数据缓冲功能,一定程度上减少了大数据读写带来的资源开销sort包为切片或自定义Map提供了排序功能,实现了4种排序算法插入排序、归并排序、堆排序、快速排序,依据数据结构自动选择最优的排序算法。strconv包实现基本数据类型的转换,如字符串与整型相互转换功能。os包实现了操作系统的访问功能,包括文件操作、进程管理、信号等功能sync包实现了多线程的锁机制以及同步互斥功能。flag包提供命令行参数定义和参数解析的功能。encoding包提供将某些数据转换为特定数据的功能,例如将数据转换为JSON、CSV、XML、base64等html/template包为WEB开发提供了模板语法功能,通过模板语法将Go语言某类数据类型转换为相应的HTML代码net/http包提供了HTTP服务,包括HTTP请求、响应和URL解析、HTTP客户端和服务端。strings包提供了字符串操作处理,包括字符串合并、查找、分割、比较、扩展名检查、索引、大小写等功能bytes包与strings包功能相同,仅仅是bytes包用于处理字节类型的切片数据reflect包提供了Go语言中的反射功能log包提供程序的日志功能,常用日志输出接口为Print、Fatal、Panictime包提供时间和日期功能testing包为Go语言提供了单元测试功能regexp包提供了正则表达式功能math包提供了基本的数学常量和运算函数
15.5 包命名
在Go语言中,包是以文件夹形式表现的。其语法如下所示:
package 包名
包命名规则如下所示:
- package:是Go语言的关键字,用于指定当前文件所属包
- 包名:代表包名
- 一个文件夹里面的所有go文件都属于同一个包
在Go语言中有一个非常特殊的包
main包,它不能被其他包导入,且里面必须存在main函数,否则程序没有入口,则无法运行。
15.6 包导入
在Go语言中使用import导入包。导入的包只能是该包里面的导出标识符。其中导出标识符可以是变量、常量、类型、函数或方法、接口等。
每个包在项目中都有唯一的导入路径,导入路径是告诉Go语言从哪里找到包,导入路径和包名称之间没有必然联系。如果一个程序需要导入多个包,每个包可以单独使用关键字import 和 import + 小括号 实现,包与包之间必须处于不同的行,示例如下所示:
// 导入包示例一:
import "fmt"
import "math"
// 导入包示例二:
import (
"fmt"
"math"
)
一般推荐方式二,代码比较简洁
下面来演示一下,目录结构如下所示:

示例代码如下所示:
- calc.go
package calc
import "fmt"
func Add[T int | float32 | float64 | string](x, y T) T {
return x + y
}
func Div[T int | float32 | float64](x, y T) T {
if y == 0 {
fmt.Println("除数为0")
}
return x / y
}
- log.go
package log
import "log"
func PrintInfo(message any) {
log.Print(message)
}
func PrintError(message any) {
log.Fatal(message)
}
15.6.1 绝对导入
package main
import (
"fmt"
"surpass.net/tools/calc"
)
func main() {
fmt.Println(calc.Add(10, 20))
}
15.6.2 别名导入
如果在导入的包里面存在同名的包,则可以使用别名导入来避免冲突。在对包进行命名别名时,新名字必须在包前面,且两者之间使用空格间隔,示例使用方法如下所示:
package main
import (
"fmt"
c "surpass.net/tools/calc" // 将导入的包重新命名为 c
)
func main() {
fmt.Println(c.Add("100", "200")) // 通过别名调用包里面的导出标识符
}
注意事项: 别名导入的包仅在当前go文件中有效,其他go文件在导入时还是以原有名称导入
15.6.3 相对导入
这种方式不推荐使用
package main
import (
"fmt"
c "./tools/calc"
)
func main() {
fmt.Println(c.Add("100", "200"))
}
如果是在启用了go.mod的环境使用相对导入,会提示
main.go:6:2: "./tools/calc" is relative, but relative import paths are not supported in module mode
15.6.4 点导入
这种方式适用于将包里面所有导出成员直接导入到本地,即在调用包里面的导出标识符时无须通过包名调用,但有可能会导致导入的标识符存在冲突,且同一个包在一行时不能同时使用别名导入和点导入。仍然不推荐使用
package main
import (
"fmt"
. "surpass.net/calc"
)
func main() {
fmt.Println(Add("100", "200"))
fmt.Println(Add(12, 13))
fmt.Println(Div(12, 6))
}
15.6.5 匿名导入
这种方式类似于别名导入,区别就是该别名为_,通过这种方式导入的包,则意味着该包无法使用。通常适用于在导入的包仅执行导入包里面的init()函数,其主要作用是做包的初始化。示例如下所示:
import (
"fmt"
_ "surpass.net/tools/calc"
)
15.6.6 导入本地其他项目
如果将calc包放到其他项目时,如何导入呢?例如将calc放置到x:\calc,同时在calc目录使用go mod init surpass.net/calc。其go.mod内容如下所示:
- calc 目录的go.mod内容
module surpass.net/calc
go 1.22.5
- main.go中的go.mod需要按以下进行修改
module surpass.net
go 1.22.5
require (
surpass.net/calc v1.0.0 // 后面的版本号可以随便定义,满足格式要求即可
)
replace surpass.net/calc => "F:\\calc" // replace 指令指定包的搜索路径,而不是去GOPATH/pkg/mod搜索
go.mod一些参数的详细说明如下所示:
- require:用于设置一个特定的模块版本
- indirect:表示该模块为间接依赖,即在当前应用程序中的import语句中,并没有发现这个模块的明确调用,可能是事先通过
go get下载,也可能是依赖的模块所依赖的 - exclude:用于排除使用中的一个特定的模块版本
- replace:用于将一个模块版本替换为另外一个模块版本
在main.go中用法如下所示:
package main
import (
"fmt"
"surpass.net/calc"
)
func main() {
fmt.Println(calc.Add("100", "200"))
fmt.Println(calc.Add(12, 13))
fmt.Println(calc.Div(12, 6))
}
代码运行结果如下所示:
$ go run main.go
100200
25
2
15.6.7 导入第三方包
日常开发过程中,除了自己写的包,也还有可能用到其他第三方的包,可以通过 https://pkg.go.dev/ 进行搜索和下载(例如:go get -u github.com/gin-gonic/gin),导入第三方包的示例代码如下所示:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
在下载第三方包之后,会生成一个go.sum文件,在里面详细列举当前项目直接或间接依赖的所有模块版本,同时也带有详细的SHA-256值,以确保Go在售后的操作中保证项目所依赖的模块版本不会被修改。
15.7 init函数
Go语言中有一个特殊的函数init(),它的执行优先级比main()函数要高,主要实现包的初始化。其具备以下特征:
- init()函数,既无参数也无返回值,不能被其他函数调用
- 包中的init()函数在main()函数之前执行
- 每个包中init()函数可以存在多个,且可以位于不同的文件中
- 一个文件中最多拥有一个init()函数
- 同一个包中的init()函数没有明确的执行顺序
- 不同包中的init()函数的执行顺序由导入顺序决定
由于init()函数主要功能是初始化一些操作,但由于同一个包里面init()函数执行顺序是无序的。因此,除非有必要,不要在同一个包里面定义多个init()函数。另外,init()函数和main()函数也不一定在同一个文件夹里面。
- import _ "package": 仅执行该包里面的init()函数,无法使用包里面的导出标识符
- import "package": 既执行该包里面的init()函数,也可以使用包里面的导出标识符
示例代码如下所示:
- calc.go
package calc
import "fmt"
func init() {
fmt.Println("package calc 中的 init()函数")
}
func Add[T int | float32 | float64 | string](x, y T) T {
return x + y
}
func Div[T int | float32 | float64](x, y T) T {
if y == 0 {
fmt.Println("除数为0")
}
return x / y
}
- log.go
package log
import (
"fmt"
"log"
)
func init() {
fmt.Println("package log 中的 init()函数")
}
func PrintInfo(message any) {
log.Print(message)
}
func PrintError(message any) {
log.Fatal(message)
}
- main.go
package main
import (
"fmt"
_ "surpass.net/tools/calc"
_ "surpass.net/tools/log"
)
func main() {
fmt.Println("main - 测试init()函数")
}
代码运行结果如下所示:
package calc 中的 init()函数
package log 中的 init()函数
main - 测试init()函数
本文同步在微信订阅号上发布,如各位小伙伴们喜欢我的文章,也可以关注我的微信订阅号:woaitest,或扫描下面的二维码添加关注:

作者: Surpassme
来源: http://www.jianshu.com/u/28161b7c9995/
http://www.cnblogs.com/surpassme/
声明:本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出 原文链接 ,否则保留追究法律责任的权利。如有问题,可发送邮件 联系。让我们尊重原创者版权,共同营造良好的IT朋友圈。

浙公网安备 33010602011771号