大家好,我是东东吖,自己是一名前端工程师,希望凭借go言语能打通前后端的任通二脉,成为一名全栈工程师!别的,需求进技术沟通群的同学,可以加我微信fangdongdong_25,需求进前端工程师沟通群的补白“前端”,需求进go后端沟通群的补白“go后端”

在工程化的Go言语开发项目中,Go言语的源码复用是建立在包(package)基础之上的。本文介绍了Go言语中怎么界说包、怎么导出包的内容及怎么引进其他包。同时也将介绍怎么在项目中运用go module办理依靠。

1.包的界说和运用

Go言语中支撑模块化的开发理念,在Go言语中运用包(package)来支撑代码模块化和代码复用。一个包是由一个或多个Go源码文件(.go结束的文件)组成,是一种高档的代码复用方案,Go言语为咱们供给了很多内置包,如fmtosio等。

例如,在之前的章节中咱们频繁运用了fmt这个内置包。

package main
import "fmt"
func main(){
  fmt.Println("Hello world!")
}

咱们可以依据自己的需求创立自界说包。一个包可以简略理解为一个寄存.go文件的文件夹。该文件夹下面的一切.go文件都要在非注释的榜首行增加如下声明,声明该文件归属的包。

package packagename

其间:

  • package:声明包的关键字
  • packagename:包名,可以不与文件夹的称号一致,不能包括-符号,最好与其完成的功用相对应。

别的需求留意一个文件夹下面直接包括的文件只能归属一个包,同一个包的文件不能在多个文件夹下。包名为main的包是应用程序的入口包,这种包编译后会得到一个可履行文件,而编译不包括main包的源代码则不会得到可履行文件。

在同一个包内部声明的标识符都坐落同一个命名空间下,在不同的包内部声明的标识符就属于不同的命名空间。想要在包的外部运用包内部的标识符就需求增加包名前缀,例如fmt.Println("Hello world!"),就是指调用fmt包中的Println函数。

假如想让一个包中的标识符(如变量、常量、类型、函数等)能被外部的包运用,那么标识符有必要是对外可见的(public)。在Go言语中是经过标识符的首字母大/小写来操控标识符的对外可见(public)/不行见(private)的。在一个包内部只要首字母大写的标识符才是对外可见的。

例如咱们界说一个名为demo的包,在其间界说了若干标识符。在别的一个包中并不是一切的标识符都能经过demo.前缀拜访到,因为只要那些首字母是大写的标识符才是对外可见的。

package demo
import "fmt"
// 包级别标识符的可见性
// num 界说一个大局整型变量
// 首字母小写,对外不行见(只能在当时包内运用)
var num = 100
// Mode 界说一个常量
// 首字母大写,对外可见(可在其它包中运用)
const Mode = 1
// person 界说一个代表人的结构体
// 首字母小写,对外不行见(只能在当时包内运用)
type person struct {
	name string
	Age  int
}
// Add 回来两个整数和的函数
// 首字母大写,对外可见(可在其它包中运用)
func Add(x, y int) int {
	return x + y
}
// sayHi 打招呼的函数
// 首字母小写,对外不行见(只能在当时包内运用)
func sayHi() {
	var myName = "七米" // 函数局部变量,只能在当时函数内运用
	fmt.Println(myName)
}

相同的规则也适用于结构体,结构体中可导出字段的字段称号有必要首字母大写。

type Student struct {
	Name  string // 可在包外拜访的办法
	class string // 仅限包内拜访的字段
}
type Student struct {
	Name  string // 可在包外拜访的办法
	class string // 仅限包内拜访的字段
}

要在当时包中运用别的一个包的内容就需求运用import关键字引进这个包,而且import句子通常放在文件的最初,package声明句子的下方。完好的引进声明句子格局如下:

import importname "path/to/package"

其间:

  • importname:引进的包名,通常都省略。默许值为引进包的包名。
  • path/to/package:引进包的途径称号,有必要运用双引号包裹起来。
  • Go言语中制止循环导入包。

一个Go源码文件中可以同时引进多个包,例如:

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

当然可以运用批量引进的方法。

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

当引进的多个包中存在相同的包名或许想自行为某个引进的包设置一个新包名时,都需求经过importname指定一个在当时文件中运用的新包名。例如,在引进fmt包时为其指定一个新包名f

import f "fmt"

这样在当时这个文件中就可以经过运用f来调用fmt包中的函数了。

f.Println("Hello world!")

假如引进一个包的时分为其设置了一个特别_作为包名,那么这个包的引进方法就称为匿名引进。一个包被匿名引进的意图首要是为了加载这个包,从而使得这个包中的资源得以初始化。 被匿名引进的包中的init函数将被履行而且仅履行一遍。

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

匿名引进的包与其他方法导入的包相同都会被编译到可履行文件中。

需求留意的是,Go言语中不答应引进包却不在代码中运用这个包的内容,假如引进了未运用的包则会触发编译过错。

2.init初始化函数的运用

在每一个Go源文件中,都可以界说恣意个如下格局的特别函数:

func init(){
  // ...
}

这种特别的函数不接收任何参数也没有任何回来值,咱们也不能在代码中主动调用它。当程序启动的时分,init函数会依照它们声明的顺序主动履行。

一个包的初始化进程是依照代码中引进的顺序来进行的,一切在该包中声明的init函数都将被串行调用而且仅调用履行一次。每一个包初始化的时分都是先履行依靠的包中声明的init函数再履行当时包中声明的init函数。保证在程序的main函数开始履行时一切的依靠包都已初始化完成。

【从0到1学习go语言】Go语言基础之包

每一个包的初始化是先从初始化包级别变量开始的。例如从下面的示例中咱们就可以看出包级别变量的初始化会先于init初始化函数。

package main
import "fmt"
var x int8 = 10
const pi = 3.14
func init() {
  fmt.Println("x:", x)
  fmt.Println("pi:", pi)
  sayHi()
}
func sayHi() {
	fmt.Println("Hello World!")
}
func main() {
	fmt.Println("你好,世界!")
}

输出成果:

x: 10
pi: 3.14
Hello World!
你好,世界!

在上面的代码中,咱们了解了Go言语中包的界说及包的初始化进程,这让咱们可以在开发时依照自己的需求界说包。同时咱们还学到了怎么在咱们的代码中引进其它的包,不过在本末节的一切示例中咱们都是引进Go内置的包。现代编程言语大多都答应开发者对外发布包/库,也支撑开发者在自己的代码中引进第三方库。这样的设计可以让广大开发者一同参加到言语的生态环境建造当中,把生态建造的愈加完善。

3.依靠办理工具go module的介绍

在Go言语的前期版别中,咱们编写Go项目代码时所依靠的一切第三方包都需求保存在GOPATH这个目录下面。这样的依靠办理方法存在一个致命的缺陷,那就是不支撑版别办理,同一个依靠包只能存在一个版别的代码。可是咱们本地的多个项目彻底可能分别依靠同一个第三方包的不同版别。

Go module 是 Go1.11 版别发布的依靠办理方案,从 Go1.14 版别开始推荐在出产环境运用,于Go1.16版别默许敞开。Go module 供给了以下指令供咱们运用:

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 解说为什么需求某个依靠

Go言语在 go module 的过渡阶段供给了GO111MODULE这个环境变量来作为是否启用 go module 功用的开关,考虑到 Go1.16 之后 go module 现已默许敞开,所以本书不再介绍该装备,对于刚触摸Go言语的读者而言彻底没有必要了解这个历史包袱。

GOPROXY

这个环境变量首要是用于设置 Go 模块署理(Go module proxy),其作用是用于使 Go 在后续拉取模块版别时可以脱离传统的 VCS 方法,直接经过镜像站点来快速拉取。

GOPROXY 的默许值是:https://proxy.golang.org,direct,因为某些原因国内无法正常拜访该地址,所以咱们通常需求装备一个可拜访的地址。现在社区运用比较多的有两个https://goproxy.cnhttps://goproxy.io,当然假如你的公司有供给GOPROXY地址那么就直接运用。设置GOPAROXY的指令如下:

go env -w GOPROXY=https://goproxy.cn,direct

GOPROXY 答应设置多个署理地址,多个地址之间需运用英文逗号 “,” 分隔。最终的 “direct” 是一个特别指示符,用于指示 Go 回源到源地址去抓取(比方 GitHub 等)。当装备有多个署理地址时,假如榜首个署理地址回来 404 或 410 过错时,Go 会主动尝试下一个署理地址,当遇见 “direct” 时触发回源,也就是回到源地址去抓取。

GOPRIVATE

设置了GOPROXY 之后,go 指令就会从装备的署理地址拉取和校验依靠包。当咱们在项目中引进了非揭露的包(公司内部git库房或 github 私有库房等),此刻便无法正常从署理拉取到这些非揭露的依靠包,这个时分就需求装备 GOPRIVATE 环境变量。GOPRIVATE用来告诉 go 指令哪些库房属于私有库房,不必经过署理服务器拉取和校验。

GOPRIVATE 的值也可以设置多个,多个地址之间运用英文逗号 “,” 分隔。咱们通常会把自己公司内部的代码库房设置到 GOPRIVATE 中,例如:

$ go env -w GOPRIVATE="git.mycompany.com"

这样在拉取以git.mycompany.com为途径前缀的依靠包时就能正常拉取了。

此外,假如公司内部自建了 GOPROXY 服务,那么咱们可以经过设置GONOPROXY=none,答应通内部署理拉取私有库房的包。

4.运用go module引进包

接下来咱们将经过一个示例来演示怎么在开发项目时运用 go module 拉取和办理项目依靠。

初始化项目咱们在本地新建一个名为holiday项目,按如下方法创立一个名为holiday的文件夹并切换到该目录下:

$ mkdir holiday
$ cd holiday

现在咱们坐落holiday文件夹下,接下来履行下面的指令初始化项目。

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

该指令会主动在项目目录下创立一个go.mod文件,其内容如下。

module holiday
go 1.16

其间:

  • module holiday:界说当时项意图导入途径
  • go 1.16:标识当时项目运用的 Go 版别

go.mod文件会记载项目运用的第三方依靠包信息,包括包名和版别,因为咱们的holiday项目现在还没有运用到第三方依靠包,所以go.mod文件暂时还没有记载任何依靠包信息,只要当时项意图一些信息。

接下来,咱们在项目目录下新建一个main.go文件,其内容如下:

// holiday/main.go
package main
import "fmt"
func main() {
	fmt.Println("现在是假日时刻...")
}

然后,咱们的holiday项目现在需求引进一个第三方包github.com/q1mi/hello来完成一些必要的功用。相似这样的场景在咱们的日常开发中是很常见的。咱们需求先将依靠包下载到本地同时在go.mod中记载依靠信息,然后才能在咱们的代码中引进并运用这个包。下载依靠包首要有两种办法。

榜首种办法是在项目目录下履行go get指令手动下载依靠的包:

holiday $ go get -u github.com/q1mi/hello
go get: added github.com/q1mi/hello v0.1.1

这样默许会下载最新的发布版别,你也可以指定想要下载指定的版别号的。

holiday $ go get -u github.com/q1mi/hello@v0.1.0
go: downloading github.com/q1mi/hello v0.1.0
go get: downgraded github.com/q1mi/hello v0.1.1 => v0.1.0

假如依靠包没有发布任何版别则会拉取最新的提交,最终go.mod中的依靠信息会变成相似下面这种由默许v0.0.0的版别号和最新一次commit的时刻和hash组成的版别格局:

require github.com/q1mi/hello v0.0.0-20210218074646-139b0bcd549d

假如想指定下载某个commit对应的代码,可以直接指定commit hash,不过没有必要写出完好的commit hash,一般前7位即可。例如:

holiday $ go get github.com/q1mi/hello@2ccfadd
go: downloading github.com/q1mi/hello v0.1.2-0.20210219092711-2ccfaddad6a3
go get: added github.com/q1mi/hello v0.1.2-0.20210219092711-2ccfaddad6a3

此刻,咱们翻开go.mod文件就可以看到下载的依靠包及版别信息都现已被记载下来了。

module holiday
go 1.16
require github.com/q1mi/hello v0.1.0 // indirect

行尾的indirect表明该依靠包为直接依靠,阐明在当时程序中的一切 import 句子中没有发现引进这个包。

别的在履行go get指令下载一个新的依靠包时一般会额定增加-u参数,强制更新现有依靠。

第二种方法是咱们直接修正go.mod文件,将依靠包和版别信息写入该文件。例如咱们修正holiday/go.mod文件内容如下:

module holiday
go 1.16
require github.com/q1mi/hello latest

表明当时项目需求运用github.com/q1mi/hello库的最新版别,然后在项目目录下履行go mod download下载依靠包。

holiday $ go mod download

假如不输出其它提示信息就阐明依靠现已下载成功,此刻go.mod文件现已变成如下内容。

module holiday
go 1.16
require github.com/q1mi/hello v0.1.1

从中咱们可以知道最新的版别号是v0.1.1。假如事前知道依靠包的具体版别号,可以直接在go.mod中指定需求的版别然后再履行go mod download下载。

这种办法相同支撑指定想要下载的commit进行下载,例如直接在go.mod文件中按如下方法指定commit hash,这儿只写出来了commit hash的前7位。

require github.com/q1mi/hello 2ccfadda

履行go mod download下载完依靠后,go.mod文件中对应的版别信息会主动更新为相似下面的格局。

module holiday
go 1.16
require github.com/q1mi/hello v0.1.2-0.20210219092711-2ccfaddad6a3

下载好要运用的依靠包之后,咱们现在就可以在holiday/main.go文件中运用这个包了。

package main
import (
	"fmt"
	"github.com/q1mi/hello"
)
func main() {
	fmt.Println("现在是假日时刻...")
	hello.SayHi() // 调用hello包的SayHi函数
}

将上述代码编译履行,就能看到履行成果了。

holiday $ go build
holiday $ ./holiday
现在是假日时刻...
你好,我是七米。很高兴知道你。

当咱们的项目功用越做越多,代码越来越多的时分,通常会选择在项目内部按功用或业务划分成多个不同包。Go言语支撑在一个项目(project)下界说多个包(package)。

例如,咱们在holiday项目内部创立一个新的package——summer,此刻新的项目目录结构如下:

holidy
├── go.mod
├── go.sum
├── main.go
└── summer
    └── summer.go

其间holiday/summer/summer.go文件内容如下:

package summer
import "fmt"
// Diving 潜水...
func Diving() {
	fmt.Println("夏天去诗巴丹潜水...")
}

此刻想要在当时项目目录下的其他包或许main.go中调用这个Diving函数需求怎么引进呢?这儿以在main.go中演示具体的调用进程为例,在项目内其他包的引进方法相似。

package main
import (
	"fmt"
	"holiday/summer" // 导入当时项目下的包
	"github.com/q1mi/hello" // 导入github上第三方包
)
func main() {
	fmt.Println("现在是假日时刻...")
	hello.SayHi()
	summer.Diving()
}

从上面的示例可以看出,项目中界说的包都会以项意图导入途径为前缀。

假如你想要导入本地的一个包,而且这个包也没有发布到到其他任何代码库房,这时分你可以在go.mod文件中运用replace句子将依靠临时替换为本地的代码包。例如在我的电脑上有别的一个名为liwenzhou.com/overtime的项目,它坐落holiday项目同级目录下:

├── holiday
│   ├── go.mod
│   ├── go.sum
│   ├── main.go
│   └── summer
│       └── summer.go
└── overtime
    ├── go.mod
    └── overtime.go

因为liwenzhou.com/overtime包只存在于我本地,并不能经过网络获取到这个代码包,这个时分应该怎么在holidy项目中引进它呢?

咱们可以在holidy/go.mod文件中正常引进liwenzhou.com/overtime包,然后像下面的示例那样运用replace句子将这个依靠替换为运用相对途径表明的本地包。

module holiday
go 1.16
require github.com/q1mi/hello v0.1.1
require liwenzhou.com/overtime v0.0.0
replace liwenzhou.com/overtime  => ../overtime

这样,咱们就可以在holiday/main.go下正常引进并运用overtime包了。

package main
import (
	"fmt"
	"holiday/summer" // 导入当时项目下的包
	"liwenzhou.com/overtime" // 经过replace导入的本地包
	"github.com/q1mi/hello" // 导入github上第三方包
)
func main() {
	fmt.Println("现在是假日时刻...")
	hello.SayHi()
	summer.Diving()
	overtime.Do()
}

咱们也经常运用replace将项目依靠中的某个包,替换为其他版别的代码包或咱们自己修正后的代码包。

go.mod文件

go.mod文件中记载了当时项目中一切依靠包的相关信息,声明依靠的格局如下:

require module/path v1.2.3

其间:

  • require:声明依靠的关键字

  • module/path:依靠包的引进途径

  • v1.2.3:依靠包的版别号。支撑以下几种格局:

    • latest:最新版别
    • v1.0.0:具体版别号
    • commit hash:指定某次commit hash

引进某些没有发布过tag版别标识的依靠包时,go.mod中记载的依靠版别信息就会出现相似v0.0.0-20210218074646-139b0bcd549d的格局,由版别号、commit时刻和commit的hash值组成。

【从0到1学习go语言】Go语言基础之包

go.sum文件

运用go module下载了依靠后,项目目录下还会生成一个go.sum文件,这个文件中具体记载了当时项目中引进的依靠包的信息及其hash 值。go.sum文件内容通常是以相似下面的格局出现。

<module> <version>/go.mod <hash>

或许

<module> <version> <hash>
<module> <version>/go.mod <hash>

不同于其他言语供给的基于中心的包办理机制,例如 npm 和 pypi等,Go并没有供给一个中心库房来办理一切依靠包,而是采用分布式的方法来办理包。为了防止依靠包被非法篡改,Go module 引进了go.sum机制来对依靠包进行校验。

依靠保存位置

Go module 会把下载到本地的依靠包会以相似下面的方法保存在$GOPATH/pkg/mod目录下,每个依靠包都会带有版别号进行区分,这样就答应在本地存在同一个包的多个不同版别。

mod
├── cache
├── cloud.google.com
├── github.com
    	└──q1mi
          ├── hello@v0.0.0-20210218074646-139b0bcd549d
          ├── hello@v0.1.1
          └── hello@v0.1.0
...

假如想铲除一切本地已缓存的依靠包数据,可以履行go clean -modcache指令。

在上面的末节中咱们学习了怎么在项目中引进他人供给的依靠包,那么当咱们想要在社区发布一个自己编写的代码包或许在公司内部编写一个供内部运用的共用组件时,咱们该怎么做呢?接下来,咱们就一同编写一个代码包并将它发布到github.com库房,让它可以被全球的Go言语开发者运用。

5.运用go module发布包

咱们首先在自己的 github 账号下新建一个项目,并把它下载到本地。我这儿就以创立和发布一个名为hello的项目为例进行演示。这个hello包将对外供给一个名为SayHi的函数,它的作用十分简略就是向调用者发去问候。

$ git clone https://github.com/q1mi/hello
$ cd hello

咱们当时坐落hello项目目录下,履行下面的指令初始化项目,创立go.mod文件。需求留意的是这儿界说项意图引进途径为github.com/q1mi/hello,读者在自行测验时需求将这部分替换为自己的库房途径。

hello $ go mod init github.com/q1mi/hello
go: creating new go.mod: module github.com/q1mi/hello

接下来咱们在该项目根目录下创立hello.go文件,增加下面的内容:

package hello
import "fmt"
func SayHi() {
	fmt.Println("你好,我是七米。很高兴知道你。")
}

然后将该项意图代码 push 到库房的远端分支,这样就对外发布了一个Go包。其他的开发者可以经过github.com/q1mi/hello这个引进途径下载并运用这个包了。

一个设计完善的包应该包括开源许可证及文档等内容,而且咱们还应该尽心维护并适时发布适当的版别。github 上发布版别号运用git tag为代码包打上标签即可。

hello $ git tag -a v0.1.0 -m "release version v0.1.0"
hello $ git push origin v0.1.0

经过上面的操作咱们就发布了一个版别号为v0.1.0的版别。

Go modules中主张运用语义化版别操控,其主张的版别号格局如下:

【从0到1学习go语言】Go语言基础之包

其间:

  • 主版别号:发布了不兼容的版别迭代时递加(breaking changes)。
  • 次版别号:发布了功用性更新时递加。
  • 修订号:发布了bug修复类更新时递加。

发布新的主版别

现在咱们的hello项目要进行与之前版别不兼容的更新,咱们计划让SayHi函数支撑向指定人宣布问候。更新后的SayHi函数内容如下:

package hello
import "fmt"
// SayHi 向指定人打招呼的函数
func SayHi(name string) {
	fmt.Printf("你好%s,我是七米。很高兴知道你。\n", name)
}

因为这次改动巨大(修正了函数之前的调用规则),对之前运用该包作为依靠的用户影响巨大。因此咱们需求发布一个主版别号递加的v2版别。在这种情况下,咱们通常会修正当时包的引进途径,像下面的示例相同为引进途径增加版别后缀。

// hello/go.mod
module github.com/q1mi/hello/v2
go 1.16

把修正后的代码提交:

hello $ git add .
hello $ git commit -m "feat: SayHi现在支撑给指定人打招呼啦"
hello $ git push

打好 tag 推送到长途库房。

hello $ git tag -a v2.0.0 -m "release version v2.0.0"
hello $ git push origin v2.0.0

这样在不影响运用旧版别的用户的前提下,咱们新的版别也发布出去了。想要运用v2版别的代码包的用户只需按修正后的引进途径下载即可。

go get github.com/q1mi/hello/v2@v2.0.0

在代码中运用的进程与之前相似,仅仅需求留意引进途径要增加 v2 版别后缀。

package main
import (
	"fmt"
	"github.com/q1mi/hello/v2" // 引进v2版别
)
func main() {
	fmt.Println("现在是假日时刻...")
	hello.SayHi("张三") // v2版别的SayHi函数需求传入字符串参数
}

抛弃已发布版别

假如某个发布的版别存在致命缺陷不再想让用户运用时,咱们可以运用retract声明抛弃的版别。例如咱们在hello/go.mod文件中按如下方法声明即可对外抛弃v0.1.2版别。

module github.com/q1mi/hello
go 1.16
retract v0.1.2

用户运用go get下载v0.1.2版别时就会收到提示,催促其升级到其他版别。