包(package) #
Go语言是使用包来组织源代码的,包(package)是多个 Go 源码的集合,是一种高级的代码复用方案。Go语言中为我们提供了很多内置包,如 fmt、os、io 等。
任何源代码文件必须属于某个包,同时源码文件的第一行有效代码必须是package pacakgeName语句,通过该语句声明自己所在的包。
包的习惯用法 #
- 包可以定义在很深的目录中,包名的定义是不包括目录路径的,但是包在引用时一般使用全路径引用。比如在
GOPATH/src/a/b/下定义一个包c。在包c的源码中只需声明为package c,而不是声明为package a/b/c,但是在导入 c 包时,需要带上路径,例如import "a/b/c" - 包名一般是小写的,使用一个简短且有意义的名称。
- 包名一般要和所在的目录同名,也可以不同,包名中不能包含
-等特殊符号。 - 包一般使用域名作为目录名称,这样能保证包名的唯一性,比如 GitHub 项目的包一般会放到
%GOPATH%/src/github.com/userName/projectName目录下。 - 包名为
main的包为应用程序的入口包,编译不包含main包的源码文件时不会得到可执行文件。 - 一个文件夹下的所有源码文件只能属于同一个包,同样属于同一个包的源码文件不能放在多个文件夹下。
包的导入 #
导入语法 #
//单行导入
import "fmt"
//多行导入
import(
"fmt"
"os"
)
导入路径 #
1、全路径 #
根目录是GOPATH/src/
import "lab/test"
import "database/sql/driver"
2、相对路径 #
相对路径只能用于导入GOPATH下的包,标准包的导入只能使用全路径导入。
相对于当前包所在路径
import "../a"
包的引用 #
在Golang中,一个包下存在多个源文件,若要在某个源文件中引入其他源文件声明的函数、结构体、全局变量等成员,引用规则如下:
- 同包:直接使用
成员名称引用 - 不同包:要引用的成员名称必须首字母大写,并且使用
包名.成员名称进行引用 internal包以及其子包中成员仅能在internal的父级包或兄弟包中引用。
例如有项目结构如下
- controller
- c1.go
- c2.go
- main.go
- go.mod
go.mod
module ygang.top/demo
go 1.21.5
c1.go
package controller
var Contro1Variable int
func Contro1() {
}
c2.go
package controller
func Contro2() {
Contro1()
}
main.go
package main
import "top.ygang/demo/controller"
func main() {
controller.Contro1()
controller.Contro2()
controller.Contro1Variable = 10
}
1、标准引用 #
直接使用包名来引用
import "fmt"
fmt.Println("hello")
2、别名引用 #
一般用于引用了两个同名包时区分使用
import F "fmt"
F.Println("hello")
3、省略引用 #
把 fmt 包直接合并到当前程序中,在使用 fmt 包内的方法是可以不用加前缀fmt.,直接引用
import . "fmt"
Println("hello")
4、匿名引用 #
在引用某个包时,如果只是希望执行包初始化的 init函数,而不使用包内部的数据时,可以使用匿名引用格式
import _ "fmt"
- 注意:
- 匿名导入的包与其他方式导入的包一样都会被编译到可执行文件中。
- 使用标准格式引用包,但是代码中却没有使用包,编译器会报错。如果包中有
init初始化函数,则通过import _ "包的路径"这种方式引用包,仅执行包的初始化函数,即使包没有 init 初始化函数,也不会引发编译器报错。
包加载流程 #
Go语言包的初始化有如下特点:
- 包初始化程序从 main 函数引用的包开始,逐级查找包的引用,直到找到没有引用其他包的包,最终生成一个包引用的有向无环图。
- Go 编译器会将有向无环图转换为一棵树,然后从树的叶子节点开始逐层向上对包进行初始化。
- 单个包的初始化过程如上图所示,先初始化常量,然后是全局变量,最后执行包的 init 函数。
封装 #
封装的好处:
- 隐藏实现细节;
- 可以对数据进行验证,保证数据安全合理。
如何体现封装:
- 对结构体中的属性进行封装;
- 通过方法,包,实现封装。
封装的实现步骤:
- 将结构体、字段的首字母小写;
- 给结构体所在的包提供一个工厂模式的函数,首字母大写,类似一个构造函数;
- 提供一个首字母大写的 Set 方法(类似其它语言的 public),用于对属性判断并赋值;
- 提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值。
init函数 #
init()函数会在每个包完成初始化后自动执行,并且执行优先级比main函数高。init()函数通常被用来:
- 对变量进行初始化
- 检查/修复程序的状态
- 注册
- 运行一次计算
init()函数无参数、无返回值
func init(){
}
整个项目执行顺序遵循:
- 依赖倒置原则:被导入的包优先初始化
- 包级别变量初始化 →
init()函数 →main()函数 - 文件内顺序:同一包内多个
init()按文件中的出现顺序执行 - 文件间顺序:同一包内多个文件的
init()按文件名字母顺序执行
包管理 #
GOPATH #
最初的时候Go语言所依赖的所有的第三方包都放在 GOPATH 目录下面,这就导致了同一个包只能保存一个版本的代码。
现已被弃用。
godep(govendor) #
注意:该方式现已不推荐使用,原因:放弃了依赖重用,使得冗余度上升。同一个依赖包如果不同工程想重用,都必须各自复制一份在自己的vendor目录下。
godep 是一个Go语言官方提供的通过 vender 模式来管理第三方依赖的工具,类似的还有由社区维护的准官方包管理工具 dep。
Go语言从 1.5 版本开始开始引入 vendor 模式,如果项目目录下有 vendor 目录,那么Go语言编译器会优先使用 vendor 内的包进行编译、测试等。
Go Modules #
Go Modules 是Go语言从 1.11 版本之后官方推出的版本管理工具,项目就不需要再放到GOPATH下了,并且从 Go1.13 版本开始,go module 成为了Go语言默认的依赖管理工具。
GOMODULE模式和GOPATH模式一样都指定了依赖包存放的位置,而不是如vendor模式放入工程内,区别在于GOMODULE的go.mod文件中记录了所依赖包的具体版本,既实现了工程之间重用依赖包,且解决了GOPATH模式下不同工程无法依赖同一个包的不同版本的问题。
Go Modules是Go语言的依赖管理系统,它解决了以下核心问题:
- 版本管理:明确指定依赖的版本
- 可重现构建:确保在不同环境下构建结果一致
- 依赖隔离:不同项目可使用同一个包的不同版本
- 中心化索引:通过proxy机制加速依赖下载
一个Go Module本质上是一个包含Go包的目录,它有一个go.mod文件定义了模块路径和依赖需求。
重要概念 #
在Go Modules中,我们需要理解几个核心概念:
- 模块(Module):一个版本化的代码单元,包含一个或多个Go包,由
go.mod文件定义。 - 包(Package):Go源码文件的集合,编译时的基本单位。
- 版本(Version):使用语义化版本标识(如
v1.2.3)的代码快照。 - 主版本(Major Version):语义化版本的第一个数字,不同主版本可能有不兼容的API变化。
例如,一个模块可能是github.com/user/project,它包含多个包,如github.com/user/project/pkg1和github.com/user/project/pkg2。
GO111MODULE #
在Go语言 1.12 版本之前,要启用 Go Modules 工具首先要设置环境变量 GO111MODULE,不过在Go语言 1.13 及以后的版本则不再需要设置环境变量。通过 GO111MODULE 可以开启或关闭 Go Modules 工具。
GO111MODULE=off:禁用 Go Modules,编译时会从 GOPATH 和 vendor 文件夹中查找包。GO111MODULE=on:(1.16后默认值) 启用 Go Modules,编译时会忽略 GOPATH 和 vendor 文件夹,只根据go.mod下载依赖。GO111MODULE=auto:(1.16前默认值),当项目在GOPATH/src目录之外,并且项目根目录有go.mod文件时,开启 Go Modules。
Windows 下开启 GO111MODULE 的命令为:
set GO111MODULE=on
#或者
set GO111MODULE=auto
MacOS 或者 Linux 下开启 GO111MODULE 的命令为:
export GO111MODULE=on
#或者
export GO111MODULE=auto
使用 Go Modules 的go mod init projectname命令后会在当前目录下生成一个go.mod 文件,并且在编译/运行当前目录下代码或者使用go get命令的时候会在当前目录下生成一个 go.sum 文件。
go.mod文件一旦创建后,它的内容将会被go toolchain全面掌控。go toolchain会在各类命令执行时,比如go get、go build、go mod等修改和维护go.mod文件。
go.mod #
go.mod 文件记录了项目所有的依赖信息,其结构大致如下:
module github.com/user/project
go 1.16
require (
github.com/pkg/errors v0.9.1
golang.org/x/text v0.3.6
)
replace github.com/pkg/errors => github.com/user/errors v0.9.1-custom
exclude golang.org/x/net v1.2.3
go.mod 文件内提供了module、go、require、replace和exclude五个关键字。
- module:声明模块路径,这是模块的唯一标识符
- go:声明编译模块所需的Go语言版本
- require:列出模块的依赖及其版本
- replace:替换依赖的模块路径或版本
- exclude:排除依赖的特定版本
在go.mod文件中,某些依赖可能标记为// indirect,表示:
- 这不是直接依赖,而是间接依赖
- 但由于某种原因(如传递依赖有冲突)需要在
go.mod中显式列出
语义化版本控制 #
Go Modules采用语义化版本控制(Semantic Versioning,简称SemVer)。版本号格式为:vX.Y.Z,其中:
- X:主版本号,变更表示不兼容的API修改
- Y:次版本号,变更表示向后兼容的功能性新增
- Z:修订号,变更表示向后兼容的问题修正
一些特殊的版本标识:
- v0.y.z:不稳定版本,API可能随时变化
- v1.0.0及以上:稳定版本,遵循向后兼容原则
- v2.0.0及以上:需要在模块路径中加入主版本后缀,如
/v2
Go Modules的一个重要概念是最小版本选择(Minimal Version Selection, MVS):当有多个模块依赖同一个模块的不同版本时,Go会选择其中最高的版本。
go.sum #
go.sum 文件则是用来记录每个依赖包的版本及哈希值,如下所示:
github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/OwnLocal/goes v1.0.0/go.mod h1:8rIFjBGTue3lCU0wplczcUgt9Gxgrkkrw7etMIcn8TM=
github.com/astaxie/beego v1.12.0 h1:MRhVoeeye5N+Flul5PoVfD9CslfdoH+xqC/xvSQ5u2Y=
github.com/astaxie/beego v1.12.0/go.mod h1:fysx+LZNZKnvh4GED/xND7jWtjCR6HzydR2Hh2Im57o=
常用的 go mod 命令 #
| 命令 | 作用 |
|---|---|
go mod download |
下载依赖包到本地(默认为 GOPATH/pkg/mod 目录) |
go mod edit |
编辑 go.mod 文件 |
go mod graph |
打印模块依赖图 |
go mod init |
初始化当前文件夹(项目),创建 go.mod 文件 |
go mod tidy |
增加缺少的包,删除无用的包 |
go mod vendor |
将依赖复制到 vendor 目录下 |
go mod verify |
校验依赖 |
go mod why |
解释为什么需要依赖 |
go get #
go get命令用于从远程代码仓库(比如 Github )上下载并安装代码包。
注意:如果GO111MODULE=on或者auto,在go get下载包时候,会下载到GOPATH/pkg/mod,引入时也是同样的从这个目录开始;如果运行了go env -w GO111MODULE=off,那么在go get下载包时候,会下载到GOPATH/src 目录下
常用命令标记:
-d # 让命令程序只执行下载动作,而不执行安装动作。
-f # 仅在使用-u标记时才有效。该标记会让命令程序忽略掉对已下载代码包的导入路径的检查。如果下载并安装的代码包所属的项目是你从别人那里 Fork 过来的,那么这样做就尤为重要了。
-fix # 让命令程序在下载代码包后先执行修正动作,而后再进行编译和安装。
-insecure # 允许命令程序使用非安全的 scheme(如 HTTP )去下载指定的代码包。如果你用的代码仓库(如公司内部的 Gitlab )没有HTTPS 支持,可以添加此标记。请在确定安全的情况下使用它。
-t # 让命令程序同时下载并安装指定的代码包中的测试源码文件中依赖的代码包。
-u # 让命令利用网络来更新已有代码包及其依赖包。默认情况下,该命令只会从网络上下载本地不存在的代码包,而不会更新已有的代码包。
- 运行
go get -u命令会将项目中的包升级到最新的次要版本或者修订版本; - 运行
go get -u=patch命令会将项目中的包升级到最新的修订版本; - 运行
go get [包名]@[版本号]命令会下载对应包的指定版本或者将对应包升级到指定的版本。
go get [包名]@[版本号]命令中版本号可以是x.y.z的形式,例如go get foo@v1.2.3,也可以是 git 上的分支或 tag,例如go get foo@master,还可以是 git 提交时的哈希值,例如go get foo@e3702bed2。
第三方包查询:https://pkg.go.dev/
私有模块与认证 #
私有仓库配置 #
在企业环境中,经常需要使用私有Git仓库。Go提供了几种访问私有模块的方式:
方法1:使用GOPRIVATE环境变量
# 设置不经过代理的私有仓库
export GOPRIVATE=github.com/mycompany/*,gitlab.com/mycompany/*
# 或者具体模块
export GOPRIVATE=github.com/mycompany/project
这告诉Go直接从源获取这些模块,而不是通过GOPROXY。
方法2:使用Git凭证
Go模块系统使用底层Git命令访问私有仓库,因此需要配置Git凭证:
# HTTPS认证
git config --global credential.helper store
# SSH认证
# 确保SSH密钥已经设置好并添加到Git服务
内部模块代理 #
对于大型组织,设置内部模块代理服务器是个好主意:
- 加速依赖下载
- 缓存依赖,防止外部依赖不可用
- 审计和控制使用的第三方依赖
可以使用Athens或GoCenter等开源项目搭建内部代理:
# 设置模块代理
export GOPROXY=https://goproxy.mycompany.com,direct
工作区模式 #
什么是工作区 #
工作区允许同时处理多个相关模块,特别适合以下场景:
- 开发相互依赖的多个模块
- 同时修改主项目和其依赖
- 大型单仓库(monorepo)项目
工作区的强大之处在于它覆盖了go.mod文件中的一些设置,但仅在本地开发环境中生效,不会影响已发布的模块。
创建工作区 #
Go 1.18引入了工作区(Workspace)模式,简化了多模块项目的开发:
# 创建工作区
go work init ./module1 ./module2
# 添加模块到工作区
go work use ./module3
# 移除模块
go work edit -dropuse=./module2
go.work #
go.work文件示例:
go 1.18
use (
./module1
./module2
)
replace github.com/user/module3 => ./module3