第18篇:包和依赖管理

一、包(package)

一般来说,一个文件夹可以作为 package,同一个 package 内部变量、类型、方法等定义可以相互看到。Go语言的包具有以下特点:

  1. package是Go语言的基本复用模块单元。

    注:以首字母大小写来区分是否可被包外代码访问。

大写代表,可以被包外引入。
小写代表,不可被包外引入。

  1. package名可以与目录名不一致。(但建议一致)

  2. 同一目录下的Go文件,必须属于同一个package。

1. 可见性

包中的变量、函数和方法等以首字母大小写来区分是否可被包外代码访问。

大写代表,可以被包外引入。
小写代表,不可被包外引入。

比如我们新建一个文件 calc.gomain.go 平级,分别定义 add 和 main 方法。

  • calc.go

    package main
    
    func add(num1 int, num2 int) int {
        return num1 + num2
    }
  • main.go

    package main
    
    import "fmt"
    
    func main() {
        fmt.Println(add(3, 5)) // 8
    }

    运行 go run main.go,会报错,add 未定义:

    ./main.go:6:14: undefined: add

    因为 go run main.go 仅编译 main.go 一个文件,所以命令需要换成

    $ go run main.go calc.go
    8

    $ go run .
    8

    Go 语言也有 Public 和 Private 的概念,粒度是包。如果类型/接口/方法/函数/字段的首字母大写,则是 Public 的,对其他 package 可见,如果首字母小写,则是 Private 的,对其他 package 不可见。

2. init和main函数

2.1 init函数

go语言中init函数用于包(package)的初始化,该函数是go语言的一个重要特性。

有下面的特征:

  • init函数是用于程序执行前做包的初始化的函数,比如初始化包里的变量等
  • 每个包可以拥有多个init函数
  • 包的每个源文件也可以拥有多个init函数
  • 同一个包中多个init函数的执行顺序go语言没有明确的定义(说明)
  • 不同包的init函数按照包导入的依赖关系决定该初始化函数的执行顺序
  • init函数不能被其他函数调用,而是在main函数执行之前,自动被调用

2.2 main函数

Go语言程序的默认入口函数(主函数)func main()
函数体用{}一对括号包裹。

func main(){
//函数体
}

2.3 init函数和main函数的异同

相同点:
两个函数在定义时不能有任何的参数和返回值,且Go程序自动调用。
不同点:
init可以应用于任意包中,且可以重复定义多个。
main函数只能用于main包中,且只能定义一个。

两个函数的执行顺序:

  • 对同一个go文件的init()调用顺序是从上到下的。
  • 对同一个package中不同文件是按文件名字符串比较“从小到大”顺序调用各文件中的init()函数。
  • 对于不同的package,如果不相互依赖的话,按照main包中”先import的后调用”的顺序调用其包中的init(),如果package存在依赖,则先调用最早被依赖的package中的init(),最后调用main函数。
  • 如果init函数中使用了println()或者print()你会发现在执行过程中这两个不会按照你想象中的顺序执行。这两个函数官方只推荐在测试环境中使用,对于正式环境不要使用。

二、依赖管理

1. 为什么使用是依赖管理

最早的时候,Go所依赖的所有的第三方库都放在GOPATH这个目录下面。这就导致了同一个库只能保存一个版本的代码。如果不同的项目依赖同一个第三方的库的不同版本,应该怎么解决?

2. go module

go module是Go1.11版本之后官方推出的版本管理工具,并且从Go1.13版本开始,go module将是Go语言默认的依赖管理工具。

2.1 GO111MODULE

要启用go module支持首先要设置环境变量GO111MODULE,通过它可以开启或关闭模块支持,它有三个可选值:offonauto,默认值是auto

  1. GO111MODULE=off禁用模块支持,编译时会从GOPATHvendor文件夹中查找包。
  2. GO111MODULE=on启用模块支持,编译时会忽略GOPATHvendor文件夹,只根据 go.mod下载依赖。
  3. GO111MODULE=auto,当项目在$GOPATH/src外且项目根目录有go.mod文件时,开启模块支持。

简单来说,设置GO111MODULE=on之后就可以使用go module了,以后就没有必要在GOPATH中创建项目了,并且还能够很好的管理项目依赖的第三方包信息。

使用 go module 管理依赖后会在项目根目录下生成两个文件go.modgo.sum

2.2 go mod命令

常用的go mod命令如下:

go mod init        初始化当前文件夹, 创建go.mod文件
go mod tidy        增加缺少的module,删除无用的module
go mod download    下载依赖的module到本地cache(默认为$GOPATH/pkg/mod目录)
go mod edit        编辑go.mod文件
go mod graph       打印模块依赖图
go mod vendor      将依赖复制到vendor下
go mod verify      校验依赖
go mod why         解释为什么需要依赖
go init
go mod init example

完成上述操作之后,就会在当前目录下生成一个go.mod文件,这个文件中的结构如下:

//module 后面是当前项目的模块名称,一般与项目名称相同,但也可以不同
module example

//预设的go版本
go 1.13

//用于将一个模块版本替换成另一个模块版本,前提是当前模块为主模块时,例如:
replace k8s.io/api => k8s.io/api v0.0.0-20190918155943-95b840bb6a1f

//也可以用于引入本地的其他模块到当前这个模块中,例如:
replace ecm-sdk-go => ./vendor/go-util

//当前项目中的依赖列表
require (
    github.com/go-git/go-git/v5 v5.1.0
)

//当前项目中禁止依赖包列表
exclude (
    github.com/go-git/go-git/v5 v5.1.0
)
go get
  • go get packageName:安装依赖
  • go get -u packageName:将依赖升级到最新的次要版本或修订版本
  • go get -u=patch packageName:将依赖升级到最新的修订版本
  • go get package@version:升级到指定的版本
go get github.com/kataras/iris@master

如果下载所有依赖可以使用go mod download命令。

go.mod文件记录了项目所有的依赖信息,其结构大致如下:

module example

go 1.12

require (
    github.com/go-sql-driver/mysql v1.4.1
    github.com/golang/protobuf v1.3.2
    github.com/grpc-ecosystem/grpc-gateway v1.11.3 // indirect
    github.com/jmoiron/sqlx v1.2.0
    github.com/micro/go-micro v1.12.0
    github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 // indirect
    go.uber.org/zap v1.10.0
    google.golang.org/grpc v1.24.0
    gopkg.in/natefinch/lumberjack.v2 v2.0.0
)

其中,

  • module用来定义包名
  • require用来定义依赖包及版本
  • indirect表示间接引用
依赖的版本

go mod支持语义化版本号,比如go get foo@v1.2.3,也可以跟git的分支或tag,比如go get foo@master,当然也可以跟git提交哈希,比如go get foo@e3702bed2。关于依赖的版本支持以下几种格式:

gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7
gopkg.in/vmihailenco/msgpack.v2 v2.9.1
gopkg.in/yaml.v2 <=v2.2.1
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e
latest
replace

在国内访问golang.org/x的各个包都需要翻墙,你可以在go.mod中使用replace替换成github上对应的库。

replace (
    golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac => github.com/golang/crypto v0.0.0-20180820150726-614d502a4dac
    golang.org/x/net v0.0.0-20180821023952-922f4815f713 => github.com/golang/net v0.0.0-20180826012351-8a410e7b638d
    golang.org/x/text v0.3.0 => github.com/golang/text v0.3.0
)
整理依赖

我们在代码中删除依赖代码后,相关的依赖库并不会在go.mod文件中自动移除。这种情况下我们可以使用go mod tidy命令更新go.mod中的依赖关系。

go mod edit

格式化

因为我们可以手动修改go.mod文件,所以有些时候需要格式化该文件。Go提供了一下命令:

go mod edit -fmt

添加依赖项

go mod edit -require=golang.org/x/text

移除依赖项

如果只是想修改go.mod文件中的内容,那么可以运行go mod edit -droprequire=package path,比如要在go.mod中移除golang.org/x/text包,可以使用如下命令:

go mod edit -droprequire=golang.org/x/text

关于go mod edit的更多用法可以通过go help mod edit查看。

2.3 在项目中使用 go module

在一个空文件夹下,初始化一个 Module

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

此时,在当前文件夹下生成了go.mod,这个文件记录当前模块的模块名以及所有依赖包的版本。

接着,我们在当前目录下新建文件 main.go,添加如下代码:

package main

import (
    "fmt"

    "rsc.io/quote"
)

func main() {
    fmt.Println(quote.Hello())  // Ahoy, world!
}

运行 go run .,将会自动触发第三方包 rsc.io/quote的下载,具体的版本信息也记录在了go.mod中:

module example

go 1.13

require rsc.io/quote v3.1.0+incompatible

go run命令会直接下载依赖并且运行项目文件,如果我们只想下载依赖可以使用go mod tidy命令。

go mod tidy

这时go编译器会去查找go mod和项目文件,增加缺少的module,删除无用的module。

我们在当前目录,添加一个子 package calc,代码目录如下:

demo/
   |--calc/
      |--calc.go
   |--main.go

calc.go 中写入

package calc

func Add(num1 int, num2 int) int {
    return num1 + num2
}

在 package main 中如何使用 package cal 中的 Add 函数呢?import 模块名/子目录名 即可,修改后的 main 函数如下:

package main

import (
    "fmt"
    "example/calc"

    "rsc.io/quote"
)

func main() {
    fmt.Println(quote.Hello())
    fmt.Println(calc.Add(10, 3))
}
$ go run .
Ahoy, world!
13

文章作者: 张亚飞
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 张亚飞 !
评论
 上一篇
第19篇:并发编程1 第19篇:并发编程1
一、并发介绍1. 进程、线程和协程 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。 一个程序至少有一个进程,一个进
2021-01-02
下一篇 
实战篇1:爬虫小案例 实战篇1:爬虫小案例
一、爬虫步骤 明确目标(确定在哪个网站搜索) 爬(爬下内容) 取(筛选想要的) 处理数据(按照你的想法去处理) package main import ( "fmt" "io/ioutil" "net/http"
2021-01-01
  目录