什么是包,为什么要使用它们?
到目前为止,我们看到的 Go 程序只有一个文件,其中包含一个 main函数和几个其他函数。在现实世界的场景中,这种将所有源代码写入单个文件的方法是不可扩展的。重用和维护以这种方式编写的代码变得不可能。这是包有用的地方。
包用于组织 Go 源代码以获得更好的可重用性和可读性。包是位于同一目录中的 Go 源文件的集合。包提供了代码划分,因此很容易维护 Go 项目。
例如,假设我们正在用 Go 编写一个金融应用程序,其中一些功能是单利计算、复利计算和贷款计算。组织此应用程序的一种简单方法是按功能。我们可以创建包simpleinterest
,compoundinterest
和 loan
。如果loan
包需要计算单利,只需导入simpleinterest
包即可。这样代码就被重用了。
我们将通过创建一个简单的应用程序来学习包,以确定给定本金、利率和持续时间(以年为单位)的单利。
main函数和main包
每个可执行的 Go 应用程序都必须包含 main 函数。该函数是执行的入口点。主要功能应该驻留在 main 包中。
package packagename
指定特定源文件属于 package packagename
。这应该是每个 go 源文件的第一行。
让我们开始为我们的应用程序创建主函数和 main 包。
运行以下命令以在预定的目录中创建一个名为learnpackage
的目录。
1 | mkdir E:\backend\Go_backend\learnpackage |
在我们的learnpackage
目录中创建一个名为main.go
的文件,其中包含以下内容。
1 | package main |
该行代码package main
指定该文件属于 main 包。import "packagename"
语句用于导入现有包。packagename.FunctionName()
是调用包中函数的语法。
在第3行,我们导入fmt
包以使用Println
功能。fmt
是一个标准包,作为 Go 标准库的一部分内置。主要功能是打印Simple interest calculation
learnpackage
。
通过使用移动到目录来 编译上述程序
1 | cd E:\backend\Go_backend\learnpackage\ |
并输入以下命令
1 | go build |
如果一切顺利,我们的二进制文件将被编译并准备好执行。在终端中键入命令learnpackage
,您将看到以下输出。
1 | Simple interest calculation |
Go Module
我们将以这样一种方式构建代码,即所有与简单兴趣相关的功能都在simpleinterest
包中。为此,我们需要创建一个自定义包simpleinterest
,其中包含计算单利的函数。在创建自定义包之前,我们需要先了解Go Modules,因为创建自定义包需要Go Modules。
Go Module 只不过是 Go 包的集合。现在你可能会想到这个问题。为什么我们需要 Go 模块来创建自定义包?答案是我们创建的自定义包的导入路径来源于 go 模块的名称。除此之外,我们的应用程序使用的所有其他第三方包(例如来自 github 的源代码)将与版本信息一起出现在go.mod
文件中。该go.mod
文件是在我们创建新模块时创建的。您将在下一节中更好地理解这一点。
另一个问题可能会出现在我们的脑海中。为什么我们到现在还没有创建Go 模块就成功了?答案是:到目前为止,在本教程系列中,我们从未创建过自己的自定义包,因此不需要 Go 模块。
有了足够的理论基础:)。让我们开始行动并创建我们的 go 模块和自定义包。
创建一个Go module
learnpackage
通过键入确保您在目录中cd E:\backend\Go_backend\learnpackage\
。在这个目录中运行以下命令来创建一个名为 learnpackage 的 go 模块。
1 | go mod init learnpackage |
上面的命令将创建一个名为go.mod
. 以下将是该文件的内容。
1 | module learnpackage |
该行module learnpackage
指定模块的名称是learnpackage
。正如我们之前提到的,learnpackage
将是导入此模块内创建的任何包的基本路径。该行go 1.19
指定此模块中的文件使用 go version 1.19
。
创建简单的自定义interest package
属于一个包的源文件应该放在它们自己的单独文件夹中。Go 中的约定是将此文件夹命名为与包相同的名称。
让我们在文件夹learnpackage
内创建一个名为simpleinterest
的文件夹。 mkdir simpleinterest
将为我们创建此文件夹。
simpleinterest 文件夹中的所有文件都应以package simpleinterest
开头,因为它们都属于simpleinterest
包。
在 simpleinterest 文件夹中创建一个simpleinterest.go
文件。
下面将是我们应用程序的目录结构。
1 | ├── learnpackage |
将以下代码添加到simpleinterest.go
文件中。
1 | package simpleinterest |
在上面的代码中,我们创建了一个Calculate
计算并返回单利的函数。这个函数是不言自明的。它计算并返回单利。
请注意,函数名称Calculate以大写字母开头。这是必不可少的,我们将很快解释为什么需要这样做。
导入自定义包
要使用自定义包,我们必须先导入它。导入路径是模块名加上包的子目录和包名。在我们的例子中,模块名称是learnpackage
,包 simpleinterest
在learnpackage
的直接子文件夹simpleinterest
下
1 | ├── learnpackage |
所以该行import "learnpackage/simpleinterest"
将导入simpleinterest包。
如果我们有这样的目录结构
1 | learnpackage |
那么导入语句将是import "learnpackage/finance/simpleinterest"
将以下代码添加到main.go
1 | package main |
上面的代码导入simpleinterest
包并使用Calculate
函数找到简单的兴趣。标准库中的包不需要模块名称前缀,因此“fmt”无需模块前缀即可工作。当应用程序运行时,输出将是
1 | Simple interest calculation |
更多关于 go install
现在我们了解了包的工作原理,是时候多谈谈go install
. Go 工具,例如go install
就在当前目录的上下文中工作。让我们了解这意味着什么。到目前为止,我们一直在从目录E:\backend\Go_backend\learnpackage\
运行go install
。如果我们尝试从任何其他目录运行它,它将失败。
尝试进入cd E:\backend\Go_backend\
然后运行go install learnpackage
. 它将失败并出现以下错误。
1 | go: go.mod file not found in current directory or any parent directory; see 'go help modules' |
让我们了解此错误背后的原因。go install
将可选的包名作为参数(在我们的例子中包名是learnpackage
),如果包存在于运行它的当前目录或父目录或父目录等中,它会尝试编译 main 函数.
在Go_backend
目录中,那里没有go.mod
文件,因此go install
抱怨它找不到包learnpackage
。
当我们移动到E:\backend\Go_backend\learnpackage\
时,那里有一个go.mod
文件,我们的模块名称learnpackage
在该go.mod
文件中。
所以go install learnpackage
将从E:\backend\Go_backend\learnpackage\
目录内部工作。
但是到目前为止我们只是在使用go install
并且没有指定包名。如果未指定包名,go install
则默认为当前工作目录中的模块名。这就是为什么当go install
在没有任何包名的情况下能够运行的原因。因此,以下 3 个命令在运行时是等效的。
1 | go install |
我还提到它go install
能够递归地在父目录中搜索 go.mod
文件。让我们检查一下这是否有效。
1 | cd ~/Documents/learnpackage/simpleinterest/ |
上面的命令会将我们带到simpleinterest
目录。从那个目录运行
1 | go install learnpackage |
Go install 将成功地在定义了learnpackage
模块的父目录中找到一个go.mod
file,因此它可以工作:)。
Exported Names
Calculate
我们将 Simple Interest 包中的函数大写。这在 Go 中有特殊的含义。任何以大写字母开头的变量或函数都是 go 中的导出名称(Exported Names)。只能从其他包访问导出的函数和变量。在我们的例子中,我们想Calculate
从 main 包中访问函数。因此这是大写的。
如果在simpleinterest.go
中将函数名由 Calculate
改为 calculate
,如果我们尝试在 main.go 中调用该函数simpleinterest.calculate(p, r, t)
,编译器会报错
1 | # learnpackage |
因此,如果您想访问包外的函数,则应将其大写。
init function
Go 中的每个包都可以包含一个init
函数。该init
函数不能有任何返回类型,也不能有任何参数。init 函数不能在我们的源代码中显式调用。包初始化时会自动调用。init 函数具有以下语法:
1 | func init() { |
该init
函数可用于执行初始化任务,也可用于在执行开始前验证程序的正确性。
一个包的初始化顺序如下:
- 包级变量首先被初始化
- 接下来调用 init 函数。一个包可以有多个 init 函数(在单个文件中或分布在多个文件中),它们按照它们呈现给编译器的顺序被调用。
如果一个包导入其他包,则首先初始化导入的包。
一个包即使是从多个包导入的,也只会被初始化一次。
让我们对我们的应用程序进行一些修改以了解init
功能。
首先,让我们将init
函数添加到simpleinterest.go
文件中。
1 | package simpleinterest |
我们添加了一个简单的 init 函数,它只打印Simple interest package initialised
现在让我们修改 main 包。我们知道在计算单利时,本金、利率和期限应该大于零。我们将使用main.go
文件中的 init 函数和包级变量来定义此检查。
修改main.go
如下,
1 | package main |
以下是对main.go
更改的描述
- p,r 和 t 变量从主函数级别移动到包级别。
- 添加了一个初始化函数。如果使用
log.Fatal
函数的本金、利率或持续时间小于零,init
函数会打印日志并终止程序执行。
的初始化顺序如下,
- 导入的包首先被初始化。因此
simpleinterest
包首先被初始化并且它的init
方法被调用。 - 包级变量p、r和t接下来被初始化。
init
函数在 main 中调用。- 最后调用
main
函数。
如果您运行该程序,您将获得以下输出。
1 | Simple interest package initialized |
正如预期的那样,simpleinterest
首先调用包的 init
函数,然后是包级变量的初始化p
,r
然后t
。接下来调用 main 包的 init
函数。它检查和是否小于零,如果条件为真则终止p
。现在你可以假设它会检查是否小于 0,如果是,程序将被终止。我们为和写了一个类似的条件。在这种情况下,所有这些条件都为假,程序继续执行。最后调用main函数。
让我们稍微修改一下这个程序来学习init
函数的使用。
在main.go
中改变下行代码
1 | var p, r, t = 5000.0, 10.0, 1.0 |
为
1 | var p, r, t = -5000.0, 10.0, 1.0 |
我们已经初始化p
为负数。
现在,如果您运行应用程序,您将看到
1 | Simple interest package initialized |
p为负数。因此,当 init 函数运行时,程序在打印后终止Principal is less than zero
。
使用空白标识符
在 Go 中导入一个包并且不在代码中的任何地方使用它是非法的。如果您这样做,编译器会抱怨。这样做的原因是为了避免未使用的包膨胀(bloating),这将显着增加编译时间。将代码替换为main.go
以下内容,
1 | package main |
上面的程序会报错
1 | # learnpackage |
但是,当应用程序正在积极开发时导入包并在以后(如果不是现在)在代码中的某个地方使用它们是很常见的。在这些情况下,_
空白标识符可以帮助我们。
上述程序中的错误可以通过以下代码静音,
1 | package main |
该var _ = simpleinterest.Calculate
行使错误静音。我们应该跟踪这些类型的错误消音器(error silencers),并在应用程序开发结束时删除它们,包括导入的包,在导入的包没有使用的时候。因此,建议在 import 语句之后的包级别中编写错误消音器。
有时我们需要导入一个包只是为了确保初始化发生,即使我们不需要使用包中的任何函数或变量。例如,我们可能需要确保调用simpleinterest
包的init
函数,即使我们计划不在代码中的任何地方使用该包。在这种情况下也可以使用 _
空白标识符,如下所示。
1 | package main |
运行上述程序将输出Simple interest package initialized
. 我们已经成功地初始化了这个simpleinterest
包,即使它没有在代码中的任何地方使用。