欢迎小伙伴们观看我的文章,若文中有不正确的地方,小伙伴们可以在评论区处指出,希望能够与小伙伴们一起成长与进步,谢谢!
一、包
1、包的概念
在Go语言中,使用包(package)
来支持代码的模块化与复用化。一个包是由一个或多个go
源码文件(以.go
结尾的文件)组成,而go
程序也都是由一个或多个包组成。在Go语言中提供了许多的内置包,例如fmt
、os
、io
等,而fmt
包则是我们经常使用的一个内置包。
一般情况下包的名称与其go
文件所在的目录名称相同,虽然没有强制要求包名必须和其go
文件所在的目录名同名,但建议包名和所在目录同名,可以使结构更清晰。
2、包的定义
包可以简单的理解为存放.go
文件的文件夹,在当前包下的.go
文件都需要在第一行中添加包的声明,声明当前.go
文件归属的包。
go
package packagename
上述声明中:
package
:Go
中声明包的关键字packagename
:包名,可以与目录名称不一致,且不能包含-
符号,且包的命名最好与其对应实现的功能相对应。
任何go
源代码文件必须属于某个包,通过上述语句声明当前go
文件自己所在的包。
3、可见性
在同一个包内声明的标识符 (Go语言中的命名对象,包括变量、常量、函数、类型、结构体等)都位于这个包的命名空间下。如果想要在包外使用包内的标识符就需要添加包名前缀 ,例如fmt.Println("Hello world!")
,即调用fmt
包中的Println
函数。
若一个包中的标识符能够被外部包所访问与使用,则标识符必须是对外可见的(public
)。在Go语言中,通过标识符的首字母大小写 来控制标识符的对外是否可见(public
、private
)。标识符首字母大写时,表示对外可见。
举个例子,同一目录下定义两个demo1
包与main
包,在两个包下分别创建demo1.go
与main.go
文件,demo1.go
的代码如下:
go
package demo1
import "fmt"
// number 定义一个全局整型变量
// 首字母小写,对外不可见(只能在当前包内使用)
var number = 100
// Str 定义一个常量
// 首字母大写,对外可见(可在其它包中使用)
const Str = "字符串常量"
// user 定义用户的结构体
// 首字母小写,对外不可见(只能在当前包内使用)
type user struct {
name string
age int
}
// Add 返回两个整数和的函数
// 首字母大写,对外可见(可在其它包中使用)
func Add(x, y int) int {
return x + y
}
// printHello 定义一个打印函数
// 首字母小写,对外不可见(只能在当前包内使用)
func printHello() {
fmt.Println("Hello")
}
此时在main.go
,只能访问demo1
包中,包外可见的标识符。
go
package main // 声明为main包,提供程序的入口
import (
"fmt"
"go-practise/demo1"
)
func main() {
fmt.Println(demo1.Str) // 字符串常量
fmt.Println(demo1.Add(10, 20)) // 30
}
上述代码中,导入了demo1
包并访问了demo1
包中可见的标识符。这里就涉及到包的导入。
4、包的导入
标准引用
如果需要在当前包中使用外部包的内容,可以通过使用import
关键字引入需要使用的包,import
语句通常放在go
文件开头,package声明语句的下方。
go
import packagename "path/package"
import
:导入语句,通常放在go文件开头包声明语句的下面;packagename
:自定义包名,可以通过自定义包名为导入的包重新命名,可以防止同包名导入产生歧义。通常都省略,默认值为引入包的包名。path/package
:引入包的路径名称,需要使用双引号。
Go文件中可以引入多个包,例如:
go
import "fmt"
import "net/http"
import "os"
另外,也可以使用括号进行批量引入,例如:
go
import (
"fmt"
"net/http"
"os"
)
自定义别名引用
当引入的多个包中存在相同的包名或者想为某一个包设置一个新包名时,可以通过packagename
指定一个在当前go文件中使用的新包名。例如,在引入fmt
包时为其指定一个新包名f
。
go
import f "fmt"
在定义完新包名后,在go文件中则可以使用这个新包名。
go
f.Println("Hello world!")
省略引用
go
import . "fmt"
省略引用可以将导入的包直接合并到当前程序,在使用该包时可以不用加包名前缀,直接引用。
go
import . "fmt"
func main() {
Println("Hello World!")
}
匿名引用
如果引入一个包时为,在其引入的路径名称前设置_
标识作为包名,这种包的引入方式就称为匿名引入。
一个包被匿名引入的目的主要是为了加载这个包,从而使得这个包中的资源得以初始化。被匿名引入的包中的init
函数将被执行并且仅执行一遍。
匿名引入的包与其他方式导入的包一样都会被编译到可执行文件中。
go
import _ "github.com/go-sql-driver/mysql"
在Go中,如果引入包却不使用该包的内容,则会触发编译错误,如果包中有 init
初始化函数,则使用匿名引用的方式(import _ "包的路径"
)来执行包的初始化函数,即使包没有 init
初始化函数,也不会引发编译器报错。
5、init函数
在Go文件中,可以定义init
特殊函数。当一个包被导入时,其中的每个init
函数都会按照它们在源文件中的顺序被调用。这些init
函数会在包的变量初始化之后、main
函数执行之前被调用。
go
func init() {
}
init
函数不接收任何参数且没有任何返回值,不能够主动调用init
函数。
go
package main
import "fmt"
var number int = 100
const pi = 3.1415926
func init() {
fmt.Println(number)
fmt.Println(pi)
printHello()
}
func printHello() {
fmt.Println("hello world")
}
func main() {
fmt.Println("main")
}
// 执行结果
100
3.1415926
hello world
main
一个包的初始化过程是按照导入顺序来进行初始化的,当前包声明的所有init
函数会被串行调用并且每个init
函数仅调用一次。包初始化时先执行依赖的包中声明的init
函数后,再执行当前包中声明的init
函数。确保在程序的main
函数开始执行前,所有的依赖包都已初始化完成。
上述流程图中,main包导入了a包,a包导入了b包,b包导入了c包,init
函数的执行顺序为c包init() -> b包init() -> a包init() -> main包init()
。
二、go module
1、go module介绍
在早期编写Go项目时,需要将Go项目代码依赖的所有第三方包放入GOPATH
目录下,这是方式的缺点在于不支持版本管理,同一个依赖包只能存在一个版本的代码。而多个项目下可能会分别使用不同版本的依赖包。
在Go1.11版本中,发布了Go module
的依赖管理方式,可以帮助开发人员更方便地管理项目依赖,并且保证依赖的版本管理和代码的可复用性。Go module在Go1.14 版本开始推荐在生产环境使用,于Go1.16版本默认开启。
具体来说,Go module
将依赖包版本信息和程序代码本身实现分离管理,每个Go module
都会有一个go.mod
文件,该文件包含了module
的依赖包列表以及对应的版本信息,当一个module
需要引用其他依赖包时,会根据go.mod
文件中的信息去下载对应的依赖包和对应版本的代码供程序使用。
2、go module相关命令
命令 | 说明 |
---|---|
go mod init |
初始化项目依赖,生成go.mod 文件 |
go mod download |
下载go.mod 文件中指明的所有依赖 |
go mod tidy |
检查项目中的依赖关系,项目文件中引入的依赖与go.mod 进行比对 |
go mod graph |
打印出模块依赖图,显示项目中所有模块及其之间的依赖关系 |
go mod edit |
编辑go.mod 文件 |
go mod vendor |
将当前项目的依赖项复制到项目的 vendor 目录中,以便离线构建和依赖管理 |
go mod verify |
校验项目中的依赖项是否被篡改过,以保证项目依赖的完整性和安全性 |
go mod why |
显示指定模块依赖关系的原因,即为什么当前模块需要依赖该指定模块 |
3、go module相关env配置
在Go module中,可以通过go env
命令查看常用环境变量。
GO111MODULE
在环境变量中,GO111MODULE
变量作为Go modules
的开关,其允许设置如下参数:
auto
:项目若包含go.mod文件,则启用Go modules
;on
:启用Go modules
,go命令行会使用modules,而不会去GOPATH
目录下查找依赖包,推荐设置;off
:禁用Go modules
,寻找依赖包的方式沿用旧版本通过vendor
目录或者GOPATH
模式来查找,不推荐设置;
如果需要对GO111MODULE
环境变量的值进行变更,则可以使用如下命令:
bash
go env -w GO111MODULE=on
GOPROXY
GOPROXY
环境变量主要用于设置Go模块代理(Go module proxy
),使Go在后续拉取依赖时能够脱离传统的VCS(版本控制系统)方式,直接通过镜像站点快速拉取。
GOPROXY
的默认值是:https://proxy.golang.org,direct
,该站点在国内无法正常访问,因此在开启Go modules时,需要设置国内的Go模块代理。目前社区使用比较多的有两个https://goproxy.cn
和https://goproxy.io
。执行命令如下:
bash
go env -w GOPROXY=https://goproxy.cn,direct
GOPROXY
允许设置多个模块代理地址,多个地址之间以英文逗号","分隔开,若不想使用,则可以设置为"off",这将会禁止Go在后续操作中使用任何的Go模块代理。
direct
是一个特殊指示符,用于只是Go回到模块版本的源地址去抓取,例如GitHub。当配置多个代理地址时,值代理地址列表中上一个Go模块代理地址返回404 或 410 错误时,Go 会自动尝试下一个模块代理地址,当遇到 direct
时会回到源地址去抓取,当遇到EOF
时中止并抛出错误。
GOSUMDB
GOSUMDB
环境变量(Go checksum database)在拉取依赖版本时(无论是在源站点还是Go module proxy)保证拉取的模块版本数据未经过篡改,若发现不一致,即可能存在篡改,则会立即中止拉取。
GOSUMDB
的默认值为sum.golang.org
,该站点在国内无法访问,不过可以通过之前设置的GOPROXY
解决,GOPROXY
设置的代理goproxy.cn
同样支持代理sum.golang.org
。
若该值设置为off
,则禁止GO在后续操作中校验模块版本。
4、使用go module
通过一个案例,在开发项目时可以更好的理解和使用go module拉取和管理依赖。
在本地新建一个名为testProject
的项目目录,并切换到该目录下:
bash
$ mkdir testProject
$ cd testProject
在testProject
项目目录下,初始化创建一个go.mod
文件。
bash
$ go mod init testProject
go: creating new go.mod: module testProject
使用该命令后自动会在项目目录下创建一个go.mod
文件,内容如下:
go
module testProject
go 1.19
module testProject
:表示当前项目的导入路径;go 1.19
:表示当前项目使用的Go版本;
go.mod
文件会记录项目中使用的第三方依赖包信息,包括包名和版本,但由于目前项目并未使用到任何第三方包,因此go.mod
文件暂时还没有记录任何依赖包信息。
可以在项目的目录下创建一个main.go
文件,然后使用go get
命令手动下载一个依赖包来测试一下:
bash
$ go get -u github.com/labstack/echo
go: added github.com/labstack/echo v3.3.10+incompatible
go: added github.com/labstack/gommon v0.4.1
go: added github.com/mattn/go-colorable v0.1.13
go: added github.com/mattn/go-isatty v0.0.20
go: added github.com/valyala/bytebufferpool v1.0.0
go: added github.com/valyala/fasttemplate v1.2.2
go: added golang.org/x/crypto v0.16.0
go: added golang.org/x/net v0.19.0
go: added golang.org/x/sys v0.15.0
go: added golang.org/x/text v0.14.0
可以看到调用go get
命令后,将依赖添加到项目中,前缀 go: added
表示成功添加了一个新的依赖包。此时go.mod中的内容:
go
module testProject
go 1.19
require (
github.com/labstack/echo v3.3.10+incompatible // indirect
github.com/labstack/gommon v0.4.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.16.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
)
从上述go.mod
文件中记录的当前项目中所有依赖包的相关信息可知,声明依赖的格式如下:
go
require module/path v1.2.3
require
:声明依赖包的关键字;module/path
:依赖包的导入路径;v1.2.3
:依赖包的版本号,支持latest
最新版本、详细版本v1.2.3
、指定某次commit hash
这几种格式。
在导入了指定的依赖包后,此时可以在main.go文件中使用导入的依赖包,例如:
go
package main
import (
"github.com/labstack/echo"
"net/http"
)
func main() {
e := echo.New()
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})
e.Logger.Fatal(e.Start(":8080"))
}
使用go build
生成.exe
可执行文件后,执行生成的可执行文件后,在浏览器中输入http://localhost:8080/
则可以看到Hello, World!
打印。
通过案例可以了解到,在创建项目后,如果需要使用go module
,则需要将go env
中的GO111MODULE
环境变量设置成on
,打开go module
模式。打开后,可以到项目目录下,执行go mod init
初始化生成 go.mod
文件。如果需要导入指定版本的第三方依赖包,则可以使用go get
命令进行下载。
执行go get
命令,在下载依赖包的同时还可以指定依赖包的版本。
go get -u
命令会将项目中的依赖包升级到最新的次要版本或者修订版本;go get -u=patch
命令会将项目中的依赖包包升级到最新的修订版本;go get [包名]@[版本号]
命令会下载对应依赖包的指定版本或者将对应包升级到指定的版本,例如go get foo@v1.2.3
。
go module
安装依赖包时先拉取最新的 release tag
,若无 tag
则拉取最新的 commit
,且go项目会自动生成一个 go.sum
文件来记录 dependency tree
。go module
引入了go.sum
机制来对依赖包进行校验。
另外,go module
会把下载到本地的依赖包会以类似下面的形式保存在 $GOPATH/pkg/mod
目录下,每个依赖包都会带有版本号进行区分,这样就允许在本地存在同一个包的多个不同版本。
bash
mod
├── cache
├── cloud.google.com
├── github.com
└──google
├── uuid@v1.1.2
├── uuid@v1.3.0
└── uuid@v1.3.1
...
如果想清除所有本地已缓存的依赖包数据,可以执行 go clean -modcache
命令。