介绍
借用和共享不同项目中的代码的能力对于任何广泛使用的编程语言和整个开源社区都至关重要,借用代码使程序员能够花费大部分时间来编写具体的代码,但往往他们的一些新代码最终对其他人有用,然后他们可以决定将这些可重复使用的部分组织成一个单位,并在团队或更广泛的编程社区中分享。
在Go中,可重复使用的代码的基本单元称为 package。即使是最简单的Go程序也是其自己的软件包,并且可能使用至少一个其他软件包。在本教程中,您将写出两个小程序:一个使用标准库包来生成随机号码,另一个使用受欢迎的第三方软件包来生成UUID。
<$>[注] **注:**在Go中也有一个更高级别的可重复使用代码单元: module. 模块是包的版本集。
前提条件
在开始本教程之前,您只需要安装 Go. 阅读您的操作系统的正确教程:
- [如何在Ubuntu 20.04上安装Go(https://andsky.com/tech/tutorials/how-to-install-go-on-ubuntu-20-04)
- 如何在macOS上安装Go并设置本地编程环境
- 如何在Windows 10上安装Go并设置本地编程环境
步骤 1 – 使用标准图书馆包
与大多数语言一样,Go 配备了可重复使用的代码的内置库,可用于常见任务.您不需要编写自己的代码来格式化和打印字符串,或者发送 HTTP 请求,例如。
在 How To Write Your First Program In Go中的程序使用了标准库中的 fmt
和 strings
包,让我们写一个使用 math/rand
包来生成一些随机数字的程序。
在nano
或您最喜欢的文本编辑器中打开名为random.go
的新文件:
1nano random.go
让我们创建一个打印从零到九的五个随机整数的程序,将以下内容插入您的编辑器:
1package main
2
3import "math/rand"
4
5func main() {
6 for i := 0; i < 5; i++ {
7 println(rand.Intn(10))
8 }
9}
该程序导入 math/rand
包,并通过引用其 base name, rand
来使用它,这是在包中的每个 Go 源文件的顶部出现的 package <pkgname>
声明中的名称。
for-loop 的每个迭代都会调用rand.Intn(10)来生成零到九之间的随机整数(10不包括),然后将整数打印到控制台。
<$>[note]
请注意,呼叫到 println()
不会引用一个包名称。 这是一个不需要导入的 builtin 函数。 通常,你会从这里的 fmt' 包中使用
fmt.Println()' 函数,但这个程序使用 `println()' 来导入 builtin 函数
<$>
如果您正在使用nano
,请按CTRL+X
,然后按Y
和ENTER
,以确认您的更改。
1go run random.go
你应该看到从零到九的五个整数:
1[secondary_label Output]
21
37
47
59
61
它看起来像是随机号码生成器正在工作,但请注意,如果您运行该程序一遍又一遍,它会打印相同的数字,而不是新的随机号码,就像您预期的那样。
因此,您需要在每次运行程序时将数字生成器种植到一个独特的值。程序员通常会使用纳米秒的当前时刻印。 要做到这一点,您需要时间
包。 在您的编辑器中再次打开`随机。
1package main
2
3import (
4 "math/rand"
5 "time"
6)
7
8func main() {
9 now := time.Now()
10 rand.Seed(now.UnixNano())
11println("Numbers seeded using current date/time:", now.UnixNano())
12 for i := 0; i < 5; i++ {
13 println(rand.Intn(10))
14 }
15}
当导入多个包时,您可以使用窗口创建一个导入块. 使用一个块,您可以避免在每个行上重复导入
关键字,从而使您的代码更清洁。
首先,您通过time.Now()
函数获取当前的系统时间,该函数返回Time
结构,然后您将时间传递到rand.Seed()
函数,该函数需要64位整数(int64
),因此您需要在now
结构上使用Time.UnixNano()
方法以在纳米秒内传递时间。
现在保存并重新运行程序:
1go run random.go
你应该看到类似于此的输出:
1[secondary_label Output]
2Numbers seeded using current date/time: 1674489465616954000
32
46
53
61
70
如果你运行该程序几次,你现在应该看到不同的整数每次,以及用于播种随机数生成器的独特整数。
让我们再一次编辑程序,以便以更易于使用的格式打印种子时间。
1println("Numbers seeded using current date/time:", now.Format(time.StampNano))
现在你正在调用Time.Format()
方法,并通过在time
包中定义的许多格式之一。time.StampNano
常数(const
)是一个字符串,并将其传输到Time.Format()
,允许你打印月,日和时间,降至纳米秒。
1go run random.go
1[secondary_label Output]
2Numbers seeded using current date/time: Jan 23 10:01:50.721413000
37
46
53
67
73
这比看到一个巨大的整数,代表了自1970年1月1日以来过去的纳米秒数更漂亮。
如果你的程序不需要随机整数,而是 UUID,许多程序员在部署中使用这些数据作为全球唯一标识符? Go 标准库没有生成这些数据的包,但社区确实这样做。
步骤 2 – 使用第三方包
生成 UUID 的最流行的包之一是 github.com/google/uuid
。 第三方包总是以其完全合格的名称而闻名,其中包括托管代码的网站(如 github.com)、开发它的用户或组织(如 google)和基础名称(如 uuid)。
在下载一个包之前,你需要初始化一个模块,这就是 Go 如何管理一个程序的依赖性及其版本。 要初始化一个模块,请使用go mod init
并为自己的包输入一个完全合格的名称。
1go mod init github.com/sammy/random
这会创建一个名为go.mod
的文件,让我们看看这个文件:
1cat go.mod
1[secondary_label Output]
2module github.com/sammy/random
3
4go 1.19
此文件必须出现在将作为 Go 模块分发的任何 Go 存储库的根部。它至少必须定义模块名称和所需的 Go 版本。
您不会在本教程中分发您的模块,但此步骤是下载和使用第三方包所必需的。
现在使用go get
命令下载第三方 UUID 模块:
1go get github.com/google/uuid
下载最新版本:
1[secondary_label Output]
2go: downloading github.com/google/uuid v1.3.0
3go: added github.com/google/uuid v1.3.0
该包被放置在您的本地目录$GOPATH/pkg/mod/
中。如果您的壳中没有明确设置$GOPATH
,其默认值是$HOME/go
。如果您的本地用户是sammy
,并且您正在运行macOS,例如,此包将下载到/Users/sammy/go/pkg/mod
。
让我们来看看你的新依赖的go.mod
文件:
1cat /Users/sammy/go/pkg/mod/github.com/google/[email protected]/go.mod
1[secondary_label Output]
2module github.com/google/uuid
看起来这个模块没有第三方依赖性;它只使用来自Go标准库的包。
请注意,该模块的版本包含在其目录名称中,这允许您对同一包的多个版本进行开发和测试,无论是在一个程序中还是在您正在编写的不同程序中。
现在再来看看你自己的go.mod
文件:
1cat go.mod
1[secondary_label Output]
2module github.com/sammy/random
3
4go 1.19
5
6require github.com/google/uuid v1.3.0 // indirect
「go get」命令注意到你当前目录中的「go.mod」文件,并更新它以反映你的程序的新依赖性,包括它的版本。
1package main
2
3import "github.com/google/uuid"
4
5func main() {
6 for i := 0; i < 5; i++ {
7 println(uuid.New().String())
8 }
9}
这个程序类似于random.go
,但它使用github.com/google/uuid
来打印五个UUID,而不是使用math/rand
来打印五个整数。
保存新文件并运行它:
1go run uuid.go
您的输出应该类似于此:
1[secondary_label Output]
2243811a3-ddc6-4e26-9649-060622bba2b0
3b8129aa1-3803-4dae-bd9f-6ba8817f44b2
43ae27c71-caa8-4eaa-b8e6-e629b7c1cb49
537e06706-004d-4504-ad37-03c68252bb0f
6a2da6904-a6ab-4cc2-849b-d9d25a86e373
github.com/google/uuid
包通过使用标准库包crypto/rand
生成这些 UUID,这与你在random.go
中使用的math/rand
包相似但不同。
步骤 3 – 导入相同名称的包
数/rand
的文档说,它实际上生成伪随机数,并且不适合安全敏感的工作
。 对于这种工作,你会使用crypto/rand
代替,但如果整数的随机性质量对你的程序并不重要? 也许你真的只需要任意数。
你可以编写一个程序来比较这两个边缘
包的性能,但你不能在整个程序中用边缘
名称引用这两个包。
以下是如何导入具有相同基础名称的两个包:
1import (
2 “math/rand”
3 crand “crypto/rand”
4)
您可以选择您喜欢的任何副名称(只要它不匹配其他包名称),并将其放置在完全合格的包名称的左侧。在这种情况下,副名称是crand
。 请注意,该副名称周围没有引用标记。 在包含此导入块的其他源文件中,您可以使用您选择的名称crand
访问crypto/rand
包。
您也可以将包导入自己的名称空间(使用 .
作为名称)或作为空白标识符(使用 _
作为名称)。
为了说明您可能想要如何使用相同名称的包,让我们创建并运行一个更长的程序,该程序使用两种包生成随机整数,并测量每个案例所需的时间。
比较math/rand
和crypto/rand
(可选)
获取命令行论点
首先,在工作目录中打开一个名为compare.go
的第三个新文件,然后粘贴到以下程序中:
1package main
2
3import "os"
4import "strconv"
5
6func main() {
7 // User must pass in number of integers to generate
8 if len(os.Args) < 2 {
9 println("Usage:\n")
10 println(" go run compare.go <numberOfInts>")
11 return
12 }
13 n, err := strconv.Atoi(os.Args[1])
14 if err != nil { // Maybe they didn't pass an integer
15 panic(err)
16 }
17
18 println("User asked for " + strconv.Itoa(n) + " integers.")
19}
此代码为您准备以后续使用rand
套件生成用户给出的假随机整数数。它使用os
和strconv
标准库套件将单个命令行参数转换为整数。
使用单个10
参数运行程序,以确保它工作:
1go run compare.go 10
1[seconary_label Output]
2User asked for 10 integers.
到目前为止,那么好吧,现在让我们使用数/边
包生成随机整数,就像你以前一样,但这次你会计算需要花费的时间来做到这一点。
第一阶段 - 测量数学/边缘
绩效
删除最终的 println()
语句,并用以下语句替换:
1// Phase 1 — Using math/rand
2 // Initialize the byte slice
3 b1 := make([]byte, n)
4 // Get the time
5 start := time.Now()
6 // Initialize the random number generator
7 rand.Seed(start.UnixNano())
8 // Generate the pseudo-random numbers
9 for i := 0; i < n; i++ {
10 b1[i] = byte(rand.Intn(256)) // Where the magic happens!
11 }
12 // Compute the elapsed time
13 elapsed := time.Now().Sub(start)
14 // In case n is very large, only print a few numbers
15 for i := 0; i < 5; i++ {
16 println(b1[i])
17 }
18 fmt.Printf("Time to generate %v pseudo-random numbers with math/rand: %v\n", n, elapsed)
首先,您正在使用内置函数 make()
创建一个空的字节([]byte
)片段,以保持生成的整数(作为字节)。
然后,你正在获得当前的时间,并与它一起播种随机数字生成器,就像你在步骤 1 中在`随机。
之后,你会生成n
在 0 和 255 之间的伪随机整数,将每一个整数转换为一个字节,并将其放入你的字节片段。为什么整数在 0 和 255 之间? 因为你要写的crypto/rand
代码会生成字节,而不是任何大小的整数,我们应该对这些包进行平等的比较。 一个字节,即 8 位,可能由 0 到 255 之间的未签名的整数表示。
最后,您只打印了前五个字节,如果用户要求一个非常大的整数。
在运行该程序之前,不要忘了将您正在使用的新包添加到您的导入
块:
1import (
2 "fmt"
3 "math/rand"
4 "os"
5 "strconv"
6 "time"
7)
添加了突出的包后,运行该程序:
1go run compare.go 10
1[secondary_label Output]
2189
3203
4209
5238
6235
7Time to generate 10 pseudo-random numbers with math/rand: 33.417µs
它花了33417微秒来生成10个0至255之间的整数,并将它们存储在一个字节片段中。
第二阶段 – 测量加密货币/rand
绩效
在添加使用crypto/rand
的代码之前,如上所示,将该包添加到您的进口
块中:
1import (
2 "fmt"
3 "math/rand"
4 crand "crypto/rand"
5 "os"
6 "strconv"
7 "time"
8)
然后,将下面的代码附加到您的main()
函数的末尾:
1// Phase 2 — Using crypto/rand
2 // Initialize the byte slice
3 b2 := make([]byte, n)
4 // Get the time (Note: no need to seed the random number generator)
5 start = time.Now()
6 // Generate the pseudo-random numbers
7 _, err = crand.Read(b2) // Where the magic happens!
8 // Compute the elapsed time
9 elapsed = time.Now().Sub(start)
10 // exit if error
11 if err != nil {
12 panic(err)
13 }
14 // In case n is very large, only print a few numbers
15 for i := 0; i < 5; i++ {
16 println(b2[i])
17 }
18 fmt.Printf("Time to generate %v pseudo-random numbers with crypto/rand: %v\n", n, elapsed)
这个代码尽可能地反映了第一阶段的代码,你正在生成一个大小n
的字节片,获取当前时间,生成n
字节,计算过去的时间,最后打印五个整数和过去的时间。
<$>[注]
注:crypto/rand
也包含一个Int()
函数,但我们的示例在这里使用Read()
,因为这是包文档中唯一的示例代码片段所使用的。
你的整个compare.go
程序应该是这样的:
1package main
2
3import (
4 "fmt"
5 "math/rand"
6 crand "crypto/rand"
7 "os"
8 "strconv"
9 "time"
10)
11
12func main() {
13 // User must pass in number of integers to generate
14 if len(os.Args) < 2 {
15 println("Usage:\n")
16 println(" go run compare.go <numberOfInts>")
17 return
18 }
19 n, err := strconv.Atoi(os.Args[1])
20 if err != nil { // Maybe they didn't pass an integer
21 panic(err)
22 }
23
24 // Phase 1 — Using math/rand
25 // Initialize the byte slice
26 b1 := make([]byte, n)
27 // Get the time
28 start := time.Now()
29 // Initialize the random number generator
30 rand.Seed(start.UnixNano())
31 // Generate the pseudo-random numbers
32 for i := 0; i < n; i++ {
33 b1[i] = byte(rand.Intn(256)) // Where the magic happens!
34 }
35 // Compute the elapsed time
36 elapsed := time.Now().Sub(start)
37 // In case n is very large, only print a few numbers
38 for i := 0; i < 5; i++ {
39 println(b1[i])
40 }
41 fmt.Printf("Time to generate %v pseudo-random numbers with math/rand: %v\n", n, elapsed)
42
43 // Phase 2 — Using crypto/rand
44 // Initialize the byte slice
45 b2 := make([]byte, n)
46 // Get the time (Note: no need to seed the random number generator)
47 start = time.Now()
48 // Generate the pseudo-random numbers
49 _, err = crand.Read(b2) // Where the magic happens!
50 // Compute the elapsed time
51 elapsed = time.Now().Sub(start)
52 // exit if error
53 if err != nil {
54 panic(err)
55 }
56 // In case n is very large, only print a few numbers
57 for i := 0; i < 5; i++ {
58 println(b2[i])
59 }
60 fmt.Printf("Time to generate %v pseudo-random numbers with crypto/rand: %v\n", n, elapsed)
61}
使用 10 个参数运行程序,使用每个包生成 10 个 8 位整数:
1go run compare.go 10
1[secondary_label Output]
2145
365
4231
5211
6250
7Time to generate 10 pseudo-random numbers with math/rand: 32.5µs
8101
9188
10250
1145
12208
13Time to generate 10 pseudo-random numbers with crypto/rand: 42.667µs
在这个示例执行中,数/边
包比crypto/rand
更快一点. 尝试运行compare.go
以10的参数多次,然后尝试生成一千个整数 - 或者一百万。
这个示例程序旨在展示你如何在同一个程序中使用两个具有相同名称和类似目的的包。它不是用来推荐这些包中的一个。如果你想扩展 compare.go
,你可以使用 math/stats
包来比较每个包产生的字节的随机性。 无论你写什么程序,你要评估不同的包,并选择最适合你的需求的包。
最后,让我们看看如何使用goimports
工具格式化进口
声明。
步骤 4 – 使用 Goimport
有时,当你处于编程流程中时,你会忘记导入你正在使用的包,或者删除那些你没有使用的包。 goimports
命令行工具不仅格式化了你的导入
声明(s) - 以及你的代码的其余部分,使其成为gofmt
的更具功能的替代品 - 它还添加了您的代码引用,并删除了未使用的包的导入
。
该工具默认情况下不配备 Go,所以现在使用Go install
来安装它:
1go install golang.org/x/tools/cmd/goimports@latest
如果您遵循了教程 如何在 macOS 上安装 Go 和设置本地编程环境(或操作系统的匹配教程),此目录应该已经在您的 $PATH 中。
1goimports --help
如果您看不到该工具的使用声明,则$GOPATH/bin
不在您的$PATH
中。
一旦goimports
处于您的$PATH
,请从random.go
中删除整个import
块,然后使用-d
选项运行goimports
,以显示它想要添加的东西的差异:
1goimports -d random.go
1[secondary_label Outputs]
2diff -u random.go.orig random.go
3--- random.go.orig 2023-01-25 16:29:38
4+++ random.go 2023-01-25 16:29:38
5@@ -1,5 +1,10 @@
6 package main
7
8+import (
9+ "math/rand"
10+ "time"
11+)
12+
13 func main() {
14 now := time.Now()
15 rand.Seed(now.UnixNano())
令人印象深刻,但goimports
也可以识别并添加第三方包,如果它们通过go get
本地安装,请从uuid.go
中删除import
并运行goimports
:
1goimports -d uuid.go
1[secondary_label Output]
2diff -u uuid.go.orig uuid.go
3--- uuid.go.orig 2023-01-25 16:32:56
4+++ uuid.go 2023-01-25 16:32:56
5@@ -1,8 +1,9 @@
6 package main
7
8+import "github.com/google/uuid"
9+
10 func main() {
11 for i := 0; i < 5; i++ {
12 println(uuid.New().String())
13 }
14 }
现在编辑uuid.go
并:
添加一个导入
为math/rand
,代码不使用
2. 将内置的println()
函数更改为fmt.Println()
,但不要添加导入
fmt``
1[label uuid.go]
2package main
3
4import "math/rand"
5
6func main() {
7 for i := 0; i < 5; i++ {
8 fmt.Println(uuid.New().String())
9 }
10}
保存文件并再次运行goimports
:
1goimports -d uuid.go
1[secondary_label Output]
2diff -u uuid.go.orig uuid.go
3--- uuid.go.orig 2023-01-25 16:36:28
4+++ uuid.go 2023-01-25 16:36:28
5@@ -1,10 +1,13 @@
6 package main
7
8-import "math/rand"
9+import (
10+ "fmt"
11
12+ "github.com/google/uuid"
13+)
14+
15 func main() {
16 for i := 0; i < 5; i++ {
17 fmt.Println(uuid.New().String())
18 }
19 }
该工具不仅添加了缺少的导入,还删除了不必要的导入,同时也注意到它将导入置于窗格中,而不是在每个行中使用导入
关键字。
若要將變更寫入「uuid.go」 (或任何檔案),而不是將其輸出到終端,請使用「goimports」與「-w」選項:
1goimports -w uuid.go
您应该将文本编辑器或 IDE 配置为在保存一个 * go 文件时调用goimports
,以便您的源文件始终保持良好格式化。
另一个goimports
所做的事情是强制对您的进口物执行特定的排序订单. 尝试手动维护您的进口物的顺序可能是无聊的和容易出现错误的,所以让goimports
为您处理这个问题。
如果 Go 团队更改 Go 源文件的官方格式,他们将更新goimports
以反映这些变化,所以您应该定期更新goimports
,以确保您的代码始终符合最新标准。
结论
在本教程中,您创建并运行了两个程序,长达10行,使用流行的包,以帮助您生成随机数字和UUID。Go生态系统是如此丰富的书面包,写一个新的程序在Go应该是一个乐趣,你会发现自己写有用的程序,以满足你的具体需求,比你想象的更少的努力。
查看系列中的下一个教程, 如何在Go中编写包裹。然后,如果您正在寻找它,请先看看 如何使用Go模块),以了解如何将包裹组合在一起,并将它们分发给其他人作为一个单元。