创建 Go 模块
介绍模块、函数、错误处理、数组、map、单元测试和编译安装.
创建一个其他人可以使用的模块
在一个模块中,您为一组离散且有用的功能收集一个或多个相关包。例如,您可能会创建一个包含具有财务分析功能的包的模块,以便其他编写财务应用程序的人可以使用您的工作。有关开发模块的更多信息,请参阅 开发和发布模块。
Go 代码被分组到包中,包被分组到模块中。您的模块指定了运行代码所需的依赖项,包括 Go 版本和它所需的一组其他模块。
-
为 Go 模块代码创建一个目录
greetings
。mkdir greetings cd greetings
-
使用命令
go mod init
启动您的模块 。运行
go mod init
命令,会生成你的模块路径——example.com/greetings
. 如果您发布一个模块,这必须是 Go 工具可以从中下载您的模块的路径。那将是您的代码存储库。有关使用模块路径命名模块的更多信息,请参阅 管理依赖项。
$ go mod init example.com/greetings go: creating new go.mod: module example.com/greetings
-
创建代码文件 greetings.go,粘贴以下代码并保存文件。
package greetings import "fmt" // Hello returns a greeting for the named person. func Hello(name string) string { // Return a greeting that embeds the name in a message. message := fmt.Sprintf("Hi, %v. Welcome!", name) return message }
这是您的模块的第一个代码。它会向任何请求的呼叫者返回问候语。在此代码中,您:
- 声明一个
greetings
包来收集相关功能。 -
实现一个
Hello
函数来返回问候语。该函数接受一个类型为
string
的参数name
。该函数还返回一个string
. 在 Go 中,名称以大写字母开头的函数可以被不在同一个包中的函数调用。这在 Go 中称为导出名称。有关导出名称的更多信息,请参阅 Go tour 中的导出名称。 -
声明一个
message
变量来保存你的问候。在 Go 中,
:=
运算符是在一行中声明和初始化变量的快捷方式(Go 使用右侧的值来确定变量的类型)。从长远来看,您可能已将其写为:var message string message = fmt.Sprintf("Hi, %v. Welcome!", name)
-
使用
fmt
包的Sprintf
功能来创建问候消息。第一个参数是格式字符串,Sprintf
将参数name
的值替换为格式动词%v
。插入参数name
的值完成了问候文本。 - 将格式化的问候语文本返回给呼叫者。
- 声明一个
在另一个模块中调用代码
-
为 Go 模块创建一个目录
hello
,在这个文件夹写调用者的代码。cd .. mkdir hello cd hello
目录结构如下:
<home>/ |-- greetings/ |-- hello/
-
启用依赖项跟踪
运行go mod init,为模块启用依赖项跟踪
$ go mod init example.com/hello go: creating new go.mod: module example.com/hello
-
创建代码文件
hello.go
并粘贴以下代码package main import ( "fmt" "example.com/greetings" ) func main() { // Get a greeting message and print it. message := greetings.Hello("Gladys") fmt.Println(message) }
在此段代码中,您: - 声明一个
main
包。在Go中,作为应用程序执行的代码必须在main
包中。 - 导入两个包example.com/greetings
和fmt
。这使得您的代码可以访问这些包的函数。导入example.com/greetings
(您之前创建的模块中的包) 使您可以访问Hello
函数。导入fmt
,具有处理输入和输出文本的功能,例如将文本打印到控制台。 -
编辑
example.com/hello
模块以使用您的本地example.com/greetings
模块。对于生产用途,您将
example.com/greetings
从其存储库发布模块(使用反映其发布位置的模块路径),Go 工具可以在其中找到它并下载它。目前,由于您尚未发布该模块,您需要调整该example.com/hello
模块,以便它可以在您的本地文件系统上找到example.com/greetings
代码。为此,请使用
go mod edit
command 命令编辑example.com/hello
模块以将 Go 工具从其模块路径(模块所在的位置)重定向到本地目录(模块所在的位置)。-
在
hello
目录执行以下命令:$ go mod edit -replace example.com/greetings=../greetings
该命令指定
example.com/greetings
应替换../greetings
作为依赖项的定位。运行命令后, hello 目录中的 go.mod 文件应该包含一个replace
指令:module example.com/hello go 1.16 replace example.com/greetings => ../greetings
-
在 hello 目录中下,运行
go mod tidy
命令同步模块example.com/hello
的依赖关系,添加代码所需但尚未在模块中跟踪的那些。$ go mod tidy go: found example.com/greetings in example.com/greetings v0.0.0-00010101000000-000000000000
命令完成后,
example.com/hello
模块的 go.mod 文件应如下所示:module example.com/hello go 1.16 replace example.com/greetings => ../greetings require example.com/greetings v0.0.0-00010101000000-000000000000
该命令在
greetings
目录中找到了本地代码,然后添加了一个require
指令来指定example.com/hello
依赖example.com/greetings
。您在 hello.go 中导入greetings
包时创建了此依赖项。模块路径后面的数字是一个伪版本号 ——一个生成的数字用来代替语义版本号(模块还没有)。
要引用已发布的模块,go.mod 文件通常会省略
replace
指令并使用require
末尾带有标记版本号的指令。require example.com/greetings v1.1.0
-
-
在
hello
目录下,运行代码以确认其工作。$ go run . Hi, Gladys. Welcome!
返回并处理错误
处理错误是可靠代码的基本特征。在下面的教程中,您将添加一些代码以从greetings模块返回错误,然后在调用者中处理它。
-
修改
greetings/greetings.go
如下代码。如果您不知道该问候谁,那么发送问候是没有意义的。如果名称为空,则向调用者返回错误。将以下代码复制到
greetings.go
并保存文件。package greetings import ( "errors" "fmt" ) // Hello returns a greeting for the named person. func Hello(name string) (string, error) { // If no name was given, return an error with a message. if name == "" { return "", errors.New("empty name") } // If a name was received, return a value that embeds the name // in a greeting message. message := fmt.Sprintf("Hi, %v. Welcome!", name) return message, nil }
在这段代码中,您:
- 更改函数,使其返回两个值: 一个
string
和一个error
。您的调用者将检查第二个值以查看是否发生错误。(任何 Go 函数都可以返回多个值。有关更多信息,请参阅 Effective Go。) - 导入 Go 标准库
errors
包,以便您可以使用它的errors.New
函数。 - 添加一条
if
语句以检查无效请求(名称应为空字符串)并在请求无效时返回错误。该errors.New
函数返回一个error
内部带有您的消息。 - 添加
nil
(意味着没有错误)作为成功返回的第二个值。这样,调用者就可以看到函数成功了。
- 更改函数,使其返回两个值: 一个
-
在
hello/hello.go
文件中,处理Hello
函数现在返回的错误以及非错误值。package main import ( "fmt" "log" "example.com/greetings" ) func main() { // Set properties of the predefined Logger, including // the log entry prefix and a flag to disable printing // the time, source file, and line number. log.SetPrefix("greetings: ") log.SetFlags(0) // Request a greeting message. message, err := greetings.Hello("") // If an error was returned, print it to the console and // exit the program. if err != nil { log.Fatal(err) } // If no error was returned, print the returned message // to the console. fmt.Println(message) }
在这段代码中,您:
-
在
hello
目录运行 hello.go 以确认代码有效。现在你传入一个空名称,会得到一个错误。
$ go run . greetings: empty name exit status 1
返回随即问候语
您将添加一个小片段以包含三条问候消息,然后让您的代码随机返回其中一条消息。
为此,您将使用 Go slice(切片)。切片就像一个数组,只是它的大小会随着您添加和删除项目而动态变化。切片是 Go 最有用的类型之一。
有关切片的更多信息,请参阅 Go 博客中的 Go slices。
-
在
greetings/greetings.go
中,更改您的代码,使其如下所示。package greetings import ( "errors" "fmt" "math/rand" "time" ) // Hello returns a greeting for the named person. func Hello(name string) (string, error) { // If no name was given, return an error with a message. if name == "" { return name, errors.New("empty name") } // Create a message using a random format. message := fmt.Sprintf(randomFormat(), name) return message, nil } // init sets initial values for variables used in the function. func init() { rand.Seed(time.Now().UnixNano()) } // randomFormat returns one of a set of greeting messages. The returned // message is selected at random. func randomFormat() string { // A slice of message formats. formats := []string{ "Hi, %v. Welcome!", "Great to see you, %v!", "Hail, %v! Well met!", } // Return a randomly selected message format by specifying // a random index for the slice of formats. return formats[rand.Intn(len(formats))] }
在此代码中,您:
- 添加一个
randomFormat
函数,该函数返回随机选择的问候消息格式。请注意,randomFormat
以小写字母开头,使其只能被其自己的包中的代码访问(换句话说,它不会被导出)。 - 在
randomFormat
中,声明formats
具有三种消息格式的切片。声明切片时,在括号中省略其大小,如下所示:[]string
. 这告诉 Go,切片底层数组的大小可以动态更改。 - 使用
math/rand
包 生成一个随机数,用于从切片中选择一个项目。 - 添加一个
init
函数以使用当前时间为rand
包的随机数种子(seed)。Go在程序启动时自动执行init
函数,在全局变量被初始化之后。有关init
函数的更多信息,请参阅 Effective Go。 - 在
Hello
中,调用randomFormat
函数以获取您将返回的消息的格式,然后将格式和name
值一起使用来创建消息。 - 像以前一样返回消息(或错误)。
- 添加一个
-
在
hello/hello.go
中,更改您的代码,使其如下所示。只需将 Gladys 的名字(或其他名字,如果您愿意的话)作为参数添加到 hello.go 中的
Hello
函数调用中。package main import ( "fmt" "log" "example.com/greetings" ) func main() { // Set properties of the predefined Logger, including // the log entry prefix and a flag to disable printing // the time, source file, and line number. log.SetPrefix("greetings: ") log.SetFlags(0) // Request a greeting message. message, err := greetings.Hello("Gladys") // If an error was returned, print it to the console and // exit the program. if err != nil { log.Fatal(err) } // If no error was returned, print the returned message // to the console. fmt.Println(message) }
-
运行 hello.go 以确认代码正常运行。多次运行它,注意问候语发生了变化。
$ go run . Great to see you, Gladys! $ go run . Hi, Gladys. Welcome! $ go run . Hail, Gladys! Well met!
回复多人问候
添加对在一个请求中获取多个人的问候的支持。换句话说,处理多值输入,然后将该输入中的值与多值输出配对。为此,您需要将一组名称传递给一个函数,该函数可以为每个名称返回一个问候语。
但是有一个问题。将Hello
函数的参数从单个名称更改为一组名称会更改函数的签名。如果您已经发布了example.com/greetings
模块并且用户已经编写了代码调用Hello
,那么该更改将破坏他们的程序。
在这种情况下,更好的选择是编写一个具有不同名称的新函数。新函数将采用多个参数。这保留了旧功能以实现向后兼容性。
-
在
greetings/greetings.go
中,更改您的代码,使其如下所示。package greetings import ( "errors" "fmt" "math/rand" "time" ) // Hello returns a greeting for the named person. func Hello(name string) (string, error) { // If no name was given, return an error with a message. if name == "" { return name, errors.New("empty name") } // Create a message using a random format. message := fmt.Sprintf(randomFormat(), name) return message, nil } // Hellos returns a map that associates each of the named people // with a greeting message. func Hellos(names []string) (map[string]string, error) { // A map to associate names with messages. messages := make(map[string]string) // Loop through the received slice of names, calling // the Hello function to get a message for each name. for _, name := range names { message, err := Hello(name) if err != nil { return nil, err } // In the map, associate the retrieved message with // the name. messages[name] = message } return messages, nil } // Init sets initial values for variables used in the function. func init() { rand.Seed(time.Now().UnixNano()) } // randomFormat returns one of a set of greeting messages. The returned // message is selected at random. func randomFormat() string { // A slice of message formats. formats := []string{ "Hi, %v. Welcome!", "Great to see you, %v!", "Hail, %v! Well met!", } // Return one of the message formats selected at random. return formats[rand.Intn(len(formats))] }
在此代码中,您:
- 添加一个
Hellos
函数,其参数是名称切片而不是单个名称。此外,您将其返回类型之一从一个string
更改为一个map
以便您可以返回映射到问候消息的名称。 - 让新
Hellos
函数调用现有Hello
函数。这有助于减少重复,同时也保留这两个功能。 - 创建一个
messages
map 映射以将每个接收到的名称(作为键)与生成的消息(作为值)相关联。在 Go 中,您使用以下语法初始化map:make(map[key-type]value-type)
. 您有函数Hellos
将此映射返回给调用者。有关map的更多信息,请参阅Go 博客上的 Go maps in action 。 - 遍历您的函数收到的名称,检查每个名称是否具有非空值,然后将一条消息与每个关联。在这个
for
循环中,range
返回两个值:循环中当前项目的索引和项目值的副本。您不需要索引,因此您使用 Go 空白标识符(下划线)来忽略它。有关更多信息,请参阅 Effective Go 中的空白标识符。
- 添加一个
-
在您的
hello/hello.go
调用代码中,传递一段名称,然后打印您返回的名称/消息映射的内容。package main import ( "fmt" "log" "example.com/greetings" ) func main() { // Set properties of the predefined Logger, including // the log entry prefix and a flag to disable printing // the time, source file, and line number. log.SetPrefix("greetings: ") log.SetFlags(0) // A slice of names. names := []string{"Gladys", "Samantha", "Darrin"} // Request greeting messages for the names. messages, err := greetings.Hellos(names) if err != nil { log.Fatal(err) } // If no error was returned, print the returned map of // messages to the console. fmt.Println(messages) }
通过这些更改,您可以:
- 创建一个
names
变量作为包含三个名称的切片类型。 - 将
names
变量作为参数传递给Hellos
函数。
- 创建一个
-
切换到包含
hello/hello.go
的目录,然后使用go run
确认代码有效。输出应该是地图的字符串表示形式,将名称与消息相关联,如下所示:
$ go run . map[Darrin:Hail, Darrin! Well met! Gladys:Hi, Gladys. Welcome! Samantha:Hail, Samantha! Well met!]
本主题介绍了用户表示名称/值对的map映射。它还引入了通过为模块中增加新函数或更改函数实现新功能来保持向后兼容性的想法。有关向后兼容性的更多信息,请参阅 保持模块兼容。
添加测试
现在您已经将代码放到了一个稳定的地方,添加一个测试,在开发期间测试您的代码可能会暴露在您进行更改时发现的错误。
Go 对单元测试的内置支持使您可以更轻松地进行测试。具体来说,使用命名约定、Go 的testing
包和go test
命令,您可以快速编写和执行测试。
-
在 greetings 目录中,创建一个名为
greetings_test.go
的文件。以 _test.go 结尾的文件名告诉
go test
命令该文件包含测试函数。 -
在 greetings_test.go 中,粘贴以下代码并保存文件。
package greetings import ( "testing" "regexp" ) // TestHelloName calls greetings.Hello with a name, checking // for a valid return value. func TestHelloName(t *testing.T) { name := "Gladys" want := regexp.MustCompile(`\b`+name+`\b`) msg, err := Hello("Gladys") if !want.MatchString(msg) || err != nil { t.Fatalf(`Hello("Gladys") = %q, %v, want match for %#q, nil`, msg, err, want) } } // TestHelloEmpty calls greetings.Hello with an empty string, // checking for an error. func TestHelloEmpty(t *testing.T) { msg, err := Hello("") if msg != "" || err == nil { t.Fatalf(`Hello("") = %q, %v, want "", error`, msg, err) } }
在此代码中,您:
- 在与您正在测试的代码相同的包中实现测试函数。
- 创建两个测试函数来测试
greetings.Hello
函数。测试函数名称的格式为TestName
,其中Name说明了特定测试。此外,测试函数将指向包testing
类型testing.T
的指针作为参数。您可以使用此参数的方法来报告和记录您的测试。 - 实施两个测试:
TestHelloName
调用Hello
函数,传递一个name
值,函数应该能够使用该值返回有效的响应消息。如果调用返回错误或意外响应消息(不包括您传入的名称),则使用t参数的Fatalf
方法将消息打印到控制台并结束执行。TestHelloEmpty
使用空字符串调用Hello
函数。此测试旨在确认您的错误处理是否有效。如果调用返回非空字符串或没有错误,则使用t
参数的Fatalf
方法将消息打印到控制台并结束执行。
-
在greetings目录下的命令行,运行
go test
命令 执行测试。go test
命令在测试文件(名称以 _test.go 结尾)中执行测试函数(名称以Test
开头)。您可以添加-v
标志以获得列出所有测试及其结果的详细输出。$ go test PASS ok example.com/greetings 0.364s $ go test -v === RUN TestHelloName --- PASS: TestHelloName (0.00s) === RUN TestHelloEmpty --- PASS: TestHelloEmpty (0.00s) PASS ok example.com/greetings 0.372s
-
中断
greetings.Hello
函数以查看失败的测试。TestHelloName
测试函数检查您指定名称为Hello
的函数的返回值。要查看失败的测试结果,请更改greetings.Hello
函数以使其不再包含名称。在
greetings/greetings.go
中,粘贴以下代码代替Hello
函数。请注意,突出显示的行会更改函数返回的值,就好像name参数被意外删除了一样。// Hello returns a greeting for the named person. func Hello(name string) (string, error) { // If no name was given, return an error with a message. if name == "" { return name, errors.New("empty name") } // Create a message using a random format. // message := fmt.Sprintf(randomFormat(), name) message := fmt.Sprint(randomFormat()) return message, nil }
-
在greetings目录下,运行
go test
执行测试。这一次,在
go test
没有-v
参数的情况下运行。输出将仅包含失败测试的结果,这在您有大量测试时很有用。测试TestHelloName
应该失败——TestHelloEmpty
仍然通过。$ go test --- FAIL: TestHelloName (0.00s) greetings_test.go:15: Hello("Gladys") = "Hail, %v! Well met!", <nil>, want match for `\bGladys\b`, nil FAIL exit status 1 FAIL example.com/greetings 0.182s
编译和安装应用程序
学习几个新go
命令。虽然该go run
命令是在您进行频繁更改时编译和运行程序的有用快捷方式,但它不会生成二进制可执行文件。
本主题介绍了用于构建代码的两个附加命令:
- 该
go build
命令会编译软件包及其依赖项,但不会安装结果。 - 该
go install
命令编译并安装包。
常用命令
-
从 hello 目录中的命令行,运行
go build
命令将代码编译为可执行文件。$ go build
-
从 hello 目录中的命令行,运行新的
hello
可执行文件以确认代码有效。请注意,您的结果可能会有所不同,具体取决于您是否在测试后更改了 greetings.go 代码。
-
在 Linux 或 Mac 上:
$ ./hello map[Darrin:Great to see you, Darrin! Gladys:Hail, Gladys! Well met! Samantha:Hail, Samantha! Well met!]
-
在 Windows 上:
$ hello.exe map[Darrin:Great to see you, Darrin! Gladys:Hail, Gladys! Well met! Samantha:Hail, Samantha! Well met!]
您已将应用程序编译为可执行文件,以便可以运行它。但是要在当前目录运行它,您的命令提示符需要位于可执行文件的目录中,或者指定可执行文件的路径。
接下来,您将安装可执行文件,以便在不指定其路径的情况下运行它。
-
-
发现 Go 安装路径,该
go
命令将在其中安装当前包。您可以通过运行
go list
命令 来发现安装路径, 如下例所示:$ go list -f '{{.Target}}'
例如,该命令的输出可能会
/home/gopher/bin/hello
显示 ,这意味着二进制文件将被安装到/home/gopher/bin
。您将在下一步中需要此安装目录。 -
将 Go 安装目录添加到系统的 shell 路径。
这样,您将能够运行程序的可执行文件,而无需指定可执行文件的位置。
-
在 Linux 或 Mac 上,运行以下命令:
$ export PATH=$PATH:/path/to/your/install/directory
-
在 Windows 上,运行以下命令:
$ set PATH=%PATH%;C:\path\to\your\install\directory
作为替代方案,如果您的 shell 路径中已经有一个目录
$HOME/bin
,并且您想在那里安装您的 Go 程序,您可以通过使用go env
命令设置变量GOBIN
来更改安装目标:$ go env -w GOBIN=/path/to/your/bin
或者
$ go env -w GOBIN=C:\path\to\your\bin
-
-
更新 shell 路径后,运行
go install
命令编译和安装包。$ go install
-
只需键入其名称即可运行您的应用程序。为了让这个有趣,打开一个新的命令提示符并在其他目录中运行可执行文件名
hello
。$ hello map[Darrin:Hail, Darrin! Well met! Gladys:Great to see you, Gladys! Samantha:Hail, Samantha! Well met!]