这是介绍Go语言的一些基本特性的教程的第一部分,如果你刚开始接触Go,请确保已经阅读过前面介绍go命令,go语言模块以及非常简单的go代码的相关教程 Get started with Go。
在这一章的教程里我们会创建两个模块,其中一个模块是专门用来被其他库或者程序引用和导入的,而另一个模块就是用来引用和导入前面的模块的。
这一章的教程里包含7个主题,分别用来说明Go语言的不同组成部分。
Create a module
Prerequisites - 前提条件
- 本章的代码非常简单,但是如果你有一点关于函数、循环和数组的其他编程语言的经验会更有帮助。
- 一个编辑器,vscode、gland、vim都可以,个人推荐vscode。
- 一个命令行终端,Linux/Mac自带的终端即可,windows可以用powershell。
Start a module that others can use - 构建一个模块可以被他人使用
这部分让我们从创建一个模块开始,在模块中,你可以集合一个或多个想关联的包,它们包含了一些列彼此隔离又有用的函数集。比如你可以构建一个包含一些金融分析函数的包,然后其他编写金融程序的人就可以通过模块利用你已经完成的工作去访问和调用这些包里的函数。更多内容请参考 Developing and publishing modules
go语言代码被组织成包,包又被组织成模块。你的模块会具体说明需要那些依赖包才可以运行你的go语言程序,这份说明里包含了go语言的版本以及依赖的其他模块集合。
当你为自己构建的模块添加或者更新功能并且发布之后,那些原先通过调用你发布的模块实现某些功能的开发者可以通过导入更新的模块来更新自己的程序,并且在通过测试后更新他们自己的代码。
举个例子,让我们看看具体怎么做:
- 在根目录新建一个greetings的目录
$ cd
$ mkdir greetings
$ cd greetings
- 使用go mod init <module path>开始创建模块
如果你的模块是用于对外发布使用的,那么module path必须是可以被Go语言工具下载到的,一般应该是你的代码仓库地址(github之类的)。比如根据示例:
$ go mod init example.com/greetings
go: creating new go.mod: module example.com/greetings
更多关于模块命名和模块路径的内容,请参考Managing dependencies
go mod init命令会生成一个名为go.mod的文件来追踪你的代码依赖关系。但是到目前为止它只包含你的模块名称和你的go语言代码支持版本两种信息。但是随着你添加代码依赖关系,go.mod会列出你的代码依赖关系,这可以保持构建的可复制性,并让您直接控制要使用的模块版本。
- 接下来我们新建一个代码文件greetings.go,它的内容如下:
package greetings
import "fmt"
// Hello函数接受一个string类型的参数,并且返回一个string类型值。
func Hello(name string) string { //大写字母开始的函数名表示该函数是可以被外部访问到的
// Return a greeting that embeds the name in a message.
message := fmt.Sprintf("Hi, %v. Welcome!", name)
return message
}
代码很简单,就是根据函数参数的名字返回打招呼的一段字符串,可以参考代码注释。
上面的Hello()函数可以通过导入example.com/greetings模块的greetings包访问,下面我们来看看如果从其他模块来访问它。
Call your code from another module
这一节我们要编写代码来访问之前创建的greetings包中的Hello()函数,
- 在greetings的同级目录下创建hello目录
$ mkdir hello
$ cd hello
- 同样也在hello模块中开启模块依赖跟踪
$ go mod init example.com/hello
go: creating new go.mod: module example.com/hello
- 创建一个hello.go代码文件
package main
import (
"fmt"
"example.com/greetings" //导入fmt和之前创建的example.com/greetings模块
)
func main() { //go语言的可执行程序代码也是从main函数入口的
// Get a greeting message and print it.
message := greetings.Hello("Gladys") //调用example.com/greetings模块中greetings包里的Hello()函数
fmt.Println(message)
}
- 编辑example.com/hello模块使用你本地保存的example.com/greetings模块
上面hello.go文件导入的example.com/greetings在hello.go程序执行时默认会从远程仓库代码中下载模块代码,但是因为我们还没有发布它,所以需要修改在本地查找可导入模块代码。
1. 使用go mod edit命令修改go.mod。
$ go mod edit -replace example.com/greetings=../greetings
2. 执行完成后hello目录下的go.mod文件应该是这样的:
module example.com/hello
go 1.16
replace example.com/greetings => ../greetings
3. 使用go mod tidy命令同步并生效对go.mod的修改到模块当中
$ go mod tidy
4. 执行完成后hello目录下的go.mod文件应该是这样的:
module example.com/hello
go 1.16
replace example.com/greetings => ../greetings
require example.com/greetings v0.0.0-00010101000000-000000000000
模块名称后面跟着的是模块的版本号信息,在定义模块依赖关系的时候,一定要注明模块的版本号,用来指定自己需要引用的模块版本。
5. 使用go run . 执行hello.go程序
$ go run .
Hi, Gladys. Welcome!
下一节介绍错误处理
Return and handle an error
这一节我们修改greetings模块,使其能返回一个错误,并且还要在hello模块中对错误进行处理。
- 修改greetings.go添加错误返回:
package greetings
import (
"errors"
"fmt"
)
// Hello returns a greeting for the named person.
func Hello(name string) (string, error) {
// 如果name为空则返回一条错误信息
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
}
- 修改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)
// 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.go程序里传入的参数是空("")所以会返回错误:
$ go run .
greetings: empty name
exit status 1
下一节我们学习使用go语言切片实现返回随机的打招呼语句。
Return a random greeting
这一节我们修改代码实现返回随机组成的问候语句,为了实现这个目标,我们需要使用go语言的切片数据类型,切片和传统的数组十分相似,但有一点不同-它会随着切片元素的增加或者减少自动变化类型大小。
按照以下内容修改代码:
- 修改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))]
}
- 修改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)
// 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!
下一节我们再次修改greetings来实现给多人返回问候语句。
Return greetings for multiple people
这一节我们修改一下问候语句的生成,使greetings不再只能返回对一个人的问候语句,我们会定一个字符串切片包含3个人名,greetings包中的函数可以对所有的人命返回随机的一句问候。通过这些,我们能初步了解go语言的切片定义、映射定义、函数多值返回等特性。
开始修改代买之前,有个问题需要先注意一下,在我们之前构建的greetings包中,Hello()函数接受一个string类型的参数输入,返回string类型和error类型值,如果我们直接修改Hello()函数,并且这个包之前已经对外发布了,那么之前下载并且导入过我们的包的程序,如果不修改调用Hello()函数时传入的值类型以及不对Hello()函数返回值处理做相应修改,那么他们的代码就会编译出错,这不是一个很好的软件设计原则。我们对已发布的包做更新修改的时候,需要考虑它对旧版程序的兼容性,相比直接修改Hello()函数,添加一个Hellos()函数,在Hellos()函数里调用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()函数为作为参数传入的每个字符串切片元素都返回一个随机的问候消息
func Hellos(names []string) (map[string]string, error) {
// A map to associate names with messages.
messages := make(map[string]string) //定义messages是一个键和值都是string类型的map映射
// Loop through the received slice of names, calling
// the Hello function to get a message for each name.
for _, name := range names { //"_"表示在循环体里无需关注names的索引(用不到)
message, err := Hello(name) //把names切片里的元素依次传入Hello()函数
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))]
}
上面的代码有些新的内容
- 通过表达式make(map[key-type]value-type)来初始化一个map映射 更多关于map映射的内容请参考 Go maps in action
- for循环用'_'使循环体对names切片的index忽略处理,更多的内容请参考 The blank identifier
- 在hello.go中我们要修改传入的实参为一个字符串切片,然后打印语句会显示返回的是一个map映射
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)
}
- 运行hello.go打印的信息会提示我们messages返回值是一个map映射
$ go run .
map[Darrin:Hail, Darrin! Well met! Gladys:Hi, Gladys. Welcome! Samantha:Hail, Samantha! Well met!]
这一节中的内容里,相比于代码的修改,更加需要关注的是对模块兼容性的考虑,每当我们修改现有模块的时候,一定要考虑它的向后兼容性,关于这个主题可以参考 Keep your modules compatible
下一节我们介绍有关程序测试的内容
Add a test
go语言内嵌了的测试支持功能,可以方便开发者对代码进行测试,结合go test命令我们可以快速编写和执行测试。
- 在go语言里执行测试的程序文件名称以_test.go结尾,这就告诉go编译器,这个文件包含了测试的功能函数,以此为例,我们设计了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)
}
}
TestHelloName()函数和TestHelloEmpty()函数都接收了一个testing.T类型的指针作为参数,t.Fatalf()方法用来在测试出错的时候返回错误信息。
- 在*_test.go文件的目录下执行go test命令可以执行测试,命令执行成功返回PASS以及程序运行耗时情况。加上-v选项则会把*_test.go的中的每个函数的测试结果依次打印出来。如果测试有错误,就会显示t.Fatalf()方法定义的错误信息内容。
之前我们只是用go run 命令运行go代码,这种方式只能查看程序运行结果,但是并没有生成可执行程序,下一节介绍go语言编译生成可执行程序的方法。
Compile and install the application
go run 命令用来运行go语言程序查看程序执行结果,但是它并不会生成二进制可执行文件,这在我们需要频繁修改某个程序,并且想要快速观察修改效果的情况下比较常用。Go语言还有另外两个用于编译、生成和安装二进制可执行文件的命令:go build 和 go install
- 在之前教程中的hello目录下执行go build命令会在当前目录下生成hello文件,它是一个可执行的二进制程序文件。可以在命令行通过输入"./hello"来执行程序
$ ./hello
map[Darrin:Great to see you, Darrin! Gladys:Hail, Gladys! Well met! Samantha:Hail, Samantha! Well met!]
可以看到它的输出结果跟执行'go run .'结果一样。
- 那么go install命令又是用来做什么的呢?它是用来把生成的可执行程序安装到环境变量GOBIN指定的目录下,修改环境变量为GOBIN定义一个目录,或者是用go env命令定义环境变量
$ export GOBIN=/path/to/your/bin
$ go env -w GOBIN=/path/to/your/bin
$ go install
再到代码所在目录下执行go install就会在环境变量GOBIN指定的目录下生成hello可执行文件了。
总结
这一章的教程里,构建了两个模块greetings和hello,其中greetings模块实现具体的功能,hello模块导入geetings模块,调用它提供的函数完成可执行程序的构建。通过这些,我们学会了Go语言程序的基本项目结果,模块构建方法等内容。