[奔跑的 Go] 教程五、Go 语言包管理(Package)必知必会

Golang

Go编程语言中包管理和部署的完整概述

如果您熟悉 Java NodeJS 等语言,那么您可能对 packages (包)非常熟悉。 包只是一个包含一些代码文件的目录,它可以从同一个入口引入并可以使用其中的代码或变量( features )。 接下来让我解释一下,这代表了什么意思。

想象一下,在处理工作中任何项目时,您需要经常使用的功能超过一千多种。 其中一些功能具有共同的行为。 例如,toUpperCasetoLowerCase函数转换string case ,一般情况下你把它们写在一个文件中(*可能是* case.go )。 当然你还有其他函数可以对string数据类型执行一些其他操作,因此您会将它们写在单独的文件中。

你可能会有很多个用于处理 string 类型数据的文件,那么你需要创建一个名为 string 的目录,并把所有与 string 相关的文件都放进去。最终,你把所有这些目录都放在一个总目录里,就组成了一个包。整个包的结构就像下面这样:

package-name
├── string
|  ├── case.go
|  ├── trim.go
|  └── misc.go
└── number
   ├── arithmetics.go
   └── primes.go

我会为大家系统地讲解如何从一个包中引用函数和变量,以及如何把所有的东西放在一起组成一个包。不过现在,我们就先把包简单理解成是一个包含了多个 .go 文件的目录。

每一段 Go 程序都 必须 属于一个包。正如 Getting started with Go 中所说的,一个标准的可执行的 Go 程序必须有 package main 的声明。如果一段程序是属于 main 包的,那么当执行 go install 的时候就会将其生成二进制文件,当执行这个文件时,就会调用 main 函数。如果一段程序是属于 main 以外的其他包,那么当执行 go install 的时候,就会创建一个 包管理 文件。别着急,接下来我会对这些做详细的讲解。

下面就来创建一个可执行的包。我们已经知道了,要想生成可执行的二进制文件,必须把代码写在 main 包里,而且其中必须包含一个 main 函数作为程序的入口函数。

包名就是包含在 src 目录里的子目录的名字。在上面的例子中,appsrc 的子目录,所有 app 就是一个包。所以当执行 go install app 命令时,系统就会在 GOPATH 中寻找 src 目录里的 app 子目录。当编译这个包的时候,就会在 bin 目录下创建名为 app 的二进制文件。由于 bin 目录是在 PATH 中的,所以可以通过终端执行其中的文件。

位于所有代码第一行的 包的声明 并非一定要与包名同名。因此,你可能会发现很多包的包名(文件夹名)与其内部声明的名字是不同的。当引用包的时候,需要使用包的声明来作为引用变量,关于这点,我会在下面做详细的解释。

执行 go install <package> 命令后,系统会尝试在指定的包目录里寻找带有 **main** 包声明的文件。找到之后,Go 就知道这是可执行的程序,需要被编译为二进制文件。一个包里可以有很多文件,但是只能其中一个文件里有 main 函数,标志着这个文件是程序的入口文件。

如果一个包中没有带有 main 包声明的文件,那么,Go 就会在 pkg 目录中创建一个 包管理 (.a) 文件。

由于 app 不是一个可执行的包,因此系统会在 pkg 目录中创建 app.a 文件。这不是一个二进制文件,因此是不可执行的。

包的命名规范

关于包的命名,Go 团队建议以简单,扁平为原则。例如,strutilsstring utility 函数的名字,http 是 HTTP 请求的名字。
包的名字应避免使用下划线,中划线或掺杂大写字母。

创建包

正如前文所提到的,包分为两种,一种是 可执行的包,另一种是 工具包。可执行的包可以看作是主应用,因为我们需要运行它。工具包自身是不可执行的,但是它会给可执行的包增加一些功能,从而起到扩展主应用的作用。

既然我们已经知道了一个包就是一个目录,那么就在 src 目录中创建一个 greet 目录吧,然后在里面创建一些文件。greet 目录里,所有文件的顶部都要写上 package greet 的声明,表明这是一个工具包。

输出项

工具包的意义在于给引用它的包提供一些 variables。与 JavaScriptexport 语法类似,名字是以 大写字母 开头的,就是可以输出的variables,反之,就是包私有的,其他包无法引用。

在后面的内容中,我都会用 variables 这个词来表示输出项,但其实输出项可以有很多种,比如 *constant*, *map*, *function*, *struct*, array, slice 等等。

输出 day.go 文件中的 greeting 变量。

在上面的程序中,Morning 变量是可以被输出的,但是 morning 不行,因为它是以小写字母开头的。

引用包

现在,我们需要一个使用 greet 包的 可执行包。在 src 目录里创建 app 目录,然后在其中创建一个带有 main 包声明和 main 函数的 entry.go 文件。这里敲黑板,在 Go 的包中,没有类似于 Node 里的 index.js入口文件命名机制。对于一个可执行的包,带有 main 函数的文件就是其入口文件。

使用 import 语法引用包。Go 首先会在 **GOROOT**/src 目录中搜索指定的包,如果找不到,再去 **GOPATH**/src 目录中找。因为 fmt 包来自于 Go 的标准库,因此可以在 GOROOT/src 路径中找到它。而 greet 包不属于 Go 的标准库,Go 在 GOROOT 路径中找不到它,所以就会去 GOPATH/src 路径中找。

上面的代码抛出了编译错误,因为 morning 变量是 greet 包私有的。正如你所看到的,可以使用 . (点)符号来获取包的输出项。当一个包被引用的时候,Go 会使用 包的声明 把这个包创建为全局变量。在上面的例子中,greet 就是全局变量,因为在 greet 包的文件中,使用了 package greet 的包声明。

还可以以组合的形式同时引入 fmtgreet 包。现在程序就可以正常编译了,因为 Morning 变量是可以被外部引用的。

包的嵌套

可以在一个包中嵌套另一个包。因为在 Go 中,一个包就是一个目录,所谓嵌套,就相当于是在一个包中创建一个子目录。我们需要做的,就是指明其路径关系。

包的编译

正如我们在前面所讲的,go run 命令会编译并执行一个程序。go install 命令会编译包并生成二进制文件,或包管理文件。这样就避免了对引用的包的重复编译。 go install 命令会对包 ( .a 文件 ) 进行预编译。

总体来讲,当你引用第三方包的时候,Go 就会编译这个包,并创建包管理文件。如果你是在本地写的包,当你保存或修改的时候,你的 IDE 可能就已经创建包管理文件了。VSCode 会在保存的时候自动编译,如果你安装了 Go 插件。

安装包

当我们运行 Go 程序时,Go 编译器会按照一定的执行顺序来操作包,包中的文件以及包里的声明的变量。

包的 scope

scope 是指代码块中可以访问已定义变量的区域。包的 scope 是指在一个包中 ( 包括包里的所有文件 ) 可以访问已定义变量的区域。这个区域是包中所有文件的顶层块。

再来看下 go run 命令。它还可以同时运行 app 包里的所有文件。Go 能够辨别出 entry.go 是应用的入口文件,因为它里面有 main 函数。可以像下面这样使用这个命令( 文件名不分先后顺序 )。

go run src/app/version.go src/app/entry.go

*go install* 或 *go build* 命令的参数是包含了所有文件的包名,所以不需要像上面那样指定具体的文件名。

回到 main 函数,尽管 version.go 文件中声明的 version 变量并没有采用开头字母大写,但是我们依然可以在包内的任何地方使用它,因为它是在包的 scope 中声明的。如果 version 变量是在一个函数中声明的,那么它就不属于包的 scope 范畴,上面的程序就会出错。

不可以在一个包中声明重复声明全局变量。因此,一旦 version 变量声明了,在包的 scope 内,就不可以再次声明。但是你可以在 scope 以外的任何地方声明。

变量初始化

如果变量 a 依赖于变量 b,那么就要先初始化变量 b,否则程序就无法编译。Go 在函数内部会遵循这个原则。

但是在包的 scope 中定义的变量,它们在初始化周期中就已经被声明了。来看下面这个小例子。

在上面的例子中,首先是变量 c 被赋值了。在下一个初始化周期时,变量 b 被赋值,因为它依赖于变量 c,而 c 已经被赋值了。在最后一次初始化周期中,变量 a 被赋予变量 b 的值。Go 可以处理复杂的初始化周期。

在上面的例子中,首先变量 c 被赋值,然后变量 b 的值依赖于变量 c,最后变量 a 的值依赖于变量 b。注意要避免像下面这样递归式的初始化循环。

关于包 scope 的另一个例子就是,可以在另一个文件中的函数 f 中引用入口文件的变量 c

Init 函数

main 函数一样,当初始化包的时候,init 函数也会被执行。它没有参数,也没有返回值。init 函数是由 Go 来声明的,你无法引用这个函数( 也不能用类似 *init()* 的方式调用它)。你可以在一个文件或一个包中定义多个 init 函数。在同一个文件中,init 函数是按照它们被定义的先后顺序被执行的。

你可以在包中的任何位置定义 init 函数。这些 init 函数以词法文件名的顺序(字母顺序)被调用。

当所有的 init 函数被执行以后,main 函数才会被执行。因此,`init` 函数 的主要工作就是,初始化无法在全局范围内初始化的全局变量 。例如,初始化数组。

因为在包 scope 中是不可以使用 for 循环的,我们可以在 init 函数中用 for 循环初始化有 10 个元素的整型数组。

包的别名

当引用包的时候,Go 会根据包的声明来创建包的变量。如果同时引用多个重名的包,就会导致冲突。

// parent.go
package greet

var Message = "Hey there. I am parent."

// child.go
package greet

var Message = "Hey there. I am child."

因此,我们需要使用 包的别名。在关键字 import 和包名之间,声明一个变量名,用以表示这个包。

在上面的例子中,用变量 child 来表示 greet/greet 包。或许你已经发现了,我们使用了下划线作为 greet 包的别名。在 Go 中,下划线是一个特殊的符号,表示 null 容器。如果我们引用了 greet 包,但是暂时没有用到它,Go 在编译的时候就会抱怨这个问题。为了避免这个问题,我们就可以把暂时用不到的引用放在 _ 中,这样编译器就会忽略它。

给一个包加上 下划线 别名看似没有意义,但其实在某些情况下还是很有用的,比如在包初始化时用不到的变量。

// parent.go
package greet

import "fmt"

var Message = "Hey there. I am parent."

func init() {
  fmt.Println("greet/parent.go ==> init()")
}

// child.go
package greet

import "fmt"

var Message = "Hey there. I am child."

func init() {
  fmt.Println("greet/greet/child.go ==> init()")
}

特别强调一点,被引用的包只能被初始化一次。因此,如果你在一个包中,要引入很多包,每个被应用的包都只能在 main 包的生命周期中被初始化一次。

程序执行顺序

我们已经对包有了比价全面的了解,现在就结合前面所学到的知识,系统看下程序是如何被初始化的。

go run *.go
├── 执行 Main 包
├── 初始化所有引用的包
|  ├── 初始化所有引用的包 (recursive definition)
|  ├── 初始化全局变量
|  └── 以词法文件名的顺序调用 init 函数
└── 初始化 Main 包
   ├── 初始化全局变量
   └── 以词法文件名的顺序调用 init 函数

下面是个小例子。

// version/get-version.go
package version

import "fmt"

func init() {
 fmt.Println("version/get-version.go ==> init()")
}

func getVersion() string {
 fmt.Println("version/get-version.go ==> getVersion()")
 return "1.0.0"
}

/***************************/

// version/entry.go
package version

import "fmt"

func init() {
 fmt.Println("version/entry.go ==> init()")
}

var Version = getLocalVersion()

func getLocalVersion() string {
 fmt.Println("version/entry.go ==> getLocalVersion()")
 return getVersion()
}

/***************************/

// app/fetch-version.go
package main

import (
 "fmt"
 "version"
)

func init() {
 fmt.Println("app/fetch-version.go ==> init()")
}

func fetchVersion() string {
 fmt.Println("app/fetch-version.go ==> fetchVersion()")
 return version.Version
}

/***************************/

// app/entry.go
package main

import "fmt"

func init() {
 fmt.Println("app/entry.go ==> init()")
}

var myVersion = fetchVersion()

func main() {
 fmt.Println("app/fetch-version.go ==> fetchVersion()")
 fmt.Println("version ===> ", myVersion)
}

安装第三方包

安装第三方包非常简单,只要把代码克隆到本地 src/<package> 目录中即可。不幸的是,Go不支持包版本也没有提供包管理工具,但是 这里 有一个值得期待的提案。

也就是说,Go没有统一完善的包管理工具,我们需要自己提供包的主机名和路径。

$ go get -u github.com/jinzhu/gorm

上面这个命令的意思是,从 http://github.com/jinzhu/gorm URL 中引用文件,并保存到 src/github.com/jinzhu/gorm 目录中。根据嵌套包的原则,可以像下面这样引用 gorm 包。

package main

import "github.com/jinzhu/gorm"

// use ==> gorm.SomeExportedMember

所以,如果你写了一个包,把它发布到 GitHub 上,其他人就可以使用了。如果是可执行的包,就可以作为命令行工具,如果不是,就可以引入到项目中,作为一个工具模块来使用。总之,不管是哪种,都只需执行如下命令就可以使用了。

$ go get github.com/your-username/repo-name
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://medium.com/rungo/everything-you-...

译文地址:https://learnku.com/go/t/27649

本帖已被设为精华帖!
本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!