创建和导入 Go 包

Go 语言Beginner
立即练习

介绍

在前一节中,你完成了一个基本的 Go 程序,其中包含以下代码行:

package main
import "fmt"

我们如何理解这两行代码?如何有效地使用 package 和 import 语句?

在本实验中,你将学习如何在 Go 中创建和导入包。这将使你能够将代码组织成可重用的模块,从而使你的 Go 项目更具可维护性和可扩展性。

知识点:

  • 包的定义和声明
  • 理解导出(public)和未导出(private)的标识符
  • 导入包的不同形式:单行导入、分组导入、点导入、别名导入和匿名导入

声明和定义包

在本步骤中,你将声明一个可重用的包,并区分导出标识符与仅限包内访问的标识符。

在 Go 中,包(package)类似于 Python 中的模块或 C 中的库。它是用于组织和重用代码的源代码文件的集合。每个 Go 文件必须在文件开头声明一个包。

注意:Go 可执行程序由包含 main 函数的 main 包构建。可重用的包不需要这个入口点。一个模块可以包含多个命令目录,每个目录都有自己的 main 包。

关键点:

导出(公共)标识符:以大写字母开头的标识符(变量、函数、类型等)可以从其他包中访问。可以将这些视为包的公共接口。

未导出(私有)标识符:以小写字母开头的标识符只能在同一个包内访问。这些被视为包的内部实现细节。

包的内聚性:同一文件夹中的所有文件必须属于同一个包。这确保了相关代码保持在一起。

包命名规范:包名应为小写、简短且具有描述性,避免使用下划线或大写字母。

让我们创建一个自定义包:

创建一个名为 propagandist 的文件夹,并在其中创建一个文件 propagandist.go:

mkdir ~/project/propagandist
touch ~/project/propagandist/propagandist.go

在 propagandist.go 中编写以下代码:

package propagandist

var Shout = "I Love LabEx" // Public variable
var secret = "I love the dress" // Private variable

func Hit() string {
    return "Don't hit me, please!"
}
  • Shout 是公共的,因为它以大写字母开头。这意味着你可以从导入 propagandist 的其他包中访问它。
  • secret 是私有的,因为它以小写字母开头。它只能在 propagandist 包内使用。
  • Hit 是一个公共函数,可以从其他包中访问。

为包初始化一个 Go 模块:

cd ~/project/propagandist
go mod init propagandist

此命令在 propagandist 目录中初始化一个新的 Go 模块,这有助于管理包的依赖项。

单条导入声明

在本步骤中,你将使用独立的 import 声明来导入并使用本地包。

为了使用 propagandist 包,让我们创建一个新的 Go 程序。这一步将演示如何在你的 Go 代码中导入和使用单个包。

在项目文件夹中创建一个名为 pacExercise.go 的新 Go 文件:

touch ~/project/pacExercise.go

为程序初始化一个 Go 模块:

cd ~/project
go mod init pacExercise

更新 go.mod 文件以包含本地包依赖项,在终端中运行以下命令:

echo "replace propagandist => ./propagandist" >> go.mod

重要提示: 此命令向你的 go.mod 文件添加了一个 replace 指令。这非常关键,因为它告诉 Go 应该从本地目录 ./propagandist 中获取 propagandist 包,而不是尝试从远程仓库下载它。你应在终端中执行此命令,这会将 replace propagandist => ./propagandist 这一行追加到你的 go.mod 文件中。不要手动直接写入这一行。

在 pacExercise.go 中编写以下代码以导入并使用 propagandist 包:

package main

import "fmt"
import "propagandist"

func main() {
    fmt.Println(propagandist.Shout) // Access the public variable
}
  • 此代码导入了用于打印输出的 fmt 包和 propagandist 包。
  • 然后通过 propagandist.Shout 访问 propagandist 包中的公共变量 Shout。

运行程序:

go mod tidy
go run pacExercise.go

go mod tidy 命令确保你的 go.mod 文件更新了所有新的依赖项。go run pacExercise.go 命令编译并执行程序。

预期输出:

I Love LabEx

分组导入

在本步骤中,你将把包导入放在同一个分组中,而不改变程序行为。

当导入多个包时,你可以使用分组导入来提高代码的可读性和组织性。这是一种风格选择,不会改变代码的功能。

修改 pacExercise.go 以使用分组导入:

```go
package main

import (
    "fmt"
    "propagandist"
)

func main() {
    fmt.Println(propagandist.Shout)
}
```

在上面的代码片段中,fmt 和 propagandist 包被导入到一个用括号括起来的 import 块中。这使得读取和管理多个包的导入更加容易。与上一步分别使用独立的 import 声明不同,这里将两个包路径放在同一条声明中。程序的行为保持不变。

运行程序以确认它仍然正常工作:

```bash
go run pacExercise.go
```

程序应该能够无错误地执行,并输出与之前相同的结果。

点导入

在本步骤中,你将了解点导入,以及它在命名空间方面的取舍。

使用点导入(dot import)时,你可以在调用包中的函数或变量时省略包名前缀。通常不推荐使用这种方式,因为它可能导致命名空间冲突并降低代码的可读性。然而,了解它的用法仍然是有意义的。

修改 pacExercise.go 以对 fmt 使用点导入:

```go
package main

import . "fmt"
import "propagandist"

func main() {
    Println(propagandist.Shout) // No `fmt.` prefix needed
}
```
  • 这里,import . "fmt" 表示你可以直接使用 fmt 包中的函数和变量,而无需 fmt. 前缀。
  • 例如,你可以使用 Println 而不是 fmt.Println。

运行程序:

```bash
go run pacExercise.go
```

预期输出:

```bash
I Love LabEx
```

别名导入

在本步骤中,你将为导入的包设置一个本地别名。

你可以为导入的包设置别名,以提高代码的清晰度或避免两个包名称相似时发生冲突。这对于提高代码的可读性和管理命名空间冲突非常有帮助。

修改 pacExercise.go 以将 fmt 别名为 io:

```go
package main

import io "fmt"
import "propagandist"

func main() {
    io.Println(propagandist.Shout) // Use the alias `io` instead of `fmt`
}
```
  • import io "fmt" 为 fmt 包创建了一个别名 io。
  • 现在,你可以使用 io.Println 而不是 fmt.Println。

运行程序:

```bash
go run pacExercise.go
```

匿名导入

在本步骤中,你将仅为包的初始化副作用而导入它。

匿名导入用于导入一个包以利用其副作用,例如运行其 init() 函数,而无需直接引用其导出的标识符。这对于注册驱动程序或执行其他初始化任务的包非常有用。

修改 pacExercise.go 以包含对 time 的匿名导入:

```go
package main

import (
    "fmt"
    "propagandist"
    _ "time" // Anonymous import
)

func main() {
    fmt.Println(propagandist.Shout)
}
```
  • import _ "time" 是匿名导入。下划线 _ 用作空白标识符,告诉编译器你导入该包是为了其副作用,而不会在代码中直接引用其中的任何内容。
  • 当程序运行时,time 包的 init() 函数将会执行。虽然 time 包在这里没有明显的副作用,但许多包使用这种方式来注册数据库驱动程序或配置设置。

运行程序:

```bash
go run pacExercise.go
```

预期输出:

```bash
I Love LabEx
```

理解 init() 函数

在本步骤中,你将跟踪 main() 运行之前包级变量和 init() 函数的初始化过程。

init() 是 Go 中的特殊函数,在包初始化过程中起着重要作用。导入包时,Go 会自动执行它,执行时间早于 main 函数中的任何代码。本节将详细介绍 init() 函数,以及它在 Go 初始化过程中的工作方式。

关于 init() 函数的要点:

定义与用途:

  • init() 函数没有参数,也没有返回值:func init() {}
  • 它用于包的初始化任务,例如设置初始状态、注册驱动程序或验证前置条件。

执行顺序:

  • Go 保证每个包只初始化一次,即使该包被多次导入。

  • 初始化遵循明确的顺序:

  • 先初始化包级变量

  • 然后执行 init() 函数

  • 最后运行 main() 函数(仅限 main 包)

多个 init() 函数:

  • 单个 Go 文件可以包含多个 init() 函数
  • 同一包中的多个文件可以各自拥有 init() 函数
  • 所有这些 init() 函数都按照源代码中的声明顺序执行,文件顺序采用传递给编译器的顺序。Go 工具按文件名的字典序传递包中的文件;应避免依赖文件顺序来表达依赖关系。

依赖链:

  • 包导入其他包时,Go 会确保先执行依赖包中的 init() 函数
  • 这形成自底向上的初始化流程:最深层的依赖最先初始化

下面创建一个实际示例,演示 init() 函数的工作方式:

首先修改 propagandist 包,为其添加一个 init() 函数。更新 propagandist.go:

package propagandist

import "fmt"

var Shout = "I Love LabEx" // Public variable
var secret = "I love the dress" // Private variable
var initialized bool

func init() {
    fmt.Println("Initializing propagandist package...")
    initialized = true
}

func Hit() string {
    return "Don't hit me, please!"
}

func IsInitialized() bool {
    return initialized
}

现在,在 propagandist 包中创建另一个文件,演示多个 init() 函数:

touch ~/project/propagandist/second.go

将以下内容添加到该文件:

package propagandist

import "fmt"

func init() {
    fmt.Println("Second init function in propagandist package...")
}

创建一个新的 helper 包,演示初始化顺序:

mkdir -p ~/project/helper
touch ~/project/helper/helper.go

将以下内容添加到该文件:

package helper

import "fmt"

var Message = "Helper package is ready"

func init() {
    fmt.Println("Initializing helper package...")
}

func GetMessage() string {
    return Message
}

为 helper 包添加模块文件:

cd ~/project/helper
go mod init helper

更新 pacExercise.go,使用这两个包并演示初始化顺序:

package main

import (
    "fmt"
    "helper"
    "propagandist"
)

func init() {
    fmt.Println("Initializing main package...")
}

func main() {
    fmt.Println("Main function is running")
    fmt.Println(propagandist.Shout)
    fmt.Println(helper.Message)
    fmt.Printf("Propagandist initialized: %v\n", propagandist.IsInitialized())
}

更新主项目的 go.mod 文件,加入本地 helper 包:

cd ~/project
echo "replace helper => ./helper" >> go.mod
go mod tidy

运行程序,观察初始化顺序:

go run pacExercise.go

使用所提供的文件名和 Go 工具时,预期输出如下:

Initializing helper package...
Initializing propagandist package...
Second init function in propagandist package...
Initializing main package...
Main function is running
I Love LabEx
Helper package is ready
Propagandist initialized: true

这段输出展示了 Go 初始化过程的几个关键方面:

依赖包先于导入它们的包进行初始化

在同一个包中,所有 init() 函数都按声明顺序运行,源文件顺序采用传递给编译器的顺序

所有包初始化完成后,才会运行 main() 函数

包级变量先于任何 init() 函数进行初始化

设计 Go 包时,初始化顺序是一个重要的考虑因素,尤其是在管理依赖关系或执行必须按特定顺序进行的准备操作时。

总结

在本实验中,你学习了以下内容:

  1. 如何在 Go 中创建和定义自定义包,封装可重用的代码。
  2. 公共(导出)和私有(未导出)标识符之间的区别,以及它们如何影响可访问性。
  3. 导入包的各种方式及其适用场景:
    • 单条导入声明:每条声明导入一个包。
    • 分组导入:在单个块中导入多个包以提高组织性。
    • 点导入:导入包并直接使用其标识符,无需包名前缀。(需谨慎使用)
    • 别名导入:为导入的包重命名以提高可读性或避免命名冲突。
    • 匿名导入:仅为了包的副作用(例如初始化)而导入包。
  4. 包中 init() 函数的作用,以及匿名导入如何触发其执行。
  5. Go 初始化过程的详细工作方式,包括:
    • 包级变量如何先于 init() 函数进行初始化
    • 相互依赖的包之间有保证的 init() 函数执行顺序
    • 一个包内的多个 init() 函数如何工作
    • 从依赖包到 main 函数的完整初始化流程

通过完成本实验,你现在已经掌握了如何有效地使用包来构建和管理 Go 项目。你可以创建可重用的模块,控制标识符的访问权限,更好地组织代码,并理解初始化过程,从而开发出更具可维护性和可扩展性的 Go 应用程序。