在Go语言中导入包
介绍
能够在不同项目间借用和共享代码是任何广泛使用的编程语言及整个开源社区的基础。借用代码使得程序员可以将大部分时间专注于编写符合自己需求的代码,而他们的一些新代码往往也能对其他人有用。他们可以决定将这些可重复使用的部分组织成一个单元,并在团队内或更广泛的编程社区中共享。
在Go语言中,可重复使用的代码的基本单元被称为package(包)。即使是最简单的Go程序也是一个独立的包,并且可能会使用至少一个其他的包。在本教程中,你将编写两个微小的程序:一个使用标准库中的包来生成随机数,另一个使用热门的第三方包来生成UUID。然后,你可以选择编写一个更长的程序来比较两个相似的标准库包,导入并使用这两个包,尽管它们具有相同的基本名称。最后,你将使用goimports工具来查看如何格式化你的导入部分。
Note
先决条件
在开始这个教程之前,您只需安装Go。针对您的操作系统,请阅读相应的教程。
- How To Install Go On Ubuntu 20.04
- How To Install Go and Set Up a Local Programming Environment on macOS
- How To Install Go and Set Up a Local Programming Environment on Windows 10
步骤1 – 使用标准库包
和大部分其他语言一样,Go语言内置了一个可重用代码库,你可以用于常见任务。你不需要自己编写代码来格式化和打印字符串,或者发送HTTP请求等。Go标准库提供了用于这两个任务和许多其他任务的包。
在《Go编程入门:如何编写你的第一个程序》中,使用了标准库中的fmt和strings包来编写程序。现在我们来写另一个程序,利用math/rand包生成一些随机数。
在nano或者你喜欢的文本编辑器中打开一个名为random.go的新文件。
- 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不包括在内),然后将该整数打印到控制台。
Note
保存程序。如果你正在使用 nano 编辑器,按下 CTRL+X 键,然后按 Y 键并按下 ENTER 键确认你的修改。然后运行程序。
go run random.go
你应该看到从零到九的五个整数。
1 7 7 9 1
看起来随机数生成器正在工作,但如果你一次又一次地运行程序,它会打印出相同的数字,而不是你所期望的新的随机数。这是因为我们没有调用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))
}
}
当导入多个软件包时,可以使用括号创建一个导入块。通过使用块,您可以避免在每一行上重复使用导入关键字,使代码更清晰。
首先,您正在通过time.Now()函数获取当前系统时间,该函数返回一个Time结构。然后,您将时间传递给rand.Seed()函数。该函数接受一个64位整数(int64),因此您需要在now结构上使用Time.UnixNano()方法以纳秒的形式传入时间。最后,您正在打印用于初始化随机数生成器的时间。
现在保存并再次运行程序。
- go run random.go
你应该看到类似这样的输出。
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
Numbers seeded using current date/time: Jan 23 10:01:50.721413000 7 6 3 7 3
那比看到一个表示自1970年1月1日以来经过的纳秒数的巨大整数要好看。
如果你的程序不需要随机整数,而是需要UUIDs,许多程序员用它作为跨部署数据片段的全局唯一标识符,那该怎么办呢?Go标准库没有用于生成UUIDs的包,但是社区有。现在让我们看看如何下载和使用第三方包。
步骤2 — 使用第三方包
生成UUID的最受欢迎的软件包之一是github.com/google/uuid。第三方软件包的命名通常以完全限定名称来识别,其中包括托管代码的网站(例如github.com)、开发其的用户或组织(例如google)以及基本名称(例如uuid)。在导入软件包、阅读其在pkg.go.dev上的文档和其他地方时,您将使用软件包的完全限定名称。然而,在代码语句中引用它时,您只需要使用基本名称。
在下载一个软件包之前,你需要初始化一个模块,这是 Go 用于管理程序依赖及其版本的方式。要初始化一个模块,请使用 go mod init,并传入你自己的软件包的完全限定名称。如果你想在 GitHub 上以你的用户名 “sammy” 托管你的模块,你可以像这样初始化你的模块:
- go mod init github.com/sammy/random
这会创建一个名为 go.mod 的文件。让我们看一下这个文件:
- cat go.mod
module github.com/sammy/random go 1.19
这个文件必须出现在任何将作为Go模块分发的 Go 代码库的根目录中。它至少必须定义模块名称和所需的Go版本。你自己的Go版本可能不同于上面显示的版本。
在本教程中,您将不会分发您的模块,但是这一步骤对于下载和使用第三方软件包是必要的。
现在使用go get命令下载第三方UUID模块。
- go get github.com/google/uuid
这将下载最新版本。 (Zhè .)
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文件:
- cat /Users/sammy/go/pkg/mod/github.com/google/uuid@v1.3.0/go.mod
module github.com/google/uuid
看起来这个模块没有依赖第三方库,它只使用了Go标准库的包。
请注意,模块的版本包含在其目录名称中。这样可以让您在同一个程序内或不同程序之间开发和测试相同软件包的多个版本。
现在再次查看您自己的go.mod文件:
- 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来生成并打印出五个整数。
保存新的文件并运行它。
- go run uuid.go
您的输出应该类似于这个。
243811a3-ddc6-4e26-9649-060622bba2b0 b8129aa1-3803-4dae-bd9f-6ba8817f44b2 3ae27c71-caa8-4eaa-b8e6-e629b7c1cb49 37e06706-004d-4504-ad37-03c68252bb0f a2da6904-a6ab-4cc2-849b-d9d25a86e373
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的第三个新文件,并将以下程序粘贴进去。 , de compare.go de dì gè , .)
package main
import "os"
import "strconv"
func main() {
// User must pass in number of integers to generate
if len(os.Args) < 2 {
println("Usage:\n")
println(" go run compare.go <numberOfInts>")
return
}
n, err := strconv.Atoi(os.Args[1])
if err != nil { // Maybe they didn't pass an integer
panic(err)
}
println("User asked for " + strconv.Itoa(n) + " integers.")
}
这段代码可以让你通过rand包生成用户给定数量的伪随机整数。它使用os和strconv标准库包将单个命令行参数转换为整数。如果没有传入参数,它会打印使用说明并退出。
运行程序,使用一个参数为10,确保其正常工作。
- go run compare.go 10
[seconary_label Output]
User asked for 10 integers.
目前为止,一切都还不错。现在让我们使用math/rand包生成随机整数,就像之前一样,但这次你还要计算完成所需的时间。
第一阶段——测量数学/随机性能
删除最后一个println()语句,并用以下内容替换:
// Phase 1 — Using math/rand
// Initialize the byte slice
b1 := make([]byte, n)
// Get the time
start := time.Now()
// Initialize the random number generator
rand.Seed(start.UnixNano())
// Generate the pseudo-random numbers
for i := 0; i < n; i++ {
b1[i] = byte(rand.Intn(256)) // Where the magic happens!
}
// Compute the elapsed time
elapsed := time.Now().Sub(start)
// In case n is very large, only print a few numbers
for i := 0; i < 5; i++ {
println(b1[i])
}
fmt.Printf("Time to generate %v pseudo-random numbers with math/rand: %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"
)
添加了亮点套餐后,请运行程序。
- go run compare.go 10
189 203 209 238 235 Time to generate 10 pseudo-random numbers with math/rand: 33.417µs
生成十个介于0和255之间的整数并将它们存储在字节切片中,耗时33.417微秒。让我们看看这与crypto/rand的性能相比如何。
第二阶段 — 测量加密/随机性能
在添加使用crypto/rand的代码之前,按照之前演示的方式将该包添加到导入模块中。
import (
"fmt"
"math/rand"
crand "crypto/rand"
"os"
"strconv"
"time"
)
然后,将以下代码追加到你的main()函数的末尾:
// Phase 2 — Using crypto/rand
// Initialize the byte slice
b2 := make([]byte, n)
// Get the time (Note: no need to seed the random number generator)
start = time.Now()
// Generate the pseudo-random numbers
_, err = crand.Read(b2) // Where the magic happens!
// Compute the elapsed time
elapsed = time.Now().Sub(start)
// exit if error
if err != nil {
panic(err)
}
// In case n is very large, only print a few numbers
for i := 0; i < 5; i++ {
println(b2[i])
}
fmt.Printf("Time to generate %v pseudo-random numbers with crypto/rand: %v\n", n, elapsed)
此代码尽可能与第一阶段的代码相似。您正在生成大小为n的字节切片,获取当前时间,生成n个字节,计算经过的时间,最后打印五个整数和经过的时间。在使用crypto/rand包时,无需显式地设置随机数生成器的种子。
Note
你的整个compare.go程序应该是这样的:
package main
import (
"fmt"
"math/rand"
crand "crypto/rand"
"os"
"strconv"
"time"
)
func main() {
// User must pass in number of integers to generate
if len(os.Args) < 2 {
println("Usage:\n")
println(" go run compare.go <numberOfInts>")
return
}
n, err := strconv.Atoi(os.Args[1])
if err != nil { // Maybe they didn't pass an integer
panic(err)
}
// Phase 1 — Using math/rand
// Initialize the byte slice
b1 := make([]byte, n)
// Get the time
start := time.Now()
// Initialize the random number generator
rand.Seed(start.UnixNano())
// Generate the pseudo-random numbers
for i := 0; i < n; i++ {
b1[i] = byte(rand.Intn(256)) // Where the magic happens!
}
// Compute the elapsed time
elapsed := time.Now().Sub(start)
// In case n is very large, only print a few numbers
for i := 0; i < 5; i++ {
println(b1[i])
}
fmt.Printf("Time to generate %v pseudo-random numbers with math/rand: %v\n", n, elapsed)
// Phase 2 — Using crypto/rand
// Initialize the byte slice
b2 := make([]byte, n)
// Get the time (Note: no need to seed the random number generator)
start = time.Now()
// Generate the pseudo-random numbers
_, err = crand.Read(b2) // Where the magic happens!
// Compute the elapsed time
elapsed = time.Now().Sub(start)
// exit if error
if err != nil {
panic(err)
}
// In case n is very large, only print a few numbers
for i := 0; i < 5; i++ {
println(b2[i])
}
fmt.Printf("Time to generate %v pseudo-random numbers with crypto/rand: %v\n", n, elapsed)
}
使用参数为10运行程序,使用每个包生成十个8位整数。
- go run compare.go 10
145 65 231 211 250 Time to generate 10 pseudo-random numbers with math/rand: 32.5µs 101 188 250 45 208 Time to generate 10 pseudo-random numbers with crypto/rand: 42.667µs
在这个例子的执行中,math/rand 包比 crypto/rand 包稍微快一些。尝试多次运行 compare.go,并设置参数为10。然后尝试生成一千个整数,或者一百万个。哪个包始终更快呢?
这个示例程序旨在展示如何在同一个程序中使用两个名称相同、功能相似的包。它并不意味着推荐其中一个包而不是另一个。如果你想要扩展compare.go,你可以使用math/stats包来比较每个包生成的字节的随机性。无论你正在编写什么样的程序,评估不同的包并选择最适合你需求的才是你的责任。
最后,让我们来看一下如何使用goimports工具格式化导入声明。
第四步 — 使用Goimports
有时候在编程过程中,当您流畅地进行编程时,可能会忘记导入您正在使用的包,或者删除您不需要的包。goimports命令行工具不仅可以格式化您的导入声明和其他代码,使其成为gofmt的更具功能性的替代品,还可以为您代码引用的包补充任何缺失的导入,并删除未使用的包的导入。
工具默认情况下并没有随Go一起安装,所以立即使用go install来安装它。
- go install golang.org/x/tools/cmd/goimports@latest
这将把goimports二进制文件放置在你的$GOPATH/bin目录中。如果你按照教程在macOS上安装Go并设置本地编程环境(或者根据你的操作系统选择相应的教程)进行操作,那么该目录应已在你的$PATH中。尝试运行该工具。
- goimports –help
如果您无法看到工具的使用说明,则表示$GOPATH/bin不在您的$PATH中。请阅读适用于您操作系统的Go环境设置指南来设置它。
一旦goimports在您的$PATH中,从random.go文件中移除整个导入块。然后,使用-d选项运行goimports,以显示它要添加的差异。
- 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。
- 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文件,并进行以下操作:
-
- 添加一个导入 math/rand 的语句,目前代码中没有用到它。
- 将内建的 println() 函数改为 fmt.Println(),但不要添加 import “fmt”。
package main
import "math/rand"
func main() {
for i := 0; i < 5; i++ {
fmt.Println(uuid.New().String())
}
}
保存文件并再次运行goimports。
- goimports -d uuid.go
diff -u uuid.go.orig uuid.go — uuid.go.orig 2023-01-25 16:36:28 +++ 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()) } }
这个工具不仅可以添加缺失的导入,还可以删除不必要的导入。同时需要注意的是,它将导入语句放置在括号中的一个块内,而不是在每一行上使用导入关键字。
要将更改写入uuid.go(或任何文件)而不是将其输出到终端,请使用goimports的-w选项。
- goimports -w uuid.go
你应该设置你的文本编辑器或者集成开发环境,当你保存一个.go文件时调用goimports,这样你的源代码文件就会始终保持格式良好。如前所述,goimports取代了gofmt,所以如果你的文本编辑器已经使用gofmt,就把它配置成使用goimports。
另外,goimports 还会对你的导入进行强制排序。试图手动维护导入的顺序可能会很繁琐且容易出错,所以让 goimports 为你处理这个问题吧。
如果Go团队更改了Go源文件的官方格式,他们将会更新goimports以反映这些更改,因此你应该定期更新goimports以确保你的代码始终符合最新的标准。
结论 (Jié
在本教程中,您使用常用的软件包来帮助生成随机数和UUID,创建并运行了两个不超过十行的程序。Go生态系统中的软件包丰富多样且写得很好,所以使用Go编写新程序应该是一种乐趣,您会发现自己能够轻松地编写满足特定需求的实用程序,比您想象的要轻松。
请查看系列教程的下一个教程,学习如何在Go中编写包。然后,如果你愿意,可以提前了解如何使用Go模块,以便将包组合在一起并作为一个整体分发给他人。