Go语言包导入指南:从基础到最佳实践

介绍

在不同项目间借用和共享代码是任何广泛使用的编程语言以及整个开源社区的基础。代码复用使得程序员可以将大部分时间专注于编写符合自己需求的代码,而他们的一些新代码往往也能对其他人有用。他们可以决定将这些可重复使用的部分组织成一个单元,并在团队内或更广泛的编程社区中共享。

在Go语言中,可重复使用代码的基本单元被称为package(包)。即使是最简单的Go程序也是一个独立的包,并且可能会使用至少一个其他的包。在本教程中,你将编写两个小程序:一个使用标准库中的包来生成随机数,另一个使用热门的第三方包来生成UUID。然后,你可以选择编写一个更长的程序来比较两个相似的标准库包,导入并使用这两个包,尽管它们具有相同的基本名称。最后,你将使用goimports工具来查看如何格式化你的导入部分。

注意:Go语言还有一个更高级别的可重用代码单元:模块。模块是一组被版本标记的包的集合。你将在后续文章中探索如何使用Go模块。

先决条件

在开始本教程之前,您只需安装Go。针对您的操作系统,请阅读相应的教程。

步骤1 – 使用标准库包

与大多数其他语言一样,Go语言内置了一个可重用代码库,你可以用于常见任务。你不需要自己编写代码来格式化和打印字符串,或者发送HTTP请求等。Go标准库提供了用于这两个任务和许多其他任务的包。

在《Go编程入门:如何编写你的第一个程序》中,使用了标准库中的fmtstrings包来编写程序。现在我们来写另一个程序,利用math/rand包生成一些随机数。

在nano或者你喜欢的文本编辑器中打开一个名为random.go的新文件。

  1. nano random.go

我们来创建一个程序,打印出从零到九的五个随机整数。将下面的代码粘贴到你的编辑器中:

package main

import "math/rand"

func main() {
	for i := 0; i < 5; i++ {
		println(rand.Intn(10))
	}
}

该程序导入math/rand包,并通过引用其基本名称rand来使用它。这个名称出现在每个Go源文件的包声明的顶部。

for循环的每一次迭代调用rand.Intn(10)来生成一个介于零和九之间的随机整数(10不包括在内),然后将该整数打印到控制台。

注意:对println()的调用没有引用包名。这是一个内置函数,不需要被导入。通常在这里你会使用来自fmt包的fmt.Println()函数,但是这个程序使用println()来介绍内置函数。

保存程序。如果你正在使用nano编辑器,按下CTRL+X键,然后按Y键并按下ENTER键确认你的修改。然后运行程序。

go run random.go

你应该看到从零到九的五个整数。

输出

看起来随机数生成器正在工作,但如果你一次又一次地运行程序,它会打印出相同的数字,而不是你所期望的新的随机数。这是因为我们没有调用 rand.Seed() 函数,用一个唯一的值对随机数生成器进行初始化。如果不这样做,该包的行为就好像调用了 rand.Seed(1) 一样,因此它每次都会生成相同的“随机”数字。

所以每次运行程序时,您需要使用一个唯一的值来为数字生成器设置种子。程序员通常使用当前的纳秒级时间戳。要获取它,您需要使用 time 包。再次打开您的编辑器并在 random.go 中粘贴以下内容:

package main

import (
	"math/rand"
	"time"
)

func main() {
	now := time.Now()
	rand.Seed(now.UnixNano())
println("Numbers seeded using current date/time:", now.UnixNano())
	for i := 0; i < 5; i++ {
		println(rand.Intn(10))
	}
}

当导入多个软件包时,可以使用括号创建一个导入块。通过使用块,您可以避免在每一行上重复使用 import 关键字,使代码更清晰。

首先,您正在通过 time.Now() 函数获取当前系统时间,该函数返回一个 Time 结构。然后,您将时间传递给 rand.Seed() 函数。该函数接受一个 64 位整数(int64),因此您需要在 now 结构上使用 Time.UnixNano() 方法以纳秒的形式传入时间。最后,您正在打印用于初始化随机数生成器的时间。

现在保存并再次运行程序。

  1. go run random.go

 

你应该看到类似这样的输出。

Output

Numbers seeded using current date/time: 1674489465616954000 2 6 3 1 0

如果你运行程序多次,你应该每次都能看到不同的整数,以及用于生成随机数的唯一整数种子。

让我们再编辑一次程序,将种子时间以更用户友好的格式打印出来。将包含第一个 println() 调用的那一行编辑成这样:

	println("Numbers seeded using current date/time:", now.Format(time.StampNano))

现在你正在调用 Time.Format() 方法,并传入 time 包中定义的许多格式之一。 time.StampNano 常量是一个字符串,将其传递给 Time.Format() 可以让你打印出月份、日期和时间,精确到纳秒。保存并再次运行程序。

go run random.go
Output

Numbers seeded using current date/time: Jan 23 10:01:50.721413000 7 6 3 7 3

那比看到一个表示自 1970 年 1 月 1 日以来经过的纳秒数的巨大整数要好看。

如果你的程序不需要随机整数,而是需要 UUIDs(许多程序员用它作为跨部署数据片段的全局唯一标识符),那该怎么办呢?Go 标准库没有用于生成 UUIDs 的包,但是社区有。现在让我们看看如何下载和使用第三方包。

步骤 2 — 使用第三方包

这是文章《在Go语言中导入包》的第3部分(共8部分)。

生成UUID(通用唯一标识符)最受欢迎的软件包之一是 github.com/google/uuid。第三方软件包的命名通常采用完全限定名称,其中包含托管代码的网站(例如 github.com)、开发它的用户或组织(例如 google)以及基本名称(例如 uuid)。在导入软件包、阅读其在 pkg.go.dev 上的文档以及其他地方时,您将使用软件包的完全限定名称。然而,在代码语句中引用它时,您只需要使用基本名称。

在下载一个软件包之前,您需要初始化一个模块,这是 Go 用于管理程序依赖及其版本的方式。要初始化一个模块,请使用 go mod init 命令,并传入您自己的软件包的完全限定名称。如果您想在 GitHub 上以您的用户名 “sammy” 托管您的模块,您可以像这样初始化您的模块:

  1. go mod init github.com/sammy/random

 

这会创建一个名为 go.mod 的文件。让我们看一下这个文件:

  1. cat go.mod

 

输出
module github.com/sammy/random go 1.19

这个文件必须出现在任何将作为 Go 模块分发的 Go 代码库的根目录中。它至少必须定义模块名称和所需的 Go 版本。您自己的 Go 版本可能不同于上面显示的版本。

在本教程中,您将不会分发您的模块,但是这一步骤对于下载和使用第三方软件包是必要的。

现在使用 go get 命令下载第三方 UUID 模块。

  1. go get github.com/google/uuid

 

这将下载最新版本。

输出
go: downloading github.com/google/uuid v1.3.0
go: added github.com/google/uuid v1.3.0

该软件包被放置在您的本地目录 $GOPATH/pkg/mod/ 中。如果您的 shell 中没有显式设置 $GOPATH,则其默认值为 $HOME/go。举个例子,如果您的本地用户名为 sammy 并且您正在运行 macOS,该软件包将被下载到 /Users/sammy/go/pkg/mod 中。您可以运行 go env GOMODCACHE 命令来查看 Go 将下载模块放置在何处。

让我们来查看您的新依赖模块的 go.mod 文件:

  1. cat /Users/sammy/go/pkg/mod/github.com/google/uuid@v1.3.0/go.mod

 

输出
module github.com/google/uuid

看起来这个模块没有依赖第三方库,它只使用了 Go 标准库的包。

请注意,模块的版本包含在其目录名称中。这样可以让您在同一个程序内或不同程序之间开发和测试相同软件包的多个版本。

现在再次查看您自己的 go.mod 文件:

  1. cat go.mod

 

输出
module github.com/sammy/random
go 1.19
require github.com/google/uuid v1.3.0 // indirect

go get 命令在您当前的目录中注意到了 go.mod 文件,并更新它以反映您程序的新依赖性,包括其版本。现在您可以在您的包中使用这个包。在您的文本编辑器中打开一个名为 uuid.go 的新文件,并粘贴以下内容:

package main

import "github.com/google/uuid"

func main() {
	for i := 0; i < 5; i++ {
		println(uuid.New().String())
	}
}

此程序与 random.go 相似,但是它使用 github.com/google/uuid 来生成并打印出五个 UUID,而不是使用 math/rand 来生成并打印出五个整数。

保存新的文件并运行它。

  1. go run uuid.go

 

您的输出应该类似于这个。

输出

这是文章《在Go语言中导入包》的第4部分(共8部分)。

github.com/google/uuid包使用标准库包crypto/rand生成这些UUID,它与您在random.go中使用的math/rand包类似但有所不同。如果您需要同时使用这两个包,怎么办呢?它们都有一个基本名称rand,那么您如何在代码中引用每个不同的包呢?让我们接下来来看看这个问题。

步骤3 — 导入具有相同名称的包

math/rand的文档说明它实际上生成的是伪随机数,并且“不适用于对安全性要求较高的工作”。对于这种工作,我们应该使用crypto/rand。但是,如果对于你的程序来说整数的随机性质量并不重要呢?也许你真的只需要任意的数字。

你可以编写一个程序来比较这两个rand包的性能,但在这样一个程序中你不能通过rand名称引用两个包。为了解决这个问题,Go允许在导入包时选择一个替代的本地名称。

以下是如何导入具有相同基本名称的两个包:

import (
    "math/rand"
    crand "crypto/rand"
)

只要不与其他包名重复,您可以选择任何喜欢的别名并将其放在完全限定的包名左边。在这种情况下,别名是crand。请注意,别名周围没有引号。在包含此导入块的源文件的其余部分中,您可以使用您选择的名称crand来访问crypto/rand包。

你还可以将包导入到自己的命名空间中(使用点.作为别名)或者空标识符(使用下划线_作为别名)。想了解更多,请阅读Go文档。

为了说明您可能想要如何使用具有相同名称的软件包,让我们创建并运行一个更长的程序,使用这两个软件包生成随机整数,并测量每种情况下所花费的时间。这部分是可选的;如果您不感兴趣,请跳到第4步。

比较 math/rand 和 crypto/rand (可选)

获取命令行参数

首先,在您的工作目录中打开一个名为compare.go的第三个新文件,并将以下程序粘贴进去。

package main

import "os"
import "strconv"

func main() {
	// 用户必须传入要生成的整数数量
	if len(os.Args) < 2 {
		println("用法:\n")
		println("  go run compare.go <整数数量>")
		return
	}
	n, err := strconv.Atoi(os.Args[1])
	if err != nil { // 可能用户没有传入整数
		panic(err) 
	}

	println("用户要求生成 " + strconv.Itoa(n) + " 个整数。")
}

这段代码可以让你通过rand包生成用户给定数量的伪随机整数。它使用osstrconv标准库包将单个命令行参数转换为整数。如果没有传入参数,它会打印使用说明并退出。

运行程序,使用一个参数为10,确保其正常工作。

  1. go run compare.go 10

 

[输出]
用户要求生成 10 个整数。

目前为止,一切都还不错。现在让我们使用math/rand包生成随机整数,就像之前一样,但这次你还要计算完成所需的时间。

第一阶段——测量 math/rand 性能

这是文章《在Go语言中导入包》的第5部分(共8部分)。

内容片段:删除最后一个println()语句,并用以下内容替换:

	// 阶段 1 — 使用 math/rand
	// 初始化字节切片
	b1 := make([]byte, n)
	// 获取当前时间
	start := time.Now()
	// 初始化随机数生成器
	rand.Seed(start.UnixNano())
	// 生成伪随机数
	for i := 0; i < n; i++ {
		b1[i] = byte(rand.Intn(256)) // 魔法发生的地方!
	}
	// 计算耗时
	elapsed := time.Now().Sub(start)
	// 如果 n 非常大,只打印少量数字
	for i := 0; i < 5; i++ {
		println(b1[i])
	}
	fmt.Printf("使用 math/rand 生成 %v 个伪随机数耗时: %v\n", n, elapsed)

首先,您正在使用内置函数make()创建一个空的字节切片([]byte),用于存储生成的整数(以字节形式)。其长度是用户请求的整数数量。

然后,您获取当前时间,并使用它初始化随机数生成器,这与步骤1中random.go文件中的做法相同。

在此之后,您将生成n个介于0到255之间的伪随机整数,并将每个整数转换为字节并放入字节切片中。为什么选择0到255之间的整数?因为您即将编写的crypto/rand代码生成的是字节,而不是任意大小的整数,我们应该对包进行等效比较。一个字节,也就是8位,可以用0到255的无符号整数来表示。(实际上,Go语言中的byte类型是uint8类型的别名。)

最后,只有当用户请求生成大量整数时,您才打印前五个字节。打印几个整数只是为了确保数值生成器正常运行。

在运行程序之前,不要忘记将您使用的新包添加到您的导入代码块中。

import (
	"fmt"
	"math/rand"
	"os"
	"strconv"
	"time"
)

添加了高亮部分后,请运行程序。

  1. go run compare.go 10

 

输出

189 203 209 238 235 使用 math/rand 生成 10 个伪随机数所需时间:33.417µs

生成十个介于 0 到 255 之间的整数并将其存储在字节切片中,耗时 33.417 微秒。接下来,我们将其与 crypto/rand 的性能进行比较。

第二阶段 — 测量 crypto/rand 性能

在添加使用 crypto/rand 的代码之前,请按照之前演示的方式将该包添加到导入模块中:

import (
	"fmt"
	"math/rand"
	crand "crypto/rand"
	"os"
	"strconv"
	"time"
)

然后,将以下代码追加到你的 main() 函数的末尾:

	// 第二阶段 — 使用 crypto/rand
	// 初始化字节切片
	b2 := make([]byte, n)
	// 获取当前时间(注意:无需为随机数生成器设置种子)
	start = time.Now()
	// 生成伪随机数
	_, err = crand.Read(b2) // 核心操作!
	// 计算耗时
	elapsed = time.Now().Sub(start)
	// 如果出错则退出
	if err != nil {
		panic(err)
	}
	// 如果 n 非常大,则只打印少量数字
	for i := 0; i < 5; i++ {
		println(b2[i])
	}
	fmt.Printf("使用 crypto/rand 生成 %v 个伪随机数所需时间:%v\n", n, elapsed)

此代码尽可能与第一阶段的代码相似。您正在生成大小为 n 的字节切片,获取当前时间,生成 n 个字节,计算经过的时间,最后打印五个整数和经过的时间。在使用 crypto/rand 包时,无需显式地设置随机数生成器的种子。

注意:crypto/rand 还包括一个 Int() 函数,但我们在此示例中使用 Read() 是因为它在包文档中是唯一的示例代码中使用的函数。您可以随意自行探索 crypto/rand 包。

这是文章《在Go语言中导入包》的第7部分(共8部分)。

内容片段: 你的整个 `compare.go` 程序应该如下所示:

package main

import (
	"fmt"
	"math/rand"
	crand "crypto/rand"
	"os"
	"strconv"
	"time"
)

func main() {
	// 用户必须传入要生成的整数数量
	if len(os.Args) < 2 {
		println("用法:\n")
		println("  go run compare.go <整数数量>")
		return
	}
	n, err := strconv.Atoi(os.Args[1])
	if err != nil { // 可能用户没有传入整数
		panic(err)
	}

	// 阶段1 — 使用 math/rand 包
	// 初始化字节切片
	b1 := make([]byte, n)
	// 获取当前时间
	start := time.Now()
	// 初始化随机数生成器
	rand.Seed(start.UnixNano())
	// 生成伪随机数
	for i := 0; i < n; i++ {
		b1[i] = byte(rand.Intn(256)) // 核心操作!
	}
	// 计算耗时
	elapsed := time.Now().Sub(start)
	// 如果 n 非常大,只打印前几个数字
	for i := 0; i < 5; i++ {
		println(b1[i])
	}
	fmt.Printf("使用 math/rand 生成 %v 个伪随机数耗时: %v\n", n, elapsed)

	// 阶段2 — 使用 crypto/rand 包
	// 初始化字节切片
	b2 := make([]byte, n)
	// 获取当前时间(注意:无需为随机数生成器设置种子)
	start = time.Now()
	// 生成伪随机数
	_, err = crand.Read(b2) // 核心操作!
	// 计算耗时
	elapsed = time.Now().Sub(start)
	// 如果出错则退出
	if err != nil {
		panic(err)
	}
	// 如果 n 非常大,只打印前几个数字
	for i := 0; i < 5; i++ {
		println(b2[i])
	}
	fmt.Printf("使用 crypto/rand 生成 %v 个伪随机数耗时: %v\n", n, elapsed)
}

使用参数为10运行程序,使用每个包生成十个8位整数。

  1. go run compare.go 10

 

输出

145 65 231 211 250

使用 math/rand 生成 10 个伪随机数所需时间:32.5µs

101 188 250 45 208

使用 crypto/rand 生成 10 个伪随机数所需时间:42.667µs

在此示例的执行中,math/rand 包比 crypto/rand 包稍微快一些。尝试多次运行 compare.go,并将参数设置为 10。然后尝试生成一千个整数,或者一百万个。哪个包始终更快呢?

这个示例程序旨在展示如何在同一个程序中使用两个名称相同、功能相似的包。它并不意味着推荐其中一个包而不是另一个。如果你想要扩展 compare.go,你可以使用 math/stats 包来比较每个包生成的字节的随机性。无论你正在编写什么样的程序,评估不同的包并选择最适合你需求的才是你的责任。

最后,让我们来看一下如何使用 goimports 工具格式化导入声明。

第四步 — 使用 Goimports

有时候在编程过程中,当您流畅地进行编程时,可能会忘记导入您正在使用的包,或者删除您不需要的包。goimports 命令行工具不仅可以格式化您的导入声明和其他代码,使其成为 gofmt 的更具功能性的替代品,还可以为您代码引用的包补充任何缺失的导入,并删除未使用的包的导入。

工具默认情况下并没有随 Go 一起安装,所以立即使用 go install 来安装它。

  1. go install golang.org/x/tools/cmd/goimports@latest

 

这将把 goimports 二进制文件放置在你的 $GOPATH/bin 目录中。如果你按照教程在 macOS 上安装 Go 并设置本地编程环境(或者根据你的操作系统选择相应的教程)进行操作,那么该目录应已在你的 $PATH 中。尝试运行该工具。

  1. goimports –help

 

如果您无法看到工具的使用说明,则表示 $GOPATH/bin 不在您的 $PATH 中。请阅读适用于您操作系统的 Go 环境设置指南来设置它。

一旦 goimports 在您的 $PATH 中,从 random.go 文件中移除整个导入块。然后,使用 -d 选项运行 goimports,以显示它要添加的差异。

  1. goimports -d random.go

 

输出

diff -u random.go.orig random.go — random.go.orig 2023-01-25 16:29:38 +++ random.go 2023-01-25 16:29:38 @@ -1,5 +1,10 @@ package main +import (
+ “math/rand”
+ “time”
+)
+ func main() { now := time.Now() rand.Seed(now.UnixNano())

很令人印象深刻,但是 goimports 也可以识别并添加第三方包,只需通过 go get 在本地安装。从 uuid.go 中删除 import 并运行 goimports

  1. goimports -d uuid.go

 

输出

diff -u uuid.go.orig uuid.go — uuid.go.orig 2023-01-25 16:32:56 +++ uuid.go 2023-01-25 16:32:56 @@ -1,8 +1,9 @@ package main +import “github.com/google/uuid” + func main() { for i := 0; i < 5; i++ { println(uuid.New().String()) } }

现在编辑 uuid.go 文件,并进行以下操作:

    1. 添加一个导入 math/rand 的语句,目前代码中没有用到它。

 

  1. 将内建的 println() 函数改为 fmt.Println(),但不要添加 import "fmt"
uuid.go

package main

import "math/rand"

func main() {
	for i := 0; i < 5; i++ {
		fmt.Println(uuid.New().String())
	}
}

保存文件并再次运行 goimports

  1. goimports -d uuid.go

 

输出

diff -u uuid.go.orig uuid.go — uuid.go.orig 2023-01-25 16:36:28 +3 uuid.go 2023-01-25 16:36:28 @@ -1,10 +1,13 @@ package main -import “math/rand” +import (
+ “fmt”
+ “github.com/google/uuid”
+)
+ func main() { for i := 0; i < 5; i++ { fmt.Println(uuid.New().String()) } }

这个工具不仅可以添加缺失的导入,还可以删除不必要的导入。同时需要注意的是,它将导入语句放置在括号中的一个块内,而不是在每一行上使用 import 关键字。

要将更改写入 uuid.go(或任何文件)而不是将其输出到终端,请使用 goimports-w 选项。

  1. goimports -w uuid.go

 

你应该设置你的文本编辑器或者集成开发环境,当你保存一个 .go 文件时调用 goimports,这样你的源代码文件就会始终保持格式良好。如前所述,goimports 取代了 gofmt,所以如果你的文本编辑器已经使用 gofmt,就把它配置成使用 goimports

另外,goimports 还会对你的导入进行强制排序。试图手动维护导入的顺序可能会很繁琐且容易出错,所以让 goimports 为你处理这个问题吧。

如果 Go 团队更改了 Go 源文件的官方格式,他们将会更新 goimports 以反映这些更改,因此你应该定期更新 goimports 以确保你的代码始终符合最新的标准。

结论

在本教程中,您使用常用的软件包来帮助生成随机数和 UUID,创建并运行了两个不超过十行的程序。Go 生态系统中的软件包丰富多样且写得很好,所以使用 Go 编写新程序应该是一种乐趣,您会发现自己能够轻松地编写满足特定需求的实用程序,比您想象的要轻松。

请查看系列教程的下一个教程,学习如何在 Go 中编写包。然后,如果你愿意,可以提前了解如何使用 Go 模块,以便将包组合在一起并作为一个整体分发给他人。

bannerAds