本教程适用于 Go 1.13 或更高版本,并使用 Go 模块。如果你正在使用 Go 的旧版本,或者正在寻找本教程的GOPATH 版本,请访问 https://golangbot.com/packages/

欢迎来到 Golang 系列教程的第 7 课。

什么是 Packages,为什么使用它们?

到目前为止,我们看到的 Go 程序只有一个文件,其中有一个 main 函数和几个其他函数。在实际场景中,这种将所有源代码写入单个文件的方法是不好扩展的。以这种方式编写的代码不可能复用和还有维护。这就是 packages 的用处所在。

包用于组织更好的可复用性和可读性的 Go 源代码。包是位于在同一目录中的 Go 源文件的集合。包提供了代码划分,因此很容易维护 Go 项目。

例如,假设我们正在用 Go 编写一个金融应用程序,其中的一些功能是单利计算、复利计算和贷款计算。按功能组织此应用程序的一种简单方法。我们可以创建 simpleinterest,compoundinterest 和 loan。如果 loan 包需要计算单利,只需导入 simpleinterest 包即可。这样就可以重用代码。

我们将通过创建一个简单的应用程序来确定给定本金、利率和年利率的单利来学习包。

main 函数和 main package

每个可执行的 Go 应用程序都必须包含 main 函数。这个函数是执行的入口点。主函数应该位于在 main package 中。

package packagename 指定一个特定的源文件属于 package packagename。这应该是每个go源文件的第一行。

让我们从为我们的应用程序创建 main 函数和 main package 开始。

运行下面的命令,在当前用户的 Documents 目录中创建一个名为 learnpackage 的目录。

  1. mkdir ~/Documents/learnpackage/

在我们的 learnpackage 目录中创建一个名为 main.go 的文件,其中包含以下内容。

  1. package main
  2. import "fmt"
  3. func main() {
  4. fmt.Println("Simple interest calculation")
  5. }

代码行 package main 所在的行指定此文件属于 main package。import "packagename" 语句用于导入现有的软件包。 packagename.FunctionName() 是在包中调用函数的语法。

在第三行我们导入 fmt 包来使用 Println 函数。fmt 是一个标准包,可以作为 Go 标准库的一部分内置。然后是打印 Simple interest calculation 的 main 函数。

使用进入到 learnpackage 目录来编译上面的程序

  1. cd ~/Documents/learnpackage/

并输入以下命令

  1. go install

如果一切顺利,我们的二进制文件将被编译并准备执行。在终端中输入命令 learnpackage,你将看到以下输出。

  1. Simple interest calculation

如果你不明白如何 go install 或者你报错了

  1. go install: no install location for directory /home/naveen/Documents/learnpackage outside GOPATH
  2. For more details see: 'go help gopath'

请访问 https://golangbot.com/hello-world-gomod/ 了解更多信息。

Go Module

我们将以这样一种方式构造代码,即与单利相关的所有功能都在 simpleinterest 包中。为此,我们需要创建一个自定义包 simpleinterest,它将包含计算单利的函数。在创建自定义包之前,我们需要首先了解 Go Modules ,因为创建自定义包需要 Go Modules

Go Module 只不过是 Go packages 的集合。现在你可能会想到这个问题。为什么我们需要Go模块来创建自定义包?答案是,我们创建的自定义包的导入路径是从 go module 的名称派生出来的。除此之外,我们的应用程序所使用的所有其他第三方包(例如来自github的源代码)都将在运行中出现。go.mod 文件以及版本。go.mod 文件是在创建新模块时创建的。在下一节中,你将更好地理解这一点。

另一个问题可能突然出现在我们的脑海里。为什么我们到现在还没有创建一个 Go module ?答案是,在本系列教程中,我们从未创建过自己的自定义包,因此不需要 Go module

讲了足够的理论了:)。让我们开始行动,创建我们的 go module 和自定义包。

创建一个 Go module

输入 cd ~/Documents/learnpackage/, 确保你位于 learnpackage 目录下。在这个目录中运行以下命令来创建一个名为 learnpackage 的 go module。

  1. go mod init learnpackage

上面的命令将创建一个名为 go.mod 的文件。下面是该文件的内容。

  1. module learnpackage
  2. go 1.13

module learnpackage 这一行指定模块的名称为 learnpackage。如前所述,learnpackage 将是导入在这个模块中创建的任何包的基本路径。go 1.13 这行指定此模块中的文件使用 go 1.13 版本。

创建一个简单的自定义包

属于包的源文件应该放在它们自己的单独文件夹中。按照 Go 的惯例,将此文件夹命名为与包相同的名称。

让我们在 learnpackage 文件夹中创建一个名为 simpleinterest 的文件夹。mkdir simpleinterest 将为我们创建此文件夹。

simpleinterest 文件夹中的所有文件都应该以行package simpleinterest 开始,因为它们都属于 simpleinterest 包。

simpleinterest 文件夹创建一个 simpleinterest.go 文件。

下面是我们的应用程序的目录结构。

  1. ├── learnpackage
  2. ├── go.mod
  3. ├── main.go
  4. └── simpleinterest
  5. └── simpleinterest.go

将以下代码添加到 simpleinterest.go 文件中。

  1. package simpleinterest
  2. //Calculate calculates and returns the simple interest for a principal p, rate of interest r for time duration t years
  3. func Calculate(p float64, r float64, t float64) float64 {
  4. interest := p * (r / 100) * t
  5. return interest
  6. }

在上面的代码中,我们创建了一个计算并返回单利的 Calculate 函数。这个函数是很容易很懂的。它计算并返回单利。

注意,Calculate 函数名以大写字母开头。这是必要的,稍后我们将解释为什么需要这样做。

导入自定义包

要使用自定义包,我们必须首先导入它。导入路径是包的子目录所附加的模块名和包名。在我们的例子中,模块名是 learnpackage ,而包 simpleinterest 位于直接位于 learnpackage 下的 simpleinterest 文件夹中

  1. ├── learnpackage
  2. └── simpleinterest

因此,导入 import “learnpackage/simpleinterest” 这一行将导入 simpleinterest 包。

如果我们有一个像这样的目录结构

  1. learnpackage
  2. └── finance
  3. └── simpleinterest

那么import语句就是 import “learnpackage/finance/simpleinterest”

将以下代码添加到 main.go

  1. package main
  2. import (
  3. "fmt"
  4. "learnpackage/simpleinterest"
  5. )
  6. func main() {
  7. fmt.Println("Simple interest calculation")
  8. p := 5000.0
  9. r := 10.0
  10. t := 1.0
  11. si := simpleinterest.Calculate(p, r, t)
  12. fmt.Println("Simple interest is", si)
  13. }

上面的代码导入 simpleinterest 包并使用 Calculate 函数查找单利。标准库中的包不需要模块名前缀,因此“fmt”可以在没有模块前缀的情况下工作。当应用程序运行时,输出将是

  1. Simple interest calculation
  2. Simple interest is 500

理解 go install

既然我们已经了解了包是如何工作的,那么现在就可以进一步讨论 go install 了。在当前目录的上下文中 go install 等 Go 工具。让我们来理解这是什么意思。到目前为止,我们已经运行 go install 从目录 ~/Documents/learnpackage/。如果我们试图从任何其他目录运行它,它将失败。

尝试 cd ~/Documents/,然后运行 go install learnpackage。它将失败,出现以下错误。

  1. can't load package: package learnpackage: cannot find package "learnpackage" in any of:
  2. /usr/local/Cellar/go/1.13.7/libexec/src/learnpackage (from $GOROOT)
  3. /Users/nramanathan/go/src/learnpackage (from $GOPATH)

让我们来了解一下这个错误背后的原因。go install 以一个可选的包名作为参数(在我们的例子中,包名是 learnpackage),如果包存在于当前目录中,或者在父目录中,或者在父目录中,那么它会尝试编译主函数。

我们在 Documents 目录中,那里没有 go.mod 文件,因此 go install 失败,它找不到包 learnpackage。

当我们移动到 ~/Documents/learnpackage/ 时,有 go.mod 文件,我们在 go.mod 文件里的模块名是 learnpackage。

所以 go install learnpackage 在 ~/Documents/learnpackage/ 目录工作。

但是到目前为止,我们只是使用了 go install,并没有指定包名。如果没有指定包名,go install 将默认为当前工作目录中的模块名。这就是为什么运行 go install 时 ~/Documents/learnpackage/ 没有任何软件包名称的原因。因此,当从 ~/Documents/learnpackage/ 运行以下三个命令时,它们是等效的

  1. go install
  2. go install .
  3. go install learnpackage

我还提到,go install 能够递归父目录中搜索 go.mod 文件。我们来看看这是否可行。

  1. cd ~/Documents/learnpackage/simpleinterest/

上面的命令将把我们带到 simpleinterest 目录。从该目录运行

  1. go install learnpackage

go install 将成功在父目录 learnpackage 中找到一个 go.mod 文件,该文件已定义了 learnpackage 模块,因此它可以工作:)。

导出名称

我们把单利包中的 Calculate 函数大写。这在 Go 中有特殊的意义。任何以大写字母开头的变量或函数都将在 go 中导出名称。只有导出的函数和变量可以从其他包中访问。在本例中,我们希望从主包中访问 Calculate 函数。因此这是大写的。

如果在 simpleinterest.go 中将函数名从 Calculate 更改为 calculate,如果我们试着用 simpleinterest.calculate(p, r, t) 调用这个函数,编译器会报错

  1. # learnpackage
  2. ./main.go:13:8: cannot refer to unexported name simpleinterest.calculate
  3. ./main.go:13:8: undefined: simpleinterest.calculate

因此,如果要访问包之外的函数,则应将其大写。

init 函数

Go 中的每个包都可以包含一个 init 函数。init 函数不能有任何返回类型,也不能有任何参数。在源代码中不能显式地调用 init 函数。当包初始化时,它将被自动调用。init 函数具有以下语法

  1. func init() {
  2. }

init 函数可用于执行初始化任务,也可用于在执行开始之前验证程序的正确性。

包的初始化顺序如下

  1. 包里的全局变量首先被初始化

  2. init 函数将在下一步调用。一个包可以有多个 init 函数(在单个文件中,也可以分布在多个文件中),它们是按照向编译器显示的顺序调用的。

如果一个包导入其他包,则首先初始化导入的包。

一个包只初始化一次,即使它是从多个包中导入的。

让我们对应用程序进行一些修改,以理解 init 函数。

首先,我们将 init 函数添加到 simpleinterest.go 文件中。

  1. package simpleinterest
  2. import "fmt"
  3. /*
  4. * init function added
  5. */
  6. func init() {
  7. fmt.Println("Simple interest package initialized")
  8. }
  9. //Calculate calculates and returns the simple interest for principal p, rate of interest r for time duration t years
  10. func Calculate(p float64, r float64, t float64) float64 {
  11. interest := p * (r / 100) * t
  12. return interest
  13. }

我们添加了一个简单的 init 函数,该函数打印 Simple interest package initialised

现在,让我们修改 main package。 我们知道计算单利时本金,利率和持续时间应大于零。 我们将使用 main.go 文件中的 init 函数和包级变量定义此检查。

将 main.go 修改为以下内容,

  1. package main
  2. import (
  3. "fmt"
  4. "learnpackage/simpleinterest" //importing custom package
  5. "log"
  6. )
  7. var p, r, t = 5000.0, 10.0, 1.0
  8. /*
  9. * init function to check if p, r and t are greater than zero
  10. */
  11. func init() {
  12. println("Main package initialized")
  13. if p < 0 {
  14. log.Fatal("Principal is less than zero")
  15. }
  16. if r < 0 {
  17. log.Fatal("Rate of interest is less than zero")
  18. }
  19. if t < 0 {
  20. log.Fatal("Duration is less than zero")
  21. }
  22. }
  23. func main() {
  24. fmt.Println("Simple interest calculation")
  25. si := simpleinterest.Calculate(p, r, t)
  26. fmt.Println("Simple interest is", si)
  27. }

下面是对 main.go 所做的更改

  1. p、rt 变量从 main 函数级移动到 package 级。

  2. 已经添加了 init 函数。如果本金,利率或持续时间小于零,则使用 log.Fatal 函数,init 函数将打印日志并终止程序执行。

初始化的顺序如下:

  1. 首先初始化导入的包。因此,首先初始化 simpleinterest 包,然后调用它的 init 方法。

  2. Package 级别的变量 p、rt 接下来被初始化。

  3. init 函数在 main 函数中被调用。

  4. 最后调用 main 函数。

如果运行该程序,将得到以下输出。

  1. Simple interest package initialized
  2. Main package initialized
  3. Simple interest calculation
  4. Simple interest is 500

正如预期的那样,首先调用 simpleinterest 包的 init 函数,然后调用包级别变量 p、r和 t 的初始化。接下来调用 main package 的 init 函数。它检查 p、r 和 t 是否小于零,如果条件为真,则终止。我们将在单独的教程中详细学习 if 语句。现在,你可以假设 if p < 0 将检查 p 是否小于 0,如果是,则程序将终止。我们已经为 r 和 t 编写了一个类似的条件。在本例中,所有这些条件都为 false,程序继续执行。最后,调用主函数。

让我们稍微修改一下这个程序,以了解 init 函数的用法。

修改代码

  1. var p, r, t = 5000.0, 10.0, 1.0

main.go

  1. var p, r, t = -5000.0, 10.0, 1.0

我们已经将 p 初始化为负数。

现在,如果运行该应用程序,将看到

  1. Simple interest package initialized
  2. Main package initialized
  3. 2020/02/15 21:25:12 Principal is less than zero

p 是负数。因此,当 init 函数运行时,程序在输出 Principal is less than zero。

空白标识符的使用

在 Go 中导入一个包是非法的,并且不能在代码的任何地方使用它。如果你这样做,编译器会报错。这样做的原因是为了避免未使用包的增加,这将显著增加编译时间。替换 main.go 的代码。遵循以下原则

  1. package main
  2. import (
  3. "learnpackage/simpleinterest"
  4. )
  5. func main() {
  6. }

上面的程序会报错

  1. # learnpackage
  2. ./main.go:4:2: imported and not used: "learnpackage/simpleinterest"

但是,在应用程序处于活动开发阶段时导入包,并在以后(如果不是现在)在代码中的某个地方使用它们,这是很常见的。_ 标识符可以在这些情况下保存我们。

上述程序中的错误可以通过以下代码消除,

  1. package main
  2. import (
  3. "learnpackage/simpleinterest"
  4. )
  5. var _ = simpleinterest.Calculate
  6. func main() {
  7. }

var _ = simpleinterest.Calculate 这一行让程序正常运行。如果不使用包,我们应该跟踪这些错误消音器,并在应用程序开发结束时删除它们,包括导入的包。 因此,建议在 import 语句之后在程序包级别编写错误消音器。

有时我们需要导入一个包来确保初始化发生,即使我们不需要使用包中的任何函数或变量。例如,我们可能需要确保调用了 simpleinterest 包的 init 函数,即使我们不打算在代码中的任何地方使用该包。在这种情况下也可以使用 _ 标识符,如下所示。

  1. package main
  2. import (
  3. _ "learnpackage/simpleinterest"
  4. )
  5. func main() {
  6. }

运行上述程序将输出 Simple interest package initialized。我们已经成功地初始化了 simpleinterest 包,尽管它在代码中的任何地方都没有使用。

原文链接

https://golangbot.com/go-packages/