likes
comments
collection
share

探索Go包管理与Go module机制解析~

作者站长头像
站长
· 阅读数 13

欢迎小伙伴们观看我的文章,若文中有不正确的地方,小伙伴们可以在评论区处指出,希望能够与小伙伴们一起成长与进步,谢谢!

一、包

1、包的概念

在Go语言中,使用包(package)来支持代码的模块化与复用化。一个包是由一个或多个go源码文件(以.go结尾的文件)组成,而go程序也都是由一个或多个包组成。在Go语言中提供了许多的内置包,例如fmtosio等,而fmt包则是我们经常使用的一个内置包。

一般情况下包的名称与其go文件所在的目录名称相同,虽然没有强制要求包名必须和其go文件所在的目录名同名,但建议包名和所在目录同名,可以使结构更清晰。

2、包的定义

包可以简单的理解为存放.go文件的文件夹,在当前包下的.go文件都需要在第一行中添加包的声明,声明当前.go文件归属的包。

package packagename

上述声明中:

  • packageGo声明包的关键字
  • packagename:包名,可以与目录名称不一致,且不能包含-符号,且包的命名最好与其对应实现的功能相对应。

任何go源代码文件必须属于某个包,通过上述语句声明当前go文件自己所在的包。

3、可见性

在同一个包内声明的标识符(Go语言中的命名对象,包括变量、常量、函数、类型、结构体等)都位于这个包的命名空间下。如果想要在包外使用包内的标识符就需要添加包名前缀,例如fmt.Println("Hello world!"),即调用fmt包中的Println函数。

若一个包中的标识符能够被外部包所访问与使用,则标识符必须是对外可见的(public)。在Go语言中,通过标识符的首字母大小写来控制标识符的对外是否可见(publicprivate)。标识符首字母大写时,表示对外可见。

举个例子,同一目录下定义两个demo1包与main包,在两个包下分别创建demo1.gomain.go文件,demo1.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包中,包外可见的标识符。

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声明语句的下方。

import packagename "path/package"
  • import:导入语句,通常放在go文件开头包声明语句的下面;
  • packagename:自定义包名,可以通过自定义包名为导入的包重新命名,可以防止同包名导入产生歧义。通常都省略,默认值为引入包的包名。
  • path/package:引入包的路径名称,需要使用双引号。

Go文件中可以引入多个包,例如:

import "fmt"
import "net/http"
import "os"

另外,也可以使用括号进行批量引入,例如:

import (
    "fmt"
    "net/http"
    "os"
)

自定义别名引用

当引入的多个包中存在相同的包名或者想为某一个包设置一个新包名时,可以通过packagename指定一个在当前go文件中使用的新包名。例如,在引入fmt包时为其指定一个新包名f

import f "fmt"

在定义完新包名后,在go文件中则可以使用这个新包名。

f.Println("Hello world!")

省略引用

import . "fmt"

省略引用可以将导入的包直接合并到当前程序,在使用该包时可以不用加包名前缀,直接引用。

import . "fmt"

func main() {
    Println("Hello World!")
}

匿名引用

如果引入一个包时为,在其引入的路径名称前设置_标识作为包名,这种包的引入方式就称为匿名引入。

一个包被匿名引入的目的主要是为了加载这个包,从而使得这个包中的资源得以初始化。被匿名引入的包中的init函数将被执行并且仅执行一遍

匿名引入的包与其他方式导入的包一样都会被编译到可执行文件中。

import _ "github.com/go-sql-driver/mysql"

在Go中,如果引入包却不使用该包的内容,则会触发编译错误,如果包中有 init 初始化函数,则使用匿名引用的方式(import _ "包的路径")来执行包的初始化函数,即使包没有 init 初始化函数,也不会引发编译器报错。

5、init函数

在Go文件中,可以定义init特殊函数。当一个包被导入时,其中的每个init函数都会按照它们在源文件中的顺序被调用。这些init函数会在包的变量初始化之后、main函数执行之前被调用。

func init() {
}

init函数不接收任何参数且没有任何返回值,不能够主动调用init函数。

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函数开始执行前,所有的依赖包都已初始化完成。

探索Go包管理与Go module机制解析~

上述流程图中,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环境变量的值进行变更,则可以使用如下命令:

go env -w GO111MODULE=on

GOPROXY

GOPROXY环境变量主要用于设置Go模块代理(Go module proxy),使Go在后续拉取依赖时能够脱离传统的VCS(版本控制系统)方式,直接通过镜像站点快速拉取。

GOPROXY 的默认值是:https://proxy.golang.org,direct,该站点在国内无法正常访问,因此在开启Go modules时,需要设置国内的Go模块代理。目前社区使用比较多的有两个https://goproxy.cnhttps://goproxy.io。执行命令如下:

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的项目目录,并切换到该目录下:

$ mkdir testProject
$ cd testProject

testProject项目目录下,初始化创建一个go.mod文件。

$ go mod init testProject
go: creating new go.mod: module testProject

使用该命令后自动会在项目目录下创建一个go.mod文件,内容如下:

module testProject

go 1.19
  • module testProject:表示当前项目的导入路径;
  • go 1.19:表示当前项目使用的Go版本;

go.mod文件会记录项目中使用的第三方依赖包信息,包括包名和版本,但由于目前项目并未使用到任何第三方包,因此go.mod文件暂时还没有记录任何依赖包信息。

可以在项目的目录下创建一个main.go文件,然后使用go get命令手动下载一个依赖包来测试一下:

$ 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中的内容:

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文件中记录的当前项目中所有依赖包的相关信息可知,声明依赖的格式如下:

require module/path v1.2.3
  • require:声明依赖包的关键字;
  • module/path:依赖包的导入路径;
  • v1.2.3:依赖包的版本号,支持latest最新版本、详细版本v1.2.3、指定某次commit hash这几种格式。

在导入了指定的依赖包后,此时可以在main.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 treego module 引入了go.sum机制来对依赖包进行校验。

另外,go module 会把下载到本地的依赖包会以类似下面的形式保存在 $GOPATH/pkg/mod目录下,每个依赖包都会带有版本号进行区分,这样就允许在本地存在同一个包的多个不同版本。

mod
├── cache
├── cloud.google.com
├── github.com
    	└──google
          ├── uuid@v1.1.2
          ├── uuid@v1.3.0
          └── uuid@v1.3.1
...

如果想清除所有本地已缓存的依赖包数据,可以执行 go clean -modcache 命令。

转载自:https://juejin.cn/post/7309392585678766130
评论
请登录