Go-秘籍-全-
Go 秘籍(全)
原文:Go Recipes
一、Go 入门
Electronic supplementary material The online version of this chapter (doi:10.1007/978-1-4842-1188-5_1) contains supplementary material, which is available to authorized users.
Go,通常也被称为 Golang,是一种通用编程语言,由谷歌的一个团队和开源社区( http://golang.org/contributors
)的许多贡献者开发。Go 语言是由谷歌的 Robert Griesemer、Rob Pike 和 Ken Thompson 在 2007 年 9 月构想出来的。Go 于 2009 年 11 月首次出现,该语言的第一个版本于 2012 年 12 月发布。Go 是一个开源项目,它是在 BSD 风格的许可下发布的。Go 项目官方网站位于 http://golang.org/
。Go 是一种静态类型、本机编译、垃圾收集、并发编程语言,就基本语法而言,它主要属于 C 语言家族。
Go 入门
Go 编程语言可以简单地用三个词来描述:简单、最小和实用。Go 的设计目标是成为一种简单、最小化、富于表现力的编程语言,为构建可靠、高效的软件系统提供所有必要的特性。每种语言都有自己的设计目标和独特的哲学。简单性不能在语言的后期添加,所以必须在头脑中建立简单性。Go 是为简单而设计的。通过将 Go 的简单性和实用性结合起来,您可以构建具有更高生产力水平的高效软件系统。
Go 是一种静态类型的编程语言,其语法松散地来源于 C,有时被称为 21 世纪的现代 C。Go 借用了 C 的基本语法、控制流语句和基本数据类型。像 C 和 C++一样,Go 程序被编译成本机代码。Go 代码可以在多种操作系统(Linux,Windows,macOS)下编译成多种处理器(ARM,Intel)的本机代码。需要注意的是,Go 代码可以编译成 Android 和 iOS 平台。与 Java 和 C#不同,Go 不需要任何虚拟机或语言运行时来运行编译后的代码,因为它会编译成本机代码。当您为现代系统构建应用程序时,这会给您带来巨大的机会。Go 编译程序比 C 和 C++快,因此用 Go 编译更大的程序解决了用许多现有编程语言编译更大的程序时的延迟问题。尽管 Go 是一种静态类型的语言,但由于它的实用设计,它为开发人员提供了类似于动态类型语言的生产力。
在过去的十年中,计算机硬件已经发展到拥有许多 CPU 核心和更大的能力。如今,我们大量利用云平台来构建和运行应用程序,云上的服务器拥有更大的能力。尽管现代计算机和云上的虚拟机实例具有更强的能力和许多 CPU 核心,但我们仍然无法利用使用大多数现有编程语言和工具的现代计算机的能力。Go 旨在有效利用现代计算机的能力来运行高性能应用程序。Go 将并发作为一个内置特性提供,它是为编写高性能并发应用程序而设计的,允许开发人员为现代计算机构建和运行高性能、大规模可伸缩的应用程序。Go 是云计算时代语言的伟大选择。
Go 生态系统
Go 是一个生态系统,它也为编写各种软件系统提供了必要的工具和库。Go 生态系统由以下部分组成:
- Go 语言
- 去库
- 去工具化
Go 语言提供了允许你编写程序的基本语法和特性。这些程序利用库作为可重用的功能,以及用于格式化代码、编译代码、运行测试、安装程序和创建文档的工具。Go 安装附带了许多可重用的库,称为标准库包。Go 开发者社区已经建立了一个庞大的可重用库,称为第三方包。当您构建 Go 应用程序时,您可以利用 Go 本身和 Go 社区提供的包(可重用库)。您使用 Go 工具来管理您的 Go 代码。Go 工具允许你格式化、验证、测试和编译你的代码。
1-1.安装 Go 工具
问题
你想在你的开发机器上安装 Go 工具。
解决办法
Go 为 FreeBSD、Linux、macOS 和 Windows 提供二进制发行版。Go 还提供 macOS 和 Windows 的安装包。
它是如何工作的
Go 为 FreeBSD (release 8-STABLE 及更高版本)、Linux、macOS (10.7 及更高版本)、Windows 操作系统以及 32 位(386)和 64 位(amd64) x86 处理器架构的 Go 工具提供二进制发行版。如果二进制发行版不适合您的操作系统和架构组合,您可以从源代码安装它。Go 工具的二进制发行版在 https://golang.org/dl/
可用。您也可以通过从源代码构建来安装 Go 工具。如果您从源代码构建,请遵循 https://golang.org/doc/install/source
中的源代码安装说明。
图 1-1 显示了各种平台的安装包和归档源,包括 macOS、Windows 和 Linux,这些都列在 Go 网站的下载页面上( https://golang.org/dl/
)。Go 提供 macOS 和 Windows 操作系统的安装程序。
图 1-1。
Binary distributions and archived source for Go for various platforms
macOS 有一个安装包,它在/usr/local/go
安装 Go 发行版,并在您的PATH
环境变量中配置/usr/local/go/bin
目录。
在 macOS 中,也可以使用家酿( http://brew.sh/
)安装 Go。以下命令将在 macOS 上安装 Go:
brew install go
一个 MSI 安装程序可用于 Windows 操作系统,它在c:\Go
安装 Go 发行版。安装程序还会在您的PATH
环境变量中配置c:\Go\bin
目录。
图 1-2 显示了在 macOS 上运行的包安装程序。
图 1-2。
Package installer for Go running on macOS
Go 的成功安装会在 Go 工具的安装位置自动设置GOROOT
环境变量。默认情况下,这将是 macOS 下的/usr/local/go
和 Windows 下的c:\Go
。要验证 Go 工具的安装,请在命令行窗口中键入带有任何子命令的go
命令,如下所示:
go version
以下是在 macOS 中显示的结果:
go version go1.6 darwin/amd64
以下是在 Windows 系统上显示的结果:
go version go1.6 windows/amd64
以下go
命令为 Go 工具提供帮助:
go help
1-2.设置 Go 开发环境
问题
你想在你的开发机器上设置 Go 的开发环境,这样你就可以用 Go 写程序了。
解决办法
要在 Go 中编写程序,必须在开发机器上设置一个 Go 工作区。要将一个目录设置为 Go 工作区,请创建一个 Go 工作区目录来包含您的所有 Go 程序,并使用您为设置 Go 工作区而创建的目录来配置GOPATH
环境变量。
它是如何工作的
一旦你安装了 Go 工具并设置了GOPATH
环境变量指向 Go 工作空间,你就可以开始用 Go 编写程序了。GOPATH
是您将 Go 程序组织成包的目录。我们稍后将更详细地讨论包。现在,把包想象成组织 Go 程序的目录,该程序在编译后产生一个可执行程序(在 Go 网站上通常称为命令)或一个共享库。一旦在开发机器上为 Go 程序设置了工作空间目录,就必须通过设置GOPATH
环境变量将目录配置为GOPATH
。
设置 Go 工作区
Go 程序是以一种特定的方式组织的,这有助于你轻松地编译、安装和共享 Go 代码。Go 程序员把他们所有的 Go 程序保存在一个特定的目录中,这个目录叫做 Go Workspace 或 GOPATH。工作区目录在其根目录下包含以下子目录:
src
:该目录包含组织成包的源文件。pkg
:该目录包含 Go 包对象。bin
:该目录包含可执行程序(命令)。
创建一个包含三个子目录src
、pkg
和bin
的 Go 工作区目录。将所有 Go 源文件放入 Go 工作区下的src
子目录中。一个 Go 程序员将 Go 程序打包写入src
目录。Go 源文件被组织到称为包的目录中,其中单个目录将用于单个包。你用.go
扩展名编写 Go 源文件。Go 中有两种类型的包:
- 编译成可执行程序的包。
- 编译成共享库的包。
Go 工具编译 Go 源代码,并通过运行go
命令使用 Go 工具将结果二进制文件安装到Workspace
下的适当子目录中。go install
命令编译 Go 包,如果是共享库,将生成的二进制文件移入pkg
目录,如果是可执行程序,将二进制文件移入bin
目录。因此,pkg
和bin
目录用于基于包类型的包的二进制输出。
配置 GOPATH 环境变量
您在Workspace
目录中组织 Go 代码,您应该手动指定该目录,以便 Go runtime 知道工作区的位置。您可以通过设置环境变量GOPATH
来配置 Go 工作区,该变量的值为工作区的位置。
这里我们通过指定Workspace
目录的位置来配置 macOS 中的GOPATH
环境变量:
$ export GOPATH=$HOME/gocode
在前面的命令中,您通过指定GOPATH
环境变量在$HOME/gocode
配置 Go 工作空间。为了方便起见,将工作区的bin
子目录添加到您的PATH
中,以便您可以从命令行窗口中的任何位置运行可执行命令:
$ export PATH=$PATH:$GOPATH/bin
注意,在一台开发机器上可以有多个工作空间目录,但是 Go 程序员通常将他们所有的 Go 代码保存在一个工作空间目录中。
1-3.声明变量
问题
你想在 Go 中声明变量。
解决办法
关键字var
用于声明变量。除了使用var
关键字,Go 还提供了各种选项来声明变量,这些变量为语言提供了表现力,为程序员提供了生产力。
它是如何工作的
尽管 Go 借用了 C 语言家族的基本语法,但它使用不同的习惯用法来声明变量。关键字var
用于声明特定数据类型的变量。下面是声明变量的语法:
var name type = expression
声明变量时,可以省略初始化的类型或表达式,但至少应指定一个。如果变量声明中省略了该类型,则该类型由用于初始化的表达式确定。如果省略表达式,初始值对于数值类型为 0,对于布尔类型为 false,对于字符串类型为" "。清单 1-1 展示了一个使用var
关键字声明变量的程序。
package main
import "fmt"
func main() {
var fname string
var lname string
var age int
fmt.Println("First Name:", fname)
fmt.Println("Last Name:", lname)
fmt.Println("Age:", age)
}
Listing 1-1.Declare Variables Using the var Keyword
让我们使用go
工具运行程序:
go run main.go
您应该会看到以下输出:
First Name:
Last Name:
Age: 0
在这个程序中,我们通过显式指定变量的数据类型,使用var
关键字来声明变量。因为我们没有对变量进行初始化和赋值,所以它取对应类型的零值;“”代表string
型,0 代表int
型。我们可以在一条语句中声明多个相同类型的变量,如下所示:
var fname,lname string
您可以在一条语句中声明和初始化多个变量的值,如下所示:
var fname, lname string = "Shiju", "Varghese"
如果使用初始值设定项表达式来声明变量,可以使用短变量声明来省略该类型,如下所示:
fname, lname := "Shiju", "Varghese"
我们使用操作符: =
通过短变量声明来声明和初始化变量。当你用这个方法声明变量时,你不能指定类型,因为类型是由初始化表达式决定的。Go 提供了大量的生产力和表现力,就像动态类型语言和静态类型语言的特性一样。请注意,短变量声明只允许声明局部变量,即在函数中声明的变量。当在函数外部声明变量(包变量)时,必须使用var
关键字。清单 1-2 显示了一个演示函数中短变量声明和包变量声明的程序。
package main
import "fmt"
// Declare constant
const Title = "Person Details"
// Declare package variable
var Country = "USA"
func main() {
fname, lname := "Shiju", "Varghese"
age := 35
// Print constant variable
fmt.Println(Title)
// Print local variables
fmt.Println("First Name:", fname)
fmt.Println("Last Name:", lname)
fmt.Println("Age:", age)
// Print package variable
fmt.Println("Country:", Country)
}
Listing 1-2.Short Variable Declarations and Declaration of Package Variables
在这个程序中,我们在 main 函数中使用一个简短的变量声明语句来声明变量。因为短变量声明不可能用于声明包变量,所以我们使用 var 关键字来声明包变量,省略了类型,因为我们提供了初始化器表达式。我们使用关键字 const 来声明常量。
1-4.构建可执行程序
问题
你想要构建一个 Go 可执行程序来开始 Go 编程。
解决办法
Go 安装附带了标准库包,为编写 Go 程序提供了许多共享库。标准库包fmt
实现格式化的 I/O 功能,可用于打印格式化的输出消息。当你用 Go 写第一个程序的时候,一定要注意 Go 程序一定要组织成包。
它是如何工作的
您必须将 Go 源文件写入包中。在 Go 中,有两种类型的包:
- 编译成可执行程序的包。
- 编译成共享库的包。
在这个菜谱中,您将编写一个可执行程序,将输出消息打印到控制台窗口中。一个特殊的包main
用于编译成可执行程序。我们把所有的 Go 程序都写在 Go 工作区($GOPATH/src
的src
子目录下。
在$GOPATH/src
目录下创建一个名为hello
的子目录。清单 1-3 显示了一个“Hello,World”程序,演示了编写 Go 程序的基本方面。
package main
import "fmt"
func main() {
fmt.Println("Hello, World")
}
Listing 1-3.An Executable Program in main.go Under $GOPATH/src/hello
让我们通过研究这个程序来理解编写 Go 程序的基本方面。与 C 语言家族不同,在 Go 中你不需要显式地放一个分号(;)在语句的末尾。我们编写一个名为main.go
的 Go 源文件,并将它组织到包main
中。
package main
包声明指定 Go 源文件属于哪个包。这里我们指定main.go
文件是main
包的一部分。注意,一个目录(包目录)中的所有源文件都应该用相同的包名声明。对main
包的编译产生了一个可执行的程序。
import
语句用于导入包(共享库),以便您可以重用导入包的功能。这里我们导入标准库提供的包fmt
。标准库包可以在GOROOT
位置找到(转到安装目录)。
import "fmt"
我们使用func
关键字来声明函数,后跟函数名。函数main
是一个特殊函数,作为可执行程序的入口点。一个main
包必须有一个函数main
作为可执行程序的入口点。我们使用fmt
包的Println
功能打印输出数据。
func main() {
fmt.Println("Hello, World")
}
是时候构建并运行程序来查看输出了。您可以使用go
工具构建程序。在命令行窗口中导航到包目录,并运行以下命令来编译程序:
go build
build
命令编译包源代码,并生成一个可执行程序,其目录名包含包main
的 Go 源文件。因为我们使用的是名为hello
的目录,所以可执行的命令会是hello
(或者 Windows 下的hello.exe
)。在命令行窗口中从hello
目录运行命令hello
来查看输出。
您应该会看到以下输出:
Hello, World
除了使用go build
命令之外,您还可以使用go install
编译源代码,并将结果二进制文件放入GOPATH.
的bin
目录中
go install
您现在可以通过从GOPATH
的bin
目录中键入命令来运行可执行命令。如果您已经将$GOPATH/bin
添加到您的PATH
环境变量中,那么您可以从命令行窗口中的任何位置运行可执行程序。
如果你只是想编译和运行你的程序,你可以使用go run
命令后跟文件名来运行程序。
go run main.go
1-5.将包编写为共享库
问题
您希望编写的包可以被其他包重用,以共享您的 Go 代码。
解决办法
在 Go 中,您可以将一个包编写为共享库,以便它可以在其他包中重用。
它是如何工作的
Go 编程的设计理念是将小的软件组件开发成包,通过组合这些小的包来构建更大的应用。在 Go 中,代码的可重用性是通过它的包生态系统实现的。让我们构建一个小的实用程序包来演示如何在 Go 中开发一段可重用的代码。我们在本章前面的代码示例中使用了包main
,它用于构建可执行程序。这里我们想写一个共享库,与其他包共享我们的代码。
清单 1-4 显示了一个程序,该程序提供了一个带有名为strutils
的包的共享库。包strutils
提供了三个字符串实用函数。
package strutils
import (
"strings"
"unicode"
)
// Returns the string changed with uppercase.
func ToUpperCase(s string) string {
return strings.ToUpper(s)
}
// Returns the string changed with lowercase.
func ToLowerCase(s string) string {
return strings.ToLower(s)
}
// Returns the string changed to uppercase for its first letter.
func ToFirstUpper(s string) string {
if len(s) < 1 { // if the empty string
return s
}
// Trim the string
t := strings.Trim(s, " ")
// Convert all letters to lower case
t = strings.ToLower(t)
res := []rune(t)
// Convert first letter to upper case
res[0] = unicode.ToUpper(res[0])
return string(res)
}
Listing 1-4.A Shared Library for String Utility Functions
请注意,所有函数的名称都以大写字母开头。与其他编程语言不同,在 Go 中,没有任何类似于public
和private
的关键字。在 Go 中,如果名称的第一个字母是大写字母,那么所有包标识符都会被导出到其他包中。如果包标识符的名称以小写字母开头,它将不会导出到其他包,并且可访问性仅限于包内。在我们的示例程序中,我们使用了两个标准库包,strings
和unicode
,其中所有可重用函数的标识符都以大写字母开头。当你对 Go 了解更多的时候,它的简单和解决问题的方式会让你大吃一惊。
在我们的包中,我们提供了三个字符串实用函数:ToUpperCase
、ToLowerCase
和ToFirstUpper
。ToUpperCase
函数返回一个字符串参数的副本,其中所有的Unicode
字母都被映射为大写。我们使用strings
包(标准库)的ToLower
函数来改变案例。
func ToUpperCase(s string) string {
return strings.ToUpper(s)
}
ToLowerCase
函数返回一个字符串参数的副本,其中所有的Unicode
字母都被映射为小写。我们使用strings
包的ToLower
功能来改变字母大小写。
func ToLowerCase(s string) string {
return strings.ToLower(s)
}
ToFirstUpper
函数返回字符串参数的副本,其Unicode
字母的第一个字母被映射为大写。
func ToFirstUpper(s string) string {
if len(s) < 1 { // if the empty string
return s
}
// Trim the string
t := strings.Trim(s, " ")
// Convert all letters to lowercase
t = strings.ToLower(t)
res := []rune(t)
// Convert first letter to uppercase
res[0] = unicode.ToUpper(res[0])
return string(res)
}
在ToFirstUpper
函数中,我们首先将所有字母转换成小写,然后将字符串的第一个字母转换成大写。在这个函数中,我们使用了一个类型为rune
的Slice
(一个用于存储特定类型集合的数据结构)。在本书的后面,我们将更多地讨论用于保存值集合的各种数据结构。表达式string (res)
将值res
转换为类型string
。
Note
Go 语言将类型rune
定义为类型int32
的别名,以表示 Unicode 码位。Go 中的一串是一连串的符文。
组织代码路径
Go 包生态系统被设计成易于与其他包共享,它认为 Go 代码可以通过远程库共享。第三方包通过代码共享网站(如 GitHub)上的远程存储库共享。我们以一种特殊的方式组织 Go 代码,以便通过远程存储库轻松共享代码。例如,我们将本书的所有示例代码放在 GitHub 上的 https://github.com/shijuvar/go-recipes
。所以当我写代码的时候,我把源代码放到了$GOPATH/src
目录下的github.com/shijuvar/go-recipes
目录结构中。我把strutils
包的源代码写到$GOPATH/src
目录下的github.com/shijuvar/go-recipes/ch01/strutils
里。一旦我将源代码提交到它的远程存储库位置,在这个例子中是GitHub.com
,用户就可以通过提供远程存储库的位置使用go get
来访问这个包,如下所示:
go get github.com/shijuvar/go-recipes/ch01/strutils
go get
命令从远程存储库中获取源代码,并按照以下步骤安装软件包。
- 从远程存储库中获取源代码,并将源代码放入
$GOPATH/src
目录下的github.com/shijuvar/go-recipes/ch01/strutils
目录中。 - 安装软件包,将软件包对象
strutils
放入$GOPATH/pkg
目录下平台特定目录下的github.com/shijuvar/go-recipes/ch01
目录(macOS 中为darwin_amd64
目录)。
编译包
让我们构建strutils
包,这样我们就可以使它成为一个共享库,与 Go 工作区中的其他包一起使用。导航到包目录,然后运行go install
命令:
go install
install
命令编译(类似于go build
命令的动作)包源代码,然后将生成的二进制文件安装到GOPATH
的pkg
目录中。当我们从其他包中重用这个包时,我们可以从GOPATH
位置导入它。所有标准库包位于GOROOT
位置,所有定制包位于GOPATH
位置。我们把strutils package
的源码写在github.com/shijuvar/go-recipes/ch01/strutils
目录结构下的$GOPATH/src
目录下。当您运行go install
命令时,它会编译源代码,并将结果二进制文件放入$GOPATH/pkg
目录中平台特定子目录下的github.com/shijuvar/go-recipes/ch01/strutils
目录中。图 1-3 和图 1-4 显示了$GOPATH/pkg
目录中包对象strutils
的目录结构。
图 1-4。
Directory structure of package object strutils
under the go-recipes
repository
图 1-3。
Directory structure of go-recipes
repository under the platform-specific directory of the pkg
directory
我们将在本章的后面探讨更多关于包的内容。
1-6.重用共享库包
问题
您已经开发了一个共享库包。现在,您希望将共享库包与 Go 工作区中的其他包一起重用。
解决办法
您可以在包声明之后使用 Go 源文件顶部指定的import
语句导入包。然后,您可以调用包的导出函数,方法是通过包标识符访问它们,后跟点运算符(。)和要调用的导出标识符。
它是如何工作的
Go 安装将安装位于GOROOT
的pkg
目录中的标准库包。当您编写定制包时,这些包的结果二进制文件会放在GOPATH
位置的pkg
目录中。当你导入标准库的包时,你只需要指定包的短路径,因为大多数包直接位于$GOROOT/pkg
目录中。在导入fmt
包的时候,只需要引用import
块中的fmt
即可。一些标准库包如http
位于另一个根包目录下(在$GOROOT/pkg
内);对于http
来说,它是net
包目录,所以当你导入http
包时,你需要参考net/http
。从GOPATH
导入包时,必须指定包位置的完整路径,从$GOPATH/pkg
的平台特定目录后开始。让我们重用我们在清单 1-4 中开发的strutils
包,其中包的位置是github.com/shijuvar/go-recipes/ch01/strutils
。
清单 1-5 显示了一个重用strutils
包的导出函数的程序。
package main
import (
"fmt"
"github.com/shijuvar/go-recipes/ch01/strutils"
)
func main() {
str1, str2 := "Golang", "gopher"
// Convert to uppercase
fmt.Println("To Upper Case:", strutils.ToUpperCase(str1))
// Convert to lowercase
fmt.Println("To Lower Case:", strutils.ToUpperCase(str1))
// Convert first letter to uppercase
fmt.Println("To First Upper:", strutils.ToFirstUpper(str2))
}
Listing 1-5.Package main That Reuses the strutils Package
我们从位于$GOPATH/pkg
的github.com/shijuvar/go-recipes/ch01/strutils
路径导入strutils
包。在import
块中,我们通过放置一个空行来区分标准库包和定制包。没有必要这样做,但这是 Go 程序员中推荐的做法。
import (
"fmt"
"github.com/shijuvar/go-recipes/ch01/strutils"
)
我们使用包标识符strutils
来访问包的导出标识符。运行该程序时,您应该会看到以下输出:
To Upper Case: GOLANG
To Lower Case: GOLANG
To First Upper: Gopher
1-7.使用 Go 工具管理源代码
问题
您希望使用 Go 工具来管理您的 Go 源代码。
解决办法
Go 生态系统通过命令行工具提供工具支持。您可以通过运行与子命令相关的go
命令来运行 Go 工具。
它是如何工作的
Go 生态系统由 Go 语言、Go 工具和包组成。对于 Go 程序员来说,Go 工具是一个非常重要的组件。它允许您格式化、构建、安装和测试 Go 包和命令。我们在本章的前几节中使用了 Go 工具来编译、安装和运行 Go 包和命令。运行go help
命令获取关于go
命令的文档。
以下是由go
命令提供的各种子命令的文档:
Go is a tool for managing Go source code.
Usage:
go command [arguments]
The commands are:
build compile packages and dependencies
clean remove object files
doc show documentation for package or symbol
env print Go environment information
fix run go tool fix on packages
fmt run gofmt on package sources
generate generate Go files by processing source
get download and install packages and dependencies
install compile and install packages and dependencies
list list packages
run compile and run Go program
test test packages
tool run specified go tool
version print Go version
vet run go tool vet on packages
Use "go help [command]" for more information about a command.
Additional help topics:
c calling between Go and C
buildmode description of build modes
filetype file types
gopath GOPATH environment variable
environment environment variables
importpath import path syntax
packages description of package lists
testflag description of testing flags
testfunc description of testing functions
Use "go help [topic]" for more information about that topic.
如果您需要某个特定命令的帮助,运行go help
命令。让我们寻找关于install
子命令的帮助:
go help install
以下是install
命令的文档:
usage: go install [build flags] [packages]
Install compiles and installs the packages named by the import paths,
along with their dependencies.
For more about the build flags, see 'go help build'.
For more about specifying packages, see 'go help packages'.
See also: go build, go get, go clean.
格式化 Go 代码
go
命令提供了自动格式化 Go 代码的命令fmt
。go fmt
命令通过对源文件应用预定义的样式来格式化源代码,这通过正确放置花括号、制表符和空格来格式化源代码,并按字母顺序对包导入进行排序。它使用制表符(宽度= 8)缩进和空白对齐。Go 程序员通常在将他们的源代码提交到版本控制系统之前运行fmt
命令。当你从 Go 集成开发环境(ide)中保存源文件时,大部分都会自动调用fmt
命令来格式化 Go 代码。fmt
命令可用于在目录级别格式化代码或用于特定的 Go 源文件。
fmt
命令按字母顺序格式化包import
块。清单 1-6 显示了应用go fmt
之前的包import
块,这里我们列出了没有任何顺序的包。
import (
"unicode"
"log"
"strings"
)
Listing 1-6.Package import Block Before Applying go fmt
清单 1-7 显示了对清单 1-6 应用go fmt
命令后的包import
块。你可以看到go fmt
按照字母顺序格式化了import
块。
import (
"log"
"strings"
"unicode"
)
Listing 1-7.Package import Block After Applying go fmt on Listing 1-6
获取常见错误的 go 代码
go vet
命令允许您验证 Go 代码中的常见错误。vet
命令验证您的 Go 代码,如果发现任何可疑的构造,就会报告出来。编译器找不到一些常见的错误,使用go vet
也许可以识别这些错误。该命令检查源代码并报告错误,例如参数与格式字符串不一致的Printf
调用。清单 1-8 显示了一个程序,其中一个Printf
调用的参数使用了错误的格式说明符来打印浮点数。
package main
import "fmt"
func main() {
floatValue:=4.99
fmt.Printf("The value is: %d",floatValue)
}
Listing 1-8.Program That Uses the Wrong Format Specifier for Printing a Floating-Point Number
打印浮点数需要使用格式标识符%f
,但是提供了%d
,这是错误的格式标识符。当你编译这个程序的时候,你不会得到任何错误,但是当你运行程序的时候,你会得到一个错误。但是,如果您可以用go vet
验证您的代码,它会显示格式错误。让我们运行go vet
命令:
go vet main.go
Go 工具显示以下错误:
main.go:7: arg floatValue for printf verb %d of wrong type: float64
exit status 1
建议您在将 Go 代码提交到版本控制系统之前使用go vet
命令,这样可以避免一些错误。您可以在目录级别或特定的 Go 源文件上运行go vet
命令。
使用 GoDoc 获取文档
当您编写代码时,提供适当的文档是一项重要的实践,这样程序员以后可以很容易地理解代码,并且在查看他人的代码和重用第三方库时也很容易探索。Go 提供了一个名为godoc
的工具,它从 Go 程序员的 Go 代码本身为他们提供文档基础设施,这简化了开发过程,因为你不需要为文档寻找任何其他基础设施。
godoc
工具通过利用代码和注释,从 Go 代码本身生成文档。使用godoc
工具,您可以从两个地方访问文档:命令行窗口和浏览器界面。假设您想要标准库包fmt
的文档。您可以从命令行窗口运行以下命令:
godoc fmt
运行此命令会直接在命令行窗口中提供文档。您可以使用godoc
工具查看您自己定制的软件包的文档。让我们运行godoc
工具来查看我们在清单 1-4 中开发的strutils
包的文档:
godoc github.com/shijuvar/go-recipes/ch01/strutils
运行该命令会在命令行窗口中为您提供strutils
包的文档,如下所示:
PACKAGE DOCUMENTATION
package strutils
import "github.com/shijuvar/go-recipes/ch01/strutils"
Package strutils provides string utility functions
FUNCTIONS
func ToFirstUpper(s string) string
Returns the string changed to upper case for its first letter.
func ToLowerCase(s string) string
Returns the string changed with lower case.
func ToUpperCase(s string) string
Returns the string changed with upper case.
从命令行窗口查看和浏览文档会很困难。godoc
工具为 web 浏览器窗口中的文档提供了一个优雅的界面。要使用 web 浏览器界面,您需要使用godoc
工具在本地运行 web 服务器。以下命令通过监听给定端口在本地运行文档服务器:
godoc -http=:3000
运行该命令会启动一个 web 服务器。然后您可以在http://localhost:3000
.
导航文档。图 1-5 显示了文档界面的索引页面。
图 1-5。
Index page of the documentation user interface generated by the godoc
tool
由godoc
工具提供的这个用户界面与位于 https://golang.org/
的 Go 网站一模一样。通过点击包链接,您可以从GOROOT
和GOPATH
获得包的文档。当您在本地运行godoc
服务器时,它只是查看GOROOT
和GOPATH
并为驻留在这些位置的包生成文档。在 Go 代码中编写注释是一个好习惯,这样你就可以在不利用任何外部基础设施的情况下为 Go 代码生成更好的文档。
1-8.编写和重用包
问题
您想要编写和重用包。您还希望在包中提供初始化逻辑,并希望使用包别名作为包标识符。
解决办法
您编写init
函数来编写包的初始化逻辑。当您重用包时,您可以使用包标识符来访问它们的导出。如果您在import
块中导入包时能够提供别名,那么您也可以使用包别名来访问包的标识符。
它是如何工作的
Go 通过其包生态系统提供了模块化和代码可重用性,让您可以编写高度可维护和可重用的代码。编写 Go 应用程序的惯用方式是将较小的软件组件编写成包,并通过组合这些包来构建较大的应用程序。
在编写包之前,理解 Go Workspace 是很重要的。配方 1-1 涵盖了 Go 工作区,因此如果您对 Go 工作区不确定,请阅读该配方。您在 Workspace 的src
子目录中编写 Go 代码。基于 Go 编译器产生的二进制输出,你可以编写两种类型的包:可执行程序和共享库。包main
编译成可执行程序。当你写包main
的时候,你必须提供一个名为main
的函数,让它成为可执行程序的入口点。当您将包编写为共享库时,您可以选择一个名称作为包标识符。您将 Go 源文件组织到称为包的目录中。属于特定目录的所有源文件都是该包的一部分。您必须为单个目录下的所有源文件指定相同的包名。Go 程序员通常给出一个包名,这个包名与他们为这个包编写 Go 源文件的目录名相同。当您将包编写为共享库时,您必须为包指定与目录名相同的名称。当您在包目录上运行go install
时,如果它是一个包main
,那么产生的二进制文件将进入 Workspace 的bin
子目录,如果它是一个共享库包,那么将进入 Workspace 的pkg
子目录。
正在初始化包逻辑
当你写包的时候,你可能需要写一些初始化逻辑。假设您编写了一个库包,用于将数据持久化到一个数据库中,并且您希望每当这个包被其他包引用时自动建立到数据库的连接。在这种情况下,您可以编写一个名为init
的特殊函数来编写包的初始化逻辑。每当包引用其他包时,被引用包的所有init
函数都会被自动调用。你不需要显式地调用包的init
函数。当您从程序包main
中引用一个程序包时,在执行程序包main
的main
功能之前,会调用被引用程序包的init
功能。
// Initialization logic for the package
func init() {
// Initialization logic goes here
}
编写示例包
让我们编写一个示例包,作为共享库重用。在$GOPATH/sr
c 目录下的github.com/shijuvar/go-recipes/ch01/lib directory
处写源码。因为目录名是lib
,所以包名必须在包声明语句中指定为lib
。
package lib
在这个示例包中,我们将您最喜欢的项目集合的一个string
持久化到内存集合中。我们想为内存中的收藏提供一些默认的收藏项,所以我们在init
函数中编写了这个逻辑。清单 1-9 展示了lib
包的核心功能。
package lib
// Stores favorites
var favorites []string
// Initialization logic for the package
func init() {
favorites = make([]string, 3)
favorites[0] = "github.com/gorilla/mux"
favorites[1] = "github.com/codegangsta/negroni"
favorites[2] = "gopkg.in/mgo.v2"
}
// Add a favorite into the in-memory collection
func Add(favorite string) {
favorites = append(favorites, favorite)
}
// Returns all favorites
func GetAll() []string {
return favorites
}
Listing 1-9.
Favorites.go in the lib Package
Favorites.go
为lib
包提供核心功能。它允许您使用Add
函数向收藏中添加喜爱的项目,并使用GetAll
函数返回所有喜爱的项目。Add
和GetAll
函数将被导出到其他包中,因此标识符名称以大写字母开头。为了存储喜爱项目的数据,我们使用了一个名为Slice,
的集合数据结构来存储字符串集合(第二章包含了处理切片的食谱)。现在,把它想象成一个动态数组来保存收藏项的字符串值。包变量favorites
的标识符以小写字母开始,这样就不会被导出到其他包中,但是在lib
包中,可以从所有函数中访问它。使用GetAll
函数将收藏项目的数据暴露给其他包。在init
函数中,我们将一些默认的收藏项目添加到集合中。当我们将这个包导入到其他包中时,会自动调用init
函数。
现在将另一个源文件写入到lib
包中,为喜爱的项目提供实用函数。对于这个例子,只需在新的源文件utils.go
中添加一个函数,打印控制台窗口中收藏夹项目的值。清单 1-10 显示了utils.go
的来源。
package lib
import (
"fmt"
)
// Print all favorites
func PrintFavorites() {
for _, v := range favorites {
fmt.Println(v)
}
}
Listing 1-10.
utils.go in the lib Package
在PrintFavorites
函数中,我们迭代favorites
数据并打印每一项的值。在这个函数中,我们使用 Go 语言提供的特殊控制语句来迭代集合类型。range
遍历集合类型的各种数据结构中的元素,并在迭代中提供每一项的索引和值。下面是使用range
遍历集合的基本语法:
for index, value := range collection{
// code statements
}
在我们的PrintFavorites
函数中的range
语句中,我们使用每个条目值打印到控制台窗口中,但是我们不使用索引值。如果你声明了一个变量却从来没有使用过,Go 编译器会显示一个错误。我们使用空白标识符(_)
代替索引变量,以避免编译器错误。
for _, v := range favorites {
fmt.Println(v)
}
使用go install
命令构建包:
go install
从包目录运行这个命令编译源代码,并将包对象lib
放到$GOPATH/pkg
目录下的github.com/shijuvar/go-recipes/ch01
目录结构中。图 1-6 显示了lib package.
编译后的包对象
图 1-6。
Compiled package object of lib
重用包
要重用一个包,您需要导入该包。import
块用于导入包。下面的代码块显示了导入标准库包和定制包的import
块。
import (
"fmt"
"github.com/shijuvar/go-recipes/ch01/lib"
)
当您导入定制包时,您应该在$GOPATH/pkg
目录下提供包的完整路径。在$GOPATH/pkg
目录下的github.com/shijuvar/go-recipes/ch01
中有lib
包对象,所以我们导入包及其完整位置。
清单 1-11 显示了一个重用lib
包功能的程序。
package main
import (
"fmt"
"github.com/shijuvar/go-recipes/ch01/lib"
)
func main() {
// Print default favorite packages
fmt.Println("****** Default favorite packages ******\n")
lib.PrintFavorites()
// Add couple of favorites
lib.Add("github.com/dgrijalva/jwt-go")
lib.Add("github.com/onsi/ginkgo")
fmt.Println("\n****** All favorite packages ******\n")
lib.PrintFavorites()
count := len(lib.GetAll())
fmt.Printf("Total packages in the favorite list:%d", count)
}
Listing 1-11.Program Reuses the lib Package
Note
在import
块中导入包时,建议先按字母顺序导入标准库包,然后放一个空行,接着是第三方包和自己的包(自定义包)。如果您同时导入第三方软件包和您自己的软件包,请在两个软件包列表之间放置一个空行来区分它们。
运行该程序时,您应该会看到以下输出:
****** Default favorite packages ******
github.com/gorilla/mux
github.com/codegangsta/negroni
gopkg.in/mgo.v2
****** All favorite packages ******
github.com/gorilla/mux
github.com/codegangsta/negroni
gopkg.in/mgo.v2
github.com/dgrijalva/jwt-go
github.com/onsi/ginkgo
Total packages in the favorite list:5
使用包别名
在清单 1-11 中,我们导入了包lib
,并使用标识符lib
访问了包的导出标识符。如果要为包提供别名,可以这样做,并使用别名而不是其原始名称来访问包的导出标识符。下面的代码块显示了使用别名的import
语句。
import (
fav "github.com/shijuvar/go-recipes/ch01/lib"
)
在这个import
语句中,我们给lib
包起了别名fav
。下面是使用别名访问lib
包的导出标识符的代码块。
fav.PrintFavorites()
fav.Add("github.com/dgrijalva/jwt-go")
fav.Add("github.com/onsi/ginkgo")
您还可以为包使用别名,以避免包名不明确。因为包是从它们的完整路径引用的,所以可以为多个包指定相同的名称。但是,当您在一个程序中使用多个同名的包时,就会产生名称歧义。在这种情况下,您可以使用包别名来避免名称不明确。清单 1-12 显示了一个示例代码块,它导入了两个同名的包,但是它使用了一个包别名来避免名称不明确。
package main
import (
mongo "app/libs/mongodb/db"
redis "app/libs/redis/db"
)
func main() {
mongo.Connect() //calling method of package "app/libs/mongodb/db"
redis.Connect() //calling method of package "app/libs/redis/db"
}
Listing 1-12.Package Alias to Avoid Name Ambiguity
使用空白标识符作为包别名
我们讨论了被引用的包的init
函数将在程序中被自动调用。因为init
函数主要用于在包中提供初始化逻辑,你可能需要引用包来调用它们的init
函数。在某些情况下,当您不需要调用除了init
之外的任何函数时,这可能是需要的。当您导入一个包但从未使用它时,Go 编译器会显示一个错误。在这种情况下,为了避免编译错误,您可以使用空白标识符( _ )
作为包别名,这样编译器会忽略不使用包标识符的错误,但是会自动调用init
函数。
下面是使用空白标识符(_
)作为包别名以避免编译错误的代码块。
import (
_ "app/libs/mongodb/db"
)
假设包db
有一个函数init
,它只用于连接数据库和初始化数据库对象。您不希望从特定的源文件中调用包标识符,但是您希望调用数据库初始化逻辑。在这里,您可以从同一个包的其他源文件中调用包标识符。
安装第三方软件包
Go 生态系统丰富了大量的第三方包。Go 标准库提供了构建各种应用程序的基本组件。Go 开发者社区非常热衷于为众多用例构建包。当您构建真实世界的应用程序时,您可能会使用几个第三方包。要使用第三方软件包,您必须将其下载到您的GOPATH
位置。go get
命令从远程存储库中获取第三方包,并将包安装到您的GOPATH
位置。这将把包的源代码放入$GOPATH/src
,把包对象放入$GOPATH/pkg
。
以下命令下载并安装第三方包gorethink
(RethinkDB 的 Go 驱动程序)到您的GOPATH
:
go get github.com/dancannon/gorethink
一旦您将第三方包安装到您的GOPATH
位置,您可以通过导入包在您的程序中重用它们。清单 1-13 显示了一个使用第三方包gorethink
连接 RethinkDB 数据库的示例程序。我们将在本书的后面探索许多第三方包,包括gorethink
包。
package main
import (
r "github.com/dancannon/gorethink"
)
var session *r.Session
func main() {
session, err := r.Connect(r.ConnectOpts{
Address: "localhost:28015",
})
}
Listing 1-13.Using a Third-Party Package
二、Go 基础
第一章概述了 Go 编程语言和 Go 生态系统的主要组成部分。这一章包含了处理 Go 语言核心基础的方法。Go 是一种简单的编程语言,它提供了构建可伸缩软件系统的基本特性。与 C#和 Java 等其他编程语言不同,Go 在语言规范中提供了最少的特性,以保持其作为简单、最小语言的设计目标。尽管它是一种简单的语言,但 Go 提供了构建可靠而高效的软件系统所必需的语言。这一章中的方法涉及到编写函数、处理各种集合类型、错误处理以及用关键字defer
、panic
和recover
实现的 Go 的独特特性,等等。
2-1.在 Go 中编写函数
问题
如何在函数中管理 Go 代码?
解决办法
关键字func
用于声明函数。一个函数用一个名字、一个参数列表、一个可选的返回类型列表和一个编写函数逻辑的主体来声明。
它是如何工作的
Go 中的函数是一段可重用的代码,它将一系列代码语句组织成一个单元,可以从包中调用,如果函数被导出到其他包中,也可以从其他包中调用。因为函数是可重用的代码,所以可以多次调用这个表单。当您编写共享库包时,名称以大写字母开头的函数将被导出到其他包中。如果函数名以小写字母开头,它不会被导出到其他包中,但是您可以在同一个包中调用这个函数。
声明函数
下面是在 Go 中编写函数的语法:
func name(list of parameters) (list of return types)
{
function body
}
函数参数指定名称和类型。当调用者调用一个函数时,它提供函数参数的实参。在 Go 中,一个函数可以返回多个值。返回类型列表指定了函数返回值的类型。您在函数体中编写代码语句。清单 2-1 显示了一个将两个整数值相加的示例函数。
func Add(x, y int) int {
return x + y
}
Listing 2-1.An Example Function That Adds Two Integer Values
声明了一个函数Add
,它有两个类型为integer
的参数,该函数返回一个整数值。使用return
语句提供函数的返回值。
清单 2-2 显示了调用这个Add
函数的代码块。
x, y := 20, 10
result := Add(x, y)
Listing 2-2.Code Block That Calls the Add Function
两个整数变量x
和y
被初始化,为调用Add
函数提供参数。局部变量result
用Add
函数返回的返回值初始化。
清单 2-3 展示了一个示例程序,它声明了两个函数并从一个main
函数中调用它。
package main
import (
"fmt"
)
func Add(x, y int) int {
return x + y
}
func Subtract(x, y int) int {
return x - y
}
func main() {
x, y := 20, 10
result := Add(x, y)
fmt.Println("[Add]:", result)
result = Subtract(x, y)
fmt.Println("[Subtract]:", result)
}
Listing 2-3.Example Program That Defines and Calls Functions
在这个程序中,声明了两个函数:Add
和Subtract
。这两个函数是从main
函数中调用的。
运行该程序时,您应该会看到以下输出:
[Add]: 30
[Subtract]: 10
命名返回值
编写函数时,可以通过在函数顶部定义变量来命名返回值。清单 2-4 显示了带有指定返回值的Add
函数。
func Add(x, y int) (result int) {
result = x + y
return
}
Listing 2-4.
Add Function with Named Return Values
integer
类型的变量result
在函数声明中为函数返回值指定。当您指定指定的返回值时,您可以将返回值赋给指定的变量,并且可以通过简单地指定return
关键字来退出函数,而不需要随return
语句一起提供返回值。
result = x + y
return
这个return
语句返回在函数声明中指定的命名返回值。这就是所谓的裸归。我不推荐这种方法,因为它会影响程序的可读性。
返回多个值
Go 是一种在其语言设计中提供了很多实用主义的语言。在 Go 中,可以从一个函数返回多个值,这在很多实际场景中是一个很有用的特性。
清单 2-5 展示了一个示例程序,它声明了一个具有两个返回值的函数,并从一个main
函数中调用它。
package main
import (
"fmt"
)
func Swap(x, y string) (string, string) {
return y, x
}
func main() {
x, y := "Shiju", "Varghese"
fmt.Println("Before Swap:", x, y)
x, y = Swap(x, y)
fmt.Println("After Swap:", x, y)
}
Listing 2-5.An Example Program That Uses a Function with Multiple Return Values
名为Swap
的函数是用两个string
类型的返回值声明的。Swap
函数交换两个字符串值。我们从main
函数中调用Swap
函数。
运行该程序时,您应该会看到以下输出:
Before Swap: Shiju Varghese
After Swap: Varghese Shiju
可变函数
可变函数是接受可变数量参数的函数。当您不知道要传递给函数的参数数量时,这种类型的函数非常有用。fmt
包的内置Println
函数是可变函数的一个例子,它可以接受可变数量的参数。
清单 2-6 显示了一个提供变量函数Sum
的示例程序,它接受数量可变的integer
类型的参数。
package main
import (
"fmt"
)
func Sum(nums ...int) int {
total := 0
for _, num := range nums {
total += num
}
return total
}
func main() {
// Providing four arguments
total := Sum(1, 2, 3, 4)
fmt.Println("The Sum is:", total)
// Providing three arguments
total = Sum(5, 7, 8)
fmt.Println("The Sum is:", total)
}
Listing 2-6.Example Program with Variadic Function
表达式. . .
用于指定参数表的可变长度。当调用者向nums
参数提供值时,它可以提供可变数量的整数值参数。Sum
函数提供了调用者提供的可变数量的参数的总和。该函数使用range
构造迭代nums
参数的值,以获得调用者提供的参数的总值。在main
函数中,Sum
函数被调用两次。每次都提供可变数量的参数。
运行该程序时,您应该会看到以下输出:
The Sum is: 10
The Sum is: 20
当调用变量函数时,可以提供切片(动态数组)作为参数。你将在本章的后面学习切片。清单 2-7 显示了通过提供一个切片作为参数来调用变量函数的代码块。
// Providing a slice as an argument
nums := []int{1, 2, 3, 4, 5}
total = Sum(nums...)
fmt.Println("The Sum is:", total)
Listing 2-7.Code Block That Calls a Variadic Function with a Slice
当您提供切片作为参数时,您必须在切片值后提供表达式...
。
函数值、匿名函数和闭包
尽管 Go 是一种静态类型的语言,但 Go 的实用主义给开发人员带来了像动态类型语言一样的生产力。Go 中的函数为 Go 程序员提供了很大的灵活性。函数类似于值,这意味着您可以将函数值作为参数传递给其他返回值的函数。Go 还提供了对匿名函数和闭包的支持。匿名函数是没有函数名的函数定义。当您希望在不提供函数标识符的情况下内联形成函数时,这很有用。
清单 2-8 显示了一个示例程序,它演示了将一个匿名函数作为参数传递给另一个函数,其中匿名函数封闭变量以形成闭包。
package main
import (
"fmt"
)
func SplitValues(f func(sum int) (int, int)) {
x, y := f(35)
fmt.Println(x, y)
x, y = f(50)
fmt.Println(x, y)
}
func main() {
a, b := 5, 8
fn := func(sum int) (int, int) {
x := sum * a / b
y := sum - x
return x, y
}
// Passing function value as an argument to another function
SplitValues(fn)
// Calling the function value by providing argument
x, y := fn(20)
fmt.Println(x, y)
}
Listing 2-8.Example Program Demonstrating Passing Function as Value, Anonymous Function, and Closure
在main
函数中,声明了一个匿名函数,并将匿名函数的值赋给一个名为fn
的变量。
a, b := 5, 8
fn := func(sum int) (int, int) {
x := sum * a / b
y := sum - x
return x, y
}
匿名函数在main
函数中声明。在 Go 中,可以在函数内部编写函数。匿名函数使用任意逻辑将一个值拆分成两个值。为了形成任意逻辑,它访问在main
函数的外部函数中声明的几个变量的值。
匿名函数被赋给变量fn and
,将函数值传递给另一个名为SplitValues
的函数。
SplitValues(fn)
SplitValues
函数接收一个函数作为参数。
func SplitValues(f func(sum int) (int, int)) {
x, y := f(35)
fmt.Println(x, y)
x, y = f(50)
fmt.Println(x, y)
}
在SplitValues
函数中,作为参数传递的参数值被调用几次,以将值分成两个值。返回值被打印到控制台窗口。
让我们回到匿名函数。在main
函数中,匿名函数的值用于两件事:通过将函数值作为参数传递来调用SplitValues
函数,以及通过提供一个值作为参数来拆分整数值来直接调用函数值。
// Passing function value as an argument to another function
SplitValues(fn)
// Calling the function value by providing argument
x, y := fn(20)
fmt.Println(x, y)
值得注意的是,匿名函数正在访问外部函数中声明的两个变量:
a, b := 5, 8.
变量a
和b
在main
函数中声明,但是匿名函数(内部函数)可以访问这些变量。当您通过将匿名函数的值作为参数传递来调用SplitValues
函数时,匿名函数也可以访问变量a
和b
。匿名函数关闭a
和b
的值,使其成为闭包。不管从哪里调用匿名函数的值,它都可以访问外部函数中声明的变量a
和b
。
运行上述程序时,您应该会看到以下输出:
21 14
31 19
12 8
2-2.使用数组
问题
您希望将元素集合存储到固定长度的数组类型中。
解决办法
Go 的数组类型允许您存储单一类型的固定大小的元素集合。
它是如何工作的
数组是由单一类型的元素集合组成的数据结构。数组是固定大小的数据结构,通过指定长度和元素类型来声明。
声明和初始化数组
下面是声明数组的代码块:
var x [5]int
变量x
被声明为由五个int
类型的元素组成的数组。数组x
允许你存储integer values.
的五个元素,你通过指定从 0 开始的索引来赋值给一个数组。下面是为数组x
的第一个元素赋值的表达式:
x[0]=5
表达式x[4]=25
为数组x
的最后一个元素(第五个元素)赋值。
您还可以使用数组文字来声明和初始化数组,如下所示:
y := [5]int {5,10,15,20,25}
当使用数组文字初始化数组时,可以为特定元素提供值,如下所示:
langs := [4]string{0: "Go", 3: "Julia"}
一个string
类型的数组被声明为大小为 4,但是只为第一个元素(索引 0)和最后一个元素(索引 3)提供值。您将获得没有初始化的元素的默认值。对于字符串类型,它是空字符串;对于整数类型,它是 0;对于布尔类型,它是 false。如果你试图返回langs[1]
的值,你将得到一个空字符串。您可以像往常一样随时为其余元素提供值:
langs[1] = "Rust"
langs[2] = "Scala"
当使用数组文字声明和初始化数组时,可以在多行语句中提供初始化表达式,如下所示:
y := [5]int {
5,
10,
15,
20,
25,
}
在多行语句中初始化数组元素时,必须在所有元素后提供逗号,包括最后一个元素。当您修改代码时,这使可用性成为可能。因为每个元素后面都有一个逗号,所以您可以轻松地删除或注释元素初始化,或者在任何位置添加新元素,包括最后一个位置。
当您声明数组时,您总是指定数组的长度,但是当您声明和初始化数组时,您可以使用表达式…
来代替指定长度,如下所示:
z := [...] { 5,10,15,20,25}
这里,数组的长度由初始化表达式中提供的元素数量决定。
遍历数组
因为数组是一种集合类型,所以您可能希望迭代数组的元素。下面是使用普通的for
循环迭代数组元素的代码块:
langs := [4]string{"Go", "Rust", "Scala","Julia"}
for i := 0; i < len(langs); i++ {
fmt.Printf("langs[%d]:%s \n", i, langs[i])
}
在这里,我们迭代langs
数组的元素,并通过指定索引值简单地打印每个元素的值。len
函数获取集合类型的值的长度。
Note
Go 语言只有一个循环结构,那就是for
循环。与许多其他语言不同,Go 不支持while
循环结构。如果你想要一个类似于while
的循环结构,你可以使用for
循环(例如for i< 1000{}
)。
Go 有一个range
构造,可以让您迭代各种集合类型中的元素。Go 程序员通常使用range
构造来迭代数据结构的元素,比如数组、切片和映射。下面是迭代数组元素的代码块:
for k, v := range langs {
fmt.Printf("langs[%d]:%s \n", k, v)
}
数组上的range
构造为集合中的每个元素提供了索引和值。在我们的示例代码块中,变量k
获取索引,变量v
获取元素的值。如果您不想使用您在左侧声明的任何变量的值,您可以通过使用空白标识符(_
)来忽略它,如下所示:
for _, v := range langs {
fmt.Printf(v)
}
在这个range
块中,使用了元素的值,但没有使用索引,因此使用一个空白标识符(_
)来代替索引变量,以避免编译错误。如果一个变量被声明但从未被使用过,Go 编译器会显示一个错误。
示例程序
清单 2-9 显示了探索数组类型的示例程序。
package main
import (
"fmt"
)
func main() {
// Declare arrays
var x [5]int
// Assign values at specific index
x[0] = 5
x[4] = 25
fmt.Println("Value of x:", x)
x[1] = 10
x[2] = 15
x[3] = 20
fmt.Println("Value of x:", x)
// Declare and initialize array with array literal
y := [5]int{10, 20, 30, 40, 50}
fmt.Println("Value of y:", y)
// Array literal with ...
z := [...]int{10, 20, 30, 40, 50}
fmt.Println("Value of z:", z)
fmt.Println("Length of z:", len(z))
// Initialize values at specific index with array literal
langs := [4]string{0: "Go", 3: "Julia"}
fmt.Println("Value of langs:", langs)
// Assign values to remaining positions
langs[1] = "Rust"
langs[2] = "Scala"
// Iterate over the elements of array
fmt.Println("Value of langs:", langs)
fmt.Println("\nIterate over arrays\n")
for i := 0; i < len(langs); i++ {
fmt.Printf("langs[%d]:%s \n", i, langs[i])
}
fmt.Println("\n")
// Iterate over the elements of array using range
for k, v := range langs {
fmt.Printf("langs[%d]:%s \n", k, v)
}
}
Listing 2-9.Example Program on Arrays
运行该程序时,您应该会看到以下输出:
Value of x: [5 0 0 0 25]
Value of x: [5 10 15 20 25]
Value of y: [10 20 30 40 50]
Value of z: [10 20 30 40 50]
Length of z: 5
Value of langs: [Go Julia]
Value of langs: [Go Rust Scala Julia]
Iterate over arrays
langs[0]:Go
langs[1]:Rust
langs[2]:Scala
langs[3]:Julia
langs[0]:Go
langs[1]:Rust
langs[2]:Scala
langs[3]:Julia
2-3.使用切片处理动态数组
问题
您希望将数据集合存储到动态数组中,因为您在声明数组时不知道它的大小。
解决办法
Go 的切片类型允许您存储单一类型元素的动态长度。
它是如何工作的
当您声明用于存储元素集合的数据结构时,您可能不知道它的大小。例如,假设您想从数据库表或 NoSQL 集合中查询数据,并将数据放入一个变量中。在这种情况下,您不能通过提供大小来声明数组,因为数组的大小会根据数据库表中包含的数据随时变化。切片是建立在 Go 的数组类型之上的数据结构,它允许您存储单一类型元素的动态长度。在您的 Go 应用程序中,数组的使用可能是有限的,您可能会经常使用切片,因为它们提供了一个灵活的、可扩展的数据结构。
切片数据结构具有长度和容量。长度是切片引用的元素数量。容量是切片中分配有空间的元素数量。切片长度不能超过容量值,因为这是可以达到的最大值长度。切片的长度和容量可以分别通过使用 len 和 cap 函数来确定。由于存储片的动态特性,当存储片增长时,存储片的长度和容量可以随时变化。
声明零切片
声明一个slice
类似于声明一个数组,但是当声明切片时,不需要指定大小,因为它是一个动态数组。下面是声明一个nil
片的代码块:
var x []int
切片x
被声明为整数的nil
切片。此时,切片的长度和容量为零。虽然x
的长度现在为零,但是您可以在以后修改长度并初始化值,因为片是动态数组。Go 提供了一个函数append
,该函数可用于在以后放大任何片(nil 或非 nil)。
使用 make 函数初始化切片
在赋值之前,必须初始化切片。在前面的声明中,片x
被声明,但是它没有被初始化,所以如果你试图给它赋值,这将导致运行时错误。Go 内置的make
函数用于初始化切片。当使用make
函数声明切片时,length
和capacity
作为参数提供。
下面是使用指定了length
和capacity
的make
函数创建切片的代码块:
y:= make ([]int, 3,5)
使用make
函数,用为 3 的length
和为 5 的capacity
声明并初始化一个片y
。当make
函数的参数中省略了capacity
参数时,capacity
的值默认为length
的指定值。
y:= make ([]int, 3)
用 3 的length
和 3 的capacity
声明并初始化片 y。因为没有提供capacity
的值,所以默认为length
的值。
可以像数组一样给片y
赋值:
y[0] = 10
y[1] = 20
y[2] = 30
使用切片文字创建切片
除了使用make
函数创建切片之外,还可以使用切片文字创建切片,这类似于数组文字。下面是使用切片文字创建切片的代码块:
z:= []int {10,20,30}
用为 3 的length
和为 3 的capacity
声明并初始化切片z
。初始化这些值时,可以为特定的索引提供值,如下所示:
z:= []int {0:10, 2:30}
创建一个切片z
,并用 3 的length
和 3 的capacity
进行初始化。当您使用这种方法创建切片时,length
由您指定的最高索引值决定,因此您也可以通过简单地提供最高索引来创建切片,如下所示:
z:= []int {2:0}
通过初始化索引 2 的零值来创建切片z
,因此该切片的capacity
和length
将是 3。
通过使用切片文字,您还可以创建一个空切片:
z:= []int{}
切片z
是用零个值元素创建的。当您希望从函数中返回空集合时,空切片非常有用。假设您提供了一个从数据库表中查询数据的函数,并通过填充表中的数据返回一个切片。如果表格不包含任何数据,您可以在这里返回一个空的切片。请注意,零切片和空切片是不同的。如果z
是一个空片,代码表达式z == nil
返回false
,如果是一个零片,表达式z == nil
返回true
。
使用复制和附加功能放大切片
因为切片是动态数组,所以可以随时放大它们。当您想要增加切片的capacity
时,一种方法是创建一个新的更大的切片,并将原始切片的元素复制到新创建的切片中。Go 内置的copy
函数用于将数据从一个片复制到另一个片。清单 2-10 显示了一个使用copy
函数增加切片大小的示例程序。
package main
import (
"fmt"
)
func main() {
x := []int{10, 20, 30}
fmt.Printf("[Slice:x] Length is %d Capacity is %d\n", len(x), cap(x))
// Create a bigger slice
y := make([]int, 5, 10)
copy(y, x)
fmt.Printf("[Slice:y] Length is %d Capacity is %d\n", len(y), cap(y))
fmt.Println("Slice y after copying:", y)
y[3] = 40
y[4] = 50
fmt.Println("Slice y after adding elements:", y)
}
Listing 2-10.Program to Enlarge a Slice Using the copy Function
运行该程序时,您应该会看到以下输出:
[Slice:x] Length is 3 Capacity is 3
[Slice:y] Length is 5 Capacity is 10
Slice y after copying: [10 20 30 0 0]
Slice y after adding elements: [10 20 30 40 50]
创建一个切片x
,其length
为 3,capacity
为 3。为了增加capacity
并向切片添加更多元素,创建了一个新的切片y
,其length
为 5,capacity
为 10。然后,copy
函数将数据从片x
复制到目标片y
。
您还可以通过使用 Go 内置的append
函数将数据追加到现有切片的末尾来放大切片。如有必要,append
功能会自动增加slice
的大小,并返回更新后的slice
和新添加的数据。清单 2-11 显示了一个使用append
函数增加切片的示例程序。
package main
import (
"fmt"
)
func main() {
x := make([]int, 2, 5)
x[0] = 10
x[1] = 20recipes for arrays
fmt.Println("Slice x:", x)
fmt.Printf("Length is %d Capacity is %d\n", len(x), cap(x))
// Create a bigger slice
x = append(x, 30, 40, 50)
fmt.Println("Slice x after appending data:", x)
fmt.Printf("Length is %d Capacity is %d\n", len(x), cap(x))
x = append(x, 60, 70, 80)
fmt.Println("Slice x after appending data for the second time:", x)
fmt.Printf("Length is %d Capacity is %d\n", len(x), cap(x))
}
Listing 2-11.Program That Enlarges a Slice Using the append Function
运行该程序时,您应该会看到以下输出:
Slice x: [10 20]
Length is 2 Capacity is 5
Slice x after appending data: [10 20 30 40 50]
Length is 5 Capacity is 5
Slice x after appending data for the second time: [10 20 30 40 50 60 70 80]
Length is 8 Capacity is 10
创建一个切片x
,其中length
为 2,capacity
为 5。然后,三个以上的数据元素被附加到slice
。这次length
和capacity
都是 5。然后,将另外三个数据元素追加到切片中。这次你试图将切片的length
增加到 8,但是切片的capacity
是 5。如有必要,append
功能可以自动增大capacity
。这里增加到 10。
您可以将数据附加到一个 nil 片上,在那里它会分配一个新的底层数组,如清单 2-12 所示。
package main
import "fmt"
func main() {
// Declare a nil slice
var x []int
fmt.Println(x, len(x), cap(x))
x = append(x, 10, 20, 30)
fmt.Println("Slice x after appending data:", x)
}
Listing 2-12.Appending Data to a Nil Slice
运行该程序时,您应该会看到以下输出:
[] 0 0
Slice x after appending data: [10 20 30]
遍历切片
迭代切片元素的惯用方法是使用range
构造。清单 2-13 展示了一个迭代切片元素的示例程序。
package main
import (
"fmt"
)
func main() {
x := []int{10, 20, 30, 40, 50}
for k, v := range x {
fmt.Printf("x[%d]: %d\n", k, v)
}
}
Listing 2-13.Program to Iterate Over the Elements of a Slice
运行该程序时,您应该会看到以下输出:
x[0]: 10
x[1]: 20
x[2]: 30
x[3]: 40
x[4]: 50
片上的range
构造为集合中的每个元素提供了索引和值。在我们的示例程序中,变量k
获取索引,变量v
获取数据元素的值。
2-4.使用映射持久化键/值对
问题
您希望将键/值对的集合保存到类似于哈希表的集合类型中。
解决办法
Go 的 map 类型允许您将键/值对的集合存储到类似于散列表的结构中。
它是如何工作的
Go 的 map 类型是一种数据结构,它提供了哈希表的实现(在 Java 中称为 HashMap)。哈希表实现允许您将数据元素作为键和值来保存。哈希表提供了对数据元素的快速查找,因为您可以通过提供键来轻松地检索值。
声明和初始化地图
以下是地图类型的定义:
map[KeyType]ValueType
这里KeyType
是键的类型,ValueType
是值的类型。下面是声明地图的代码块:
var chapts map[int]string
用int
作为键的类型和string
作为值的类型来声明映射chapts
。此时,映射图chapts
的值是nil
,因为映射图没有初始化。试图将值写入nil
映射将导致运行时错误。在向映射写入值之前,需要初始化映射。内置的make
函数用于初始化地图,如下图所示:
chapts = make(map[int] string)
使用make
函数初始化地图chapts
。让我们向地图添加一些数据值:
chapts[1]="Beginning Go"
chapts[2]="Go Fundamentals"
chapts[3]="Structs and Interfaces"
需要注意的是,不能向映射中添加重复的键。
您还可以使用映射文字来声明和初始化映射,如下所示:
langs := map[string]string{
"EL": "Greek",
"EN": "English",
"ES": "Spanish",
"FR": "French",
"HI": "Hindi",
}
映射langs
是用string
作为键和值的类型来声明的,值是使用映射文字来初始化的。
使用地图
映射提供了对数据结构中数据元素的快速查找。通过提供如下所示的键,您可以轻松地检索元素的值:
lan, ok := langs["EN"]
通过提供一个键在 map 上执行的查找返回两个值:元素的值和一个指示查找是否成功的布尔值。变量lan
获取键"EN"
的元素值,变量ok
获取布尔值:true
如果键"EN"
有值,而false
如果键不存在。Go 为编写可用于编写查找语句的if
语句提供了方便的语法:
if lan, ok := langs["EN"]; ok {
fmt.Println(lan)
}
当把一个if
语句写成单行上的多个语句时,语句之间用分号(;)并且最后一个表达式应该有一个布尔值。
要从地图中移除项目,请通过提供键来使用内置函数delete
。delete
函数从 map 中删除给定键的元素,并且不返回任何内容。下面的代码块为键"EL"
从langs
映射中删除了一个元素。
delete(langs,"EL")
这将删除键"EL"
的一个元素。如果指定的键不存在,它不会做任何事情。
像其他集合类型一样,range
构造通常用于迭代 map 的元素。清单 2-14 展示了一个演示地图上各种操作的示例程序。
package main
import (
"fmt"
)
func main() {
// Declares a nil map
var chapts map[int]string
// Initialize map with make function
chapts = make(map[int]string)
// Add data as key/value pairs
chapts[1] = "Beginning Go"
chapts[2] = "Go Fundamentals"
chapts[3] = "Structs and Interfaces"
// Iterate over the elements of map using range
for k, v := range chapts {
fmt.Printf("Key: %d Value: %s\n", k, v)
}
// Declare and initialize map using map literal
langs := map[string]string{
"EL": "Greek",
"EN": "English",
"ES": "Spanish",
"FR": "French",
"HI": "Hindi",
}
// Delete an element
delete(langs, "EL")
// Lookout an element with key
if lan, ok := langs["EL"]; ok {
fmt.Println(lan)
} else {
fmt.Println("\nKey doesn't exist")
}
}
Listing 2-14.Various operations on maps
您应该会看到类似如下的输出:
Key: 3 Value: Structs and Interfaces
Key: 1 Value: Beginning Go
Key: 2 Value: Go Fundamentals
Key doesn't exist
地图的迭代顺序
当您使用range
构造对地图进行迭代时,迭代顺序并未指定,因此不能保证一次迭代得到相同的结果,因为 Go 会随机化地图迭代顺序。如果您想要以特定的顺序迭代地图,您必须维护一个数据结构来指定该顺序。清单 2-15 显示了一个示例程序,它遍历一个带有顺序的地图。为了指定顺序,这个例子维护了一个片来存储映射的排序键。
package main
import (
"fmt"
"sort"
)
func main() {
// Initialize map with make function
chapts := make(map[int]string)
// Add data as key/value pairs
chapts[1] = "Beginning Go"
chapts[2] = "Go Fundamentals"
chapts[3] = "Structs and Interfaces"
// Slice for specifying the order of the map
var keys []int
// Appending keys of the map
for k := range chapts {
keys = append(keys, k)
}
// Ints sorts a slice of ints in increasing order.
sort.Ints(keys)
// Iterate over the map with an order
for _, k := range keys {
fmt.Println("Key:", k, "Value:", chapts[k])
}
}
Listing 2-15.Iterate over a Map With an Order
您应该会看到以下输出:
Key: 1 Value: Structs and Interfaces
Key: 2 Value: Go Fundamentals
Key: 3 Value: Beginning Go
因为您指定了顺序,所以所有迭代的输出顺序都是相同的。
2-5.在函数中编写清理代码
问题
您希望在函数中编写清理逻辑,以便在周围的函数返回后执行清理操作。
解决办法
Go 提供了一个defer
语句,允许你在函数中编写清理逻辑。
它是如何工作的
函数中的defer
语句将函数调用或 case 语句推送到保存的调用列表中。您可以在一个函数中添加多个defer
语句。这些来自保存列表的延迟函数调用在周围函数返回后执行。defer
语句通常用于在函数内部编写清理逻辑,以释放您在其中创建的资源。例如,假设您在一个函数中打开了一个数据库连接对象,您可以在函数返回后安排关闭该连接对象以清理该连接对象的资源。defer
语句通常用于close
、disconnect
和unlock
语句,与open
、connect
或lock
语句相对。defer
语句确保函数调用的延迟列表在所有情况下都被调用,即使发生异常也是如此。
列表 2-16 显示了一个代码块,该代码块使用defer
语句来关闭一个为读取而打开的文件对象。
import (
"io/ioutil"
"os"
)
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
return ioutil.ReadAll(f)
}
Listing 2-16.
Defer Statement Used to Close a File Object
我们打开一个文件对象f
来读取它的内容。为了确保对象f
正在释放它的资源,我们将代码语句f.Close()
添加到函数调用的延迟列表中。释放资源的defer
语句通常是在资源被创建且没有任何错误之后编写的。我们把defer f.Close()
写在对象f has been successfully created.
之后
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
使用defer
编写清理逻辑类似于在 C#和 Java 等其他编程语言中使用finally
块。在try/catch/finally
块中,您在finally
块中为已经在try
块中创建的资源编写清理逻辑。Go 的defer
比传统编程语言的finally
块更强大。例如,结合defer
和recover
语句,您可以从一个混乱的函数中重新获得控制。我们将在本章的下一节介绍panic
和recover
。
2-6.使用 Panic 停止控制的执行流
问题
当您的程序出现严重错误时,您希望停止函数中的执行控制流,并开始恐慌。
解决办法
Go 提供了一个内置的panic
函数,停止一个程序的正常执行,开始死机。
它是如何工作的
当 Go 运行时在执行过程中检测到任何未处理的错误时,它会死机并停止执行。因此,所有运行时错误都会导致程序崩溃。通过显式调用内置的panic
函数,可以创建同样的情况;它停止正常执行并开始死机。在继续执行几乎不可能的情况下,通常会调用panic
函数。例如,如果您试图连接到一个数据库,但是无法连接,那么继续执行程序就没有任何意义,因为您的应用程序依赖于数据库。在这里你可以调用panic
函数来停止正常执行并使你的程序死机。panic
函数接受任何类型的值作为参数。当函数内部发生异常时,它会停止函数的正常执行,执行该函数中所有延迟的函数调用,然后调用方函数会得到一个异常函数。在停止执行之前,执行所有的延迟函数是很重要的。Go 运行时确保在所有情况下都执行 defer 语句,包括紧急情况。
清单 2-17 显示了当试图打开一个文件导致错误时调用panic
的代码块;它通过提供一个错误对象作为参数来调用panic
。
import (
"io/ioutil"
"os"
)
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
panic (err) // calls panic
}
defer f.Close()
return ioutil.ReadAll(f)
}
Listing 2-17.Using panic to Panic a Function
函数ReadFile
试图打开一个文件来读取其内容。如果Open
函数出错,就会调用panic
函数来启动一个应急函数。当你编写真实世界的应用程序时,你很少会调用panic
函数;您的目标应该是处理所有错误以避免出现恐慌情况,记录错误消息,并向最终用户显示正确的错误消息。
2-7.使用 Recover 恢复死机功能
问题
你想重新获得对恐慌功能的控制。
解决办法
Go 提供了一个内置的recover
函数,让你重新获得对一个死机函数的控制;因此,它仅用于延迟函数。在延迟函数中使用了recover
函数,以恢复死机函数的正常执行。
它是如何工作的
当函数死机时,该函数中所有延迟的函数调用都会在正常执行停止之前执行。在这里,对延迟函数中的recover
的调用获得了赋予panic
的值,并重新获得了对正常执行的控制。简而言之,即使在紧急情况下,您也可以使用recover
恢复正常执行。
清单 2-18 展示了一个使用recover
进行紧急恢复的例子。
package main
import (
"fmt"
)
func panicRecover() {
defer fmt.Println("Deferred call - 1")
defer func() {
fmt.Println("Deferred call - 2")
if e := recover(); e != nil {
// e is the value passed to panic()
fmt.Println("Recover with: ", e)
}
}()
panic("Just panicking for the sake of example")
fmt.Println("This will never be called")
}
func main() {
fmt.Println("Starting to panic")
panicRecover()
fmt.Println("Program regains control after the panic recovery")
}
Listing 2-18.Example that demonstrates recover
这个示例程序演示了如何使用recover
函数恢复一个死机函数的正常执行。在函数panicRecover
中,增加了两个延迟函数。在这两个延迟的函数调用中,第二个是匿名函数,在这个函数中,调用recover
来恢复执行,即使在出现紧急情况之后。理解您可以在函数中添加任意数量的延迟函数调用是很重要的。延迟函数的执行顺序是最后添加的,按顺序是第一个。例如,panic
通过提供一个字符串值作为参数来显式调用。这个值可以通过调用recover
函数来检索。当调用panic
函数时,控制流向延迟函数,其中从第二个延迟函数调用recover
函数(当执行延迟函数调用时,这将首先被调用)。当调用recover
时,它接收给panic
的值并恢复正常执行,程序正常运行。
运行该程序时,您应该会看到以下输出:
Starting to panic
Deferred call - 2
Recover with: Just panicking for the sake of example
Deferred call - 1
Program regains control after the panic recovery
该结果还说明了延迟函数的执行顺序。最后添加的延迟函数在第一次延迟函数调用之前执行。
2-8.执行错误处理
问题
您希望在 Go 应用程序中执行错误处理。
解决办法
Go 提供了一个内置的error
类型,用于通知函数中的错误。Go 函数可以返回多个值。这可以通过返回一个error
值和其他返回值来实现函数中的异常处理,因此调用函数可以检查函数是否提供了一个错误值。
它是如何工作的
与许多其他编程语言不同,Go 不提供try/catch
块来处理异常。取而代之,您可以使用内置的error
类型向调用者函数发出异常信号。如果你能研究一下标准库包的功能,你会对如何处理 Go 中的异常有更好的理解。标准库包的大多数函数返回多个值,包括一个error
值。在函数中返回一个error
值的惯用方法是在return
语句中提供的其他值之后提供error
值。因此,在return
语句中,error
值将是最后一个参数。在清单 2-14 中,您调用了标准库包os
的Open
函数来打开一个文件对象。
f, err := os.Open(filename)
if err != nil {
return nil, err
}
Open
函数返回两个值:一个文件对象和一个error
值。检查返回的error
值,以确定打开文件时是否出现任何异常。如果error
值返回一个非空值,这意味着发生了一个错误。
下面是os
包中Open
函数的源代码:
// Open opens the named file for reading. If successful, methods on
// the returned file can be used for reading; the associated file
// descriptor has mode O_RDONLY.
// If there is an error, it will be of type *PathError.
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}
正如标准库包通过返回一个error
值来使用异常处理一样,您可以在 Go 代码中采用相同的方法。清单 2-19 显示了一个返回error
值的示例函数。
func Login(user User) (User, error) {
var u User
err = C.Find(bson.M{"email": user.Email}).One(u)
if err != nil {
return nil, err
}
err = bcrypt.CompareHashAndPassword(u.HashPassword, []byte(user.Password))
if err != nil {
return nil, err
}
return u, nil
}
Listing 2-19.Example Function That Provides error Value
Login
函数返回两个值,包括一个error
值。下面是调用Login
函数并验证该函数是否返回任何非空值error
的代码块:
if user, err := repo.Login(loginUser); err != nil {
fmt.Println(err)
}
// Implementation here if error is nil
在这个代码块中,调用者函数检查返回的error
值;如果error
值返回一个非空值,则表明该函数返回一个错误。如果返回的error
值为nil,
,则表明函数调用成功,没有任何错误。当fmt.Println
函数获得一个error
值作为参数时,它通过调用其Error() string
方法格式化error
值。error
值的Error
方法返回字符串形式的错误信息。调用函数可以与Error
方法一起使用,以字符串形式获取错误消息。
Message := err.Error()
当您返回error
值时,您可以向调用函数提供描述性的error
值。通过使用errors
包的New
功能,您可以提供描述性的error
值,如下所示:
func Login(user User) (User, error) {
var u User
err = C.Find(bson.M{"email": user.Email}).One(u)
if err != nil {
return nil, errors.New("Email doesn't exists")
}
// Validate password
err = bcrypt.CompareHashAndPassword(u.HashPassword, []byte(user.Password))
if err != nil {
return nil, errors.New("Invalid password")
}
return u, nil
}
errors.New
函数返回一个error
值,用于向调用函数提供描述性的error
值。fmt
包的Errorf
函数允许您使用fmt
包的格式化功能来创建描述性的error
值,如下所示:
func Login(user User) (User, error) {
var u User
err = C.Find(bson.M{"email": user.Email}).One(u)
if err != nil {
errObj:= fmt.Errorf("User %s doesn't exists. Error:%s, user.Email, err.Error())
return nil, errObj
}
// Validate password
err = bcrypt.CompareHashAndPassword(u.HashPassword, []byte(user.Password))
if err != nil {
errObj:= fmt.Errorf("Invalid password for the user:%s. Error:%s, user.Email, err.Error())
return nil, errObj
}
return u, nil
}
前面的代码块使用fmt.Errorf
函数来使用fmt
包的格式化特性来创建描述性的error
值。
Go 中的函数是一段可重用的代码,它将一系列代码语句组织成一个单元。关键字func
用于声明函数。如果函数的名称以大写字母开头,那么这些函数会被导出到其他包中。Go 函数的一个独特特性是它们可以返回多个值。
Go 提供了三种类型的数据结构来处理数据集合:数组、切片和映射。数组是固定长度的类型,包含单一类型的元素序列。通过指定长度和类型来声明数组。切片类似于数组,但是它的大小可以随时变化,所以您不必指定切片的长度。使用内置的make
函数或切片文字初始化切片。切片可以使用两个内置函数进行修改:append
和copy
。映射是哈希表的一种实现,它提供了一个无序的键/值对集合。使用内置的make
函数或使用地图文字来初始化地图。
Go 提供了defer,
,可以用来在函数中写清理逻辑。一个defer
语句将一个函数调用推送到一个保存的列表上,该列表在周围的函数返回后执行。Panic
是一个内置函数,可以让你停止正常执行,并开始一个函数的死机。Recover
是一个内置函数,可恢复对恐慌功能的控制。Recover
仅用于延迟函数内部。
Go 使用一种不同且独特的方法在 Go 代码中实现异常处理。因为 Go 函数可以返回多个值,所以return
语句提供了一个error
值,以及其他返回值。这样,调用函数可以检查返回的error
值,以确定是否有错误。
三、结构和接口
当你写程序时,你选择的语言的类型系统是非常重要的。类型允许您以结构化的方式组织应用程序数据,这些数据可以保存在各种数据存储区中。当您编写应用程序(尤其是业务应用程序)时,您使用各种类型来组织应用程序数据,并将这些类型的值保存到持久化存储中。当你用 Go 编写应用程序时,理解它的类型系统和设计理念是很重要的。Go 提供了int
、uint
、float64
、string
、bool
等多种内置类型。用于存储数组、切片和映射等值集合的数据结构被称为复合类型,因为它们由其他类型(内置类型和用户定义类型)组成。除了 Go 提供的内置类型之外,还可以通过与其他类型结合来创建自己的类型。本章包含 Go 中用户定义类型的配方。
Go 为其类型系统提供了简单性和实用性,因为该语言对各种语言规范都有很大的贡献。Go 的类型系统是为解决现实世界的问题而设计的,而不是过于依赖学术理论,当你为你的应用程序设计数据模型时,它避免了很多复杂性。Go 的面向对象方法不同于其他语言,如 C++、Java 和 C#。Go 在其类型系统中不支持继承,甚至没有一个class
关键字。Go 有一个 struct 类型,如果你想比较 Go 的类型系统和其他面向对象语言的类型系统,它类似于类。Go 中的 struct 类型是类的一个轻量级版本,它遵循一种独特的设计,这种设计更倾向于组合而不是继承。
3-1.创建用户定义的类型
问题
您希望创建用户定义的类型来组织您的应用程序数据。
解决办法
Go 有一个 struct 类型,允许您通过与其他类型组合来创建用户定义的类型。
它是如何工作的
Go struct
允许您通过组合一个或多个类型来创建自己的类型,包括内置类型和用户定义类型。结构是在 Go 中创建具体的用户定义类型的唯一方法。当您使用struct
创建自己的类型时,重要的是要理解 Go 不支持其类型系统中的继承,但是它支持类型的组合,这允许您通过组合较小的类型来创建较大的类型。Go 的设计理念是通过组合较小的和模块化的组件来创建较大的组件。如果你是一个务实的程序员,你会欣赏 Go 的设计哲学,因为它有实际的好处,所以更喜欢组合而不是继承。类型的继承有时会在可维护性方面带来实际挑战。
声明结构类型
关键字struct
用于将类型声明为 struct。清单 3-1 展示了一个表示客户信息的示例结构。
type Customer struct {
FirstName string
LastName string
Email string
Phone string
}
Listing 3-1.Declare Struct Type
声明了一个结构类型Customer
,它有四个string
类型的字段。注意,Customer
结构及其字段被导出到其他包中,因为标识符是以大写字母开头的。在 Go 中,如果名称以大写字母开头,标识符将被导出到其他包中;否则,包内的可访问性将受到限制。如果一组结构字段有一个共同的类型,你可以在一个单行语句中组织相同类型的字段,如清单 3-2 所示。
type Customer struct {
FirstName, LastName, Email, Phone string
}
Listing 3-2.Declare Struct Type
因为Customer
结构的所有字段都有string
类型,所以可以在一条语句中指定字段。
创建结构类型的实例
您可以通过声明一个struct
变量或使用 struct 文字来创建struct
类型的实例。清单 3-3 显示了通过声明一个struct
变量并将值赋给 struct 的字段来创建一个Customer
struct 实例的代码块。
var c Customer
c.FirstName = "Alex"
c.LastName = "John"
c.Email = "alex@email.com"
c.Phone = "732-757-2923"
Listing 3-3.Creating a Struct Instance and Assigning Values
创建了一个Customer
类型的实例,并将值逐个分配给结构字段。struct 文本也可以用于创建struct
类型的实例。清单 3-4 显示了代码块,该代码块通过使用一个结构文本并给该结构的字段赋值来创建一个Customer
结构的实例。
c := Customer{
FirstName: "Alex",
LastName: "John",
Email: "alex@email.com",
Phone: "732-757-2923",
}
Listing 3-4.Creating a Struct Instance Using a Struct Literal
使用 struct 文本创建一个Customer
类型的实例,并将值赋给 struct 字段。请注意,即使在 struct 的最后一个字段初始化之后,也会添加一个逗号。当使用 struct 文本创建结构的实例时,可以将值初始化为多行语句,但即使在结构字段的赋值结束后也必须加上逗号。在清单 3-4 中,您通过指定结构字段来初始化值。如果您清楚地知道字段的顺序,您可以在初始化值时省略字段标识符,如清单 3-5 所示。
c := Customer{
"Alex",
"John",
"alex@email.com",
"732-757-2923",
}
Listing 3-5.Creating a Struct Instance Using a Struct Literal
当您使用 struct 文本创建 struct 实例时,您可以向 struct 的特定字段提供值,如清单 3-6 所示。
c := Customer{
FirstName: "Alex",
Email: "alex@email.com",
}
Listing 3-6.Creating a Struct Instance Using a Struct Literal by Specifying Values to a Few Fields
使用用户定义的类型作为字段的类型
使用内置类型的字段创建了Customer
结构。您可以使用其他结构类型作为结构字段的类型。让我们扩展一下Customer
结构,添加一个新字段来保存地址信息,用一个结构作为新字段的类型。清单 3-7 显示了通过添加一个新字段来扩展的Customer
结构,该字段的类型是Address
类型的一部分。
type Address struct {
Street, City, State, Zip string
IsShippingAddress bool
}
type Customer struct {
FirstName, LastName, Email, Phone string
Addresses []Address
}
Listing 3-7.
Customer Struct with a Slice of a User-Defined Type as the Type for Field
通过添加一个新字段Addresses
扩展了Customer
结构,该字段的类型被指定为一个名为Address
的结构的一部分。使用Addresses
字段,您可以为一个客户指定多个地址。IsShippingAddress
字段用于指定默认发货地址。清单 3-8 显示了创建这个修改过的Customer
结构的实例的代码块。
c := Customer{
FirstName: "Alex",
LastName: "John",
Email: "alex@email.com",
Phone: "732-757-2923",
Addresses: []Address{
Address{
Street: "1 Mission Street",
City: "San Francisco",
State: "CA",
Zip: "94105",
IsShippingAddress: true,
},
Address{
Street: "49 Stevenson Street",
City: "San Francisco",
State: "CA",
Zip: "94105",
},
},
}
Listing 3-8.Creating an Instance of Customer Struct
通过创建一个长度为两个值的Address
类型的切片来初始化Addresses
字段。
3-2.向结构类型添加方法
问题
您希望将行为添加到struct
类型中,以提供对struct
的操作,并作为方法调用。
解决办法
Go 的类型系统允许你使用一个方法接收器向结构类型添加方法。方法接收器指定哪种类型必须将函数作为方法关联到该类型。
它是如何工作的
在 Go 中,方法是一个由接收者指定的函数。让我们给Customer
结构添加一个方法。
func (c Customer) ToString() string {
return fmt.Sprintf("Customer: %s %s, Email:%s", c.FirstName, c.LastName, c.Email)
}
方法ToString
被添加到Customer
结构中。在方法名之前使用一个额外的参数部分来指定接收方。在方法内部,您可以使用 receiver 的标识符来访问 receiver 类型的字段。ToString
方法通过访问 struct 字段以字符串形式返回客户名称和电子邮件。
return fmt.Sprintf("Customer: %s %s, Email:%s", c.FirstName, c.LastName, c.Email)
清单 3-9 展示了一个示例程序,它声明了Customer
结构并向其中添加了一些方法。
package main
import (
"fmt"
)
type Address struct {
Street, City, State, Zip string
IsShippingAddress bool
}
type Customer struct {
FirstName, LastName, Email, Phone string
Addresses []Address
}
func (c Customer) ToString() string {
return fmt.Sprintf("Customer: %s %s, Email:%s", c.FirstName, c.LastName, c.Email)
}
func (c Customer) ShippingAddress() string {
for _, v := range c.Addresses {
if v.IsShippingAddress == true {
return fmt.Sprintf("%s, %s, %s, Zip - %s", v.Street, v.City, v.State, v.Zip)
}
}
return ""
}
func main() {
c := Customer{
FirstName: "Alex",
LastName: "John",
Email: "alex@email.com",
Phone: "732-757-2923",
Addresses: []Address{
Address{
Street: "1 Mission Street",
City: "San Francisco",
State: "CA",
Zip: "94105",
IsShippingAddress: true,
},
Address{
Street: "49 Stevenson Street",
City: "San Francisco",
State: "CA",
Zip: "94105",
},
},
}
fmt.Println(c.ToString())
fmt.Println(c.ShippingAddress())
}
Listing 3-9.Struct with Methods
通过指定方法接收器,Customer
结构被附加到几个方法上。ToString
返回客户姓名和电子邮件,ShippingAddress
从存储在Addresses
字段中的地址列表中返回默认送货地址。在main
函数中,创建了一个Customer
结构的实例,并调用了它的方法。
运行该程序时,您应该会看到以下输出:
Customer: Alex John, Email:alex@email.com
1 Mission Street, San Francisco, CA, Zip - 94105
方法是带有接收器的函数。有两种类型的方法接收器:指针接收器和值接收器。清单 3-9 中的程序使用一个值接收器向Customer
结构添加方法。当用指针接收器指定方法时,用指向接收器值的指针调用该方法,当用值接收器指定方法时,使用接收器值的副本。因此,如果您想要改变接收器的状态(字段值),您必须使用指针接收器。
让我们给Customer
结构添加一个新方法(参见清单 3-9 )来探索指针接收器。首先,让我们通过指定不带指针的接收者来添加方法。
func (c Customer) ChangeEmail(newEmail string) {
c.Email = newEmail
}
新添加的ChangeEmail
方法为Email
字段分配一个新的电子邮件地址。让我们创建一个Customer
结构的实例,并通过传递一个新的电子邮件地址来调用ChangeEmail
方法。
c := Customer{
FirstName: "Alex",
LastName: "John",
Email: "alex@gmail.com",
Phone: "732-757-2923",
Addresses: []Address{
Address{
Street: "1 Mission Street",
City: "San Francisco",
State: "CA",
Zip: "94105",
IsShippingAddress: true,
},
Address{
Street: "49 Stevenson Street",
City: "San Francisco",
State: "CA",
Zip: "94105",
},
},
}
// Call ChangeEmail
c.ChangeEmail("alex.john@gmail.com")
fmt.Println(c.ToString())
运行该程序时,您应该会看到以下输出:
Customer: Alex John, Email:alex@gmail.com
您已经向ChangeEmail
方法提供了一个新的电子邮件来更改电子邮件地址,但是当您调用ToString
方法时,它并没有反映出来。您仍然会从电子邮件字段收到旧电子邮件。若要修改方法内部结构值的状态,必须用指针接收器声明方法,以便字段值的更改将反映在方法外部。清单 3-10 修改了ChangeEmail
方法,用一个指针接收器来指定,这样对 Email 字段的更改将会在ChangeEmail
方法之外得到反映。
func (c *Customer) ChangeEmail(newEmail string) {
c.Email = newEmail
}
Listing 3-10.A Method to Customer Struct with a Pointer Receiver
让我们创建一个Customer
struct 的实例,并通过传递一个新的电子邮件地址来调用ChangeEmail
方法。
c := Customer{
FirstName: "Alex",
LastName: "John",
Email: "alex@gmail.com",
Phone: "732-757-2923",
}
// Call ChangeEmail
c.ChangeEmail(alex.john@gmail.com)
fmt.Println(c.ToString())
运行该程序时,您应该会看到以下输出:
Customer: Alex John, Email:alex.john@gmail.com
1 Mission Street, San Francisco, CA, Zip - 94105
输出显示Email
字段的值已经改变。这里,Customer
类型的值用于调用用指针接收器指定的ChangeEmail
方法。
以下代码块使用类型为Customer
的指针来调用通过指针接收器指定的ChangeEmail
方法:
c := $Customer{
FirstName: "Alex",
LastName: "John",
Email: "alex@gmail.com",
Phone: "732-757-2923",
}
// Call ChangeEmail
c.ChangeEmail(alex.john@gmail.com)
值得注意的是,您可以向任何类型添加方法,包括内置类型。您可以向基元类型、复合类型和用户定义的类型添加方法。您可以为指针或值接收器类型定义方法,因此了解何时在方法上为接收器使用值或指针非常重要。简而言之,如果方法需要改变接收方的状态,接收方必须是指针。如果接收器是大型结构、数组或切片,指针接收器会更有效,因为它避免了在方法调用时复制大型数据结构的值。如果一个方法被指定了一个指针接收器,可能是为了改变接收器,那么最好在相同接收器类型的所有方法上使用指针接收器,这为用户提供了更好的可用性和可读性。
Customer
结构的ChangeEmail
方法需要改变它的接收者。因此,为了更好的可用性和清晰性,让我们修改其他方法。清单 3-11 修改了清单 3-9 的程序,所有方法都由指针接收器指定。
package main
import (
"fmt"
)
type Address struct {
Street, City, State, Zip string
IsShippingAddress bool
}
type Customer struct {
FirstName, LastName, Email, Phone string
Addresses []Address
}
func (c *Customer) ToString() string {
return fmt.Sprintf("Customer: %s %s, Email:%s", c.FirstName, c.LastName, c.Email)
}
func (c *Customer) ChangeEmail(newEmail string) {
c.Email = newEmail
}
func (c *Customer) ShippingAddress() string {
for _, v := range c.Addresses {
if v.IsShippingAddress == true {
return fmt.Sprintf("%s, %s, %s, Zip - %s", v.Street, v.City, v.State, v.Zip)
}
}
return ""
}
func main() {
c := &Customer{
FirstName: "Alex",
LastName: "John",
Email: "alex@email.com",
Phone: "732-757-2923",
Addresses: []Address{
Address{
Street: "1 Mission Street",
City: "San Francisco",
State: "CA",
Zip: "94105",
IsShippingAddress: true,
},
Address{
Street: "49 Stevenson Street",
City: "San Francisco",
State: "CA",
Zip: "94105",
},
},
}
fmt.Println(c.ToString())
c.ChangeEmail("alex.john@gmail.com")
fmt.Println("Customer after changing the Email:")
fmt.Println(c.ToString())
fmt.Println(c.ShippingAddress())
}
Listing 3-11.Struct with Pointer Receiver on Methods
因为ChangeEmail
方法需要改变接收器,所以所有方法都用指针接收器来定义。值得注意的是,您可以将方法与值和指针接收器混合使用。在前面的程序中,使用地址操作符(&
)创建了一个指针Customer
:
c := &Customer{}
Customer
指针c
用于调用Customer
结构的方法:
fmt.Println(c.ToString())
c.ChangeEmail("alex.john@gmail.com")
fmt.Println(c.ToString())
fmt.Println(c.ShippingAddress())
运行该程序时,您应该会看到以下输出:
Customer: Alex John, Email:alex@email.com
Customer after changing the Email:
Customer: Alex John, Email:alex.john@gmail.com
1 Mission Street, San Francisco, CA, Zip - 94105
3-3.使用类型嵌入合成类型
问题
您希望通过组合其他类型来创建类型。
解决办法
Go 支持将类型嵌入到其他类型中,这允许您通过组合其他类型来创建类型。
它是如何工作的
Go 的类型系统强化了组合优先于继承的设计理念,允许你通过嵌入其他类型来创建类型。通过使用通过类型嵌入实现的复合设计理念,您可以通过组合较小的类型来创建较大的类型。
让我们通过在类型中嵌入其他类型来创建类型。清单 3-12 展示了可以用来在电子商务系统中表示订单的数据模型。
type Address struct {
Street, City, State, Zip string
IsShippingAddress bool
}
type Customer struct {
FirstName, LastName, Email, Phone string
Addresses []Address
}
type Order struct {
Id int
Customer
PlacedOn time.Time
Status string
OrderItems []OrderItem
}
type OrderItem struct {
Product
Quantity int
}
type Product struct {
Code, Name, Description string
UnitPrice float64
}
Listing 3-12.Data Model for Order Entity
在清单 3-12 中,Order
结构是通过嵌入另一种类型Customer
结构来声明的。Order
结构用于为客户下订单,因此Customer
结构被嵌入到Order
结构中。要嵌入一个类型,只需指定要嵌入到另一个类型中的类型的名称。
type Order struct {
Customer
}
由于类型嵌入,Customer
结构的字段和行为在Order
结构中可用。Customer
结构将Address
结构的片用于Addresses
字段。Order
结构将OrderItem
结构的片用于OrderItems
字段。Product
结构被嵌入到OrderItem
结构中。在这里,您通过组合几个其他结构类型来创建一个更大的类型Order
结构。
让我们向为表示订单信息而声明的结构类型添加操作。清单 3-13 显示了带有各种行为的Order
的数据模型的完整版本。
package main
import (
"fmt"
"time"
)
type Address struct {
Street, City, State, Zip string
IsShippingAddress bool
}
type Customer struct {
FirstName, LastName, Email, Phone string
Addresses []Address
}
func (c Customer) ToString() string {
return fmt.Sprintf("Customer: %s %s, Email:%s", c.FirstName, c.LastName, c.Email)
}
func (c Customer) ShippingAddress() string {
for _, v := range c.Addresses {
if v.IsShippingAddress == true {
return fmt.Sprintf("%s, %s, %s, Zip - %s", v.Street, v.City, v.State, v.Zip)
}
}
return ""
}
type Order struct {
Id int
Customer
PlacedOn time.Time
Status string
OrderItems []OrderItem
}
func (o *Order) GrandTotal() float64 {
var total float64
for _, v := range o.OrderItems {
total += v.Total()
}
return total
}
func (o *Order) ToString() string {
var orderStr string
orderStr = fmt.Sprintf("Order#:%d, OrderDate:%s, Status:%s, Grand Total:%f\n", o.Id, o.PlacedOn, o.Status, o.GrandTotal())
orderStr += o.Customer.ToString()
orderStr += fmt.Sprintf("\nOrder Items:")
for _, v := range o.OrderItems {
orderStr += fmt.Sprintf("\n")
orderStr += v.ToString()
}
orderStr += fmt.Sprintf("\nShipping Address:")
orderStr += o.Customer.ShippingAddress()
return orderStr
}
func (o *Order) ChangeStatus(newStatus string) {
o.Status = newStatus
}
type OrderItem struct {
Product
Quantity int
}
func (item OrderItem) Total() float64 {
return float64(item.Quantity) * item.Product.UnitPrice
}
func (item OrderItem) ToString() string {
itemStr := fmt.Sprintf("Code:%s, Product:%s -- %s, UnitPrice:%f, Quantity:%d, Total:%f",
item.Product.Code, item.Product.Name, item.Product.Description, item.Product.UnitPrice, item.Quantity, item.Total())
return itemStr
}
type Product struct {
Code, Name, Description string
UnitPrice float64
}
Listing 3-13.Data Model for Order Entity with Operations in models.go
Order
结构的ToString
方法返回一个提供订单所有信息的string
值。ToString
调用其嵌入类型Customer
的ToString
和ShippingAddress
方法。ToString
方法还通过迭代OrderItems
字段来调用OrderItem
结构的ToString
方法,该字段是OrderItem
的一部分。
orderStr += o.Customer.ToString()
orderStr += fmt.Sprintf("\nOrder Items:")
for _, v := range o.OrderItems {
orderStr += fmt.Sprintf("\n")
orderStr += v.ToString()
}
orderStr += fmt.Sprintf("\nShipping Address:")
orderStr += o.Customer.ShippingAddress()
Order
结构的GrandTotal
方法返回订单的总计值,它调用OrderItem
结构的Total
方法来确定每个订单项的总值。
func (o *Order) GrandTotal() float64 {
var total float64
for _, v := range o.OrderItems {
total += v.Total()
}
return total
}
注意,Order
结构的ChangeStatus
方法改变了Status
字段的状态,因此该方法使用了指针接收器。
func (o *Order) ChangeStatus(newStatus string) {
o.Status = newStatus
}
因为ChangeStatus
方法需要一个指针接收器,所以Order
结构的所有其他方法都是用指针接收器定义的。
清单 3-14 显示了main
函数,该函数用于创建Order
结构的一个实例,并调用其ToString
方法来获取订单信息。
package main
import (
"fmt"
"time"
)
func main() {
order := &Order{
Id: 1001,
Customer: Customer{
FirstName: "Alex",
LastName: "John",
Email: "alex@email.com",
Phone: "732-757-2923",
Addresses: []Address{
Address{
Street: "1 Mission Street",
City: "San Francisco",
State: "CA",
Zip: "94105",
IsShippingAddress: true,
},
Address{
Street: "49 Stevenson Street",
City: "San Francisco",
State: "CA",
Zip: "94105",
},
},
},
Status: "Placed",
PlacedOn: time.Date(2016, time.April, 10, 0, 0, 0, 0, time.UTC),
OrderItems: []OrderItem{
OrderItem{
Product: Product{
Code: "knd100",
Name: "Kindle Voyage",
Description: "Kindle Voyage Wifi, 6 High-Resolution Display",
UnitPrice: 220,
},
Quantity: 1,
},
OrderItem{
Product: Product{
Code: "fint101",
Name: "Kindle Case",
Description: "Fintie Kindle Voyage SmartShell Case",
UnitPrice: 10,
},
Quantity: 2,
},
},
}
fmt.Println(order.ToString())
// Change Order status
order.ChangeStatus("Processing")
fmt.Println("\n")
fmt.Println(order.ToString())
}
Listing 3-14.Entry Point of the Program That Creates an Instance of the Order struct in main.go
通过为字段提供值来创建Order
结构的实例,包括嵌入的类型。这里使用了一个指针变量来调用Order
结构的方法。ToString
方法提供了客户所下订单的所有信息。ChangeStatus
方法用于改变订单的状态,从而改变Status
字段的值。嵌入类型时,可以提供类似于结构的普通字段的值。
运行该程序时,您应该会看到以下输出:
Order#:1001, OrderDate:2016-04-10 00:00:00 +0000 UTC, Status:Placed, Grand Total:240.000000
Customer: Alex John, Email:alex@email.com
Order Items:
Code:knd100, Product:Kindle Voyage -- Kindle Voyage Wifi, 6 High-Resolution Display, UnitPrice:220.000000, Quantity:1, Total:220.000000
Code:fint101, Product:Kindle Case -- Fintie Kindle Voyage SmartShell Case, UnitPrice:10.000000, Quantity:2, Total:20.000000
Shipping Address:1 Mission Street, San Francisco, CA, Zip - 94105
Order#:1001, OrderDate:2016-04-10 00:00:00 +0000 UTC, Status:Processing, Grand Total:240.000000
Customer: Alex John, Email:alex@email.com
Order Items:
Code:knd100, Product:Kindle Voyage -- Kindle Voyage Wifi, 6 High-Resolution Display, UnitPrice:220.000000, Quantity:1, Total:220.000000
Code:fint101, Product:Kindle Case -- Fintie Kindle Voyage SmartShell Case, UnitPrice:10.000000, Quantity:2, Total:20.000000
Shipping Address:1 Mission Street, San Francisco, CA, Zip - 94105
该输出显示订单信息,包括总计,这是通过调用类型的相应方法计算的。
3-4.使用界面
问题
您希望创建一个接口类型,将其作为其他类型的协定提供。
解决办法
Go 有一个用户定义的接口类型,可以作为具体类型的契约。Go 的接口类型为您的 Go 应用程序提供了大量的可扩展性和可组合性。用关键字interface
定义接口类型。
它是如何工作的
Go 的interface
类型为您的 Go 应用程序提供了大量的可扩展性和可组合性。像 C#和 Java 这样的编程语言都支持接口类型,但是 Go 的interface
类型在设计理念上是独一无二的。
声明接口类型
与 C#和 Java 不同,在 Go 中,你不需要通过指定任何关键字来显式地将一个interface
实现到一个具体的类型中。要将一个interface
实现为一个具体的类型,只需提供与在interface
类型中定义的相同签名的方法。清单 3-15 显示了一种interface
类型。
type TeamMember interface {
PrintName()
PrintDetails()
}
Listing 3-15.Interface Type TeamMember
interface
类型TeamMember
是在团队中创建各种员工类型的契约。TeamMember
接口在其契约中提供了两个行为:PrintName
和PrintDetails.
将接口实现为具体类型
让我们通过实现接口的两个行为PrintName
和PrintDetails
来创建一个具体类型的TeamMember
接口。清单 3-16 显示了一个具体的TeamMember
类型,它实现了在interface
类型中定义的方法。
type Employee struct {
FirstName, LastName string
Dob time.Time
JobTitle, Location string
}
func (e Employee) PrintName() {
fmt.Printf("\n%s %s\n", e.FirstName, e.LastName)
}
func (e Employee) PrintDetails() {
fmt.Printf("Date of Birth: %s, Job: %s, Location: %s\n", e.Dob.String(), e.JobTitle, e.Location)
}
Listing 3-16.Concrete Type of TeamMember
一个 struct Employee
用保存其状态的字段和基于在TeamMember
接口中定义的行为实现的方法来声明。您不需要使用任何语法来将interface
实现到类型中。相反,只需提供在接口中定义了相同签名的方法,就像您为实现TeamMember
接口的Employee
类型所做的那样。
一个interface
类型的最大好处是它允许你为同一个interface
类型创建不同的实现,这支持了更高层次的可扩展性。
清单 3-17 显示了通过嵌入Employee
类型创建的TeamMember
接口的实现,它是TeamMember
接口的实现。
type Developer struct {
Employee //type embedding for composition
Skills []string
}
Listing 3-17.Type Developer Implements TeamMember Interface
声明了一个结构Developer
,其中嵌入了类型Employee
。在这里你创建了更多具体类型的TeamMember
接口。因为类型Employee
是TeamMember
接口的实现,所以类型Developer
也是TeamMember
接口的实现。类型Employee
中定义的所有字段和方法在Developer
类型中也可用。除了Employee
的嵌入类型外,Developer
结构还提供了一个Skill
字段来表示Developer
类型的技能。
清单 3-18 显示了创建一个Developer
实例并通过嵌入类型Employee
调用可用方法的代码块。
d := Developer{
Employee{
"Steve",
"John",
time.Date(1990, time.February, 17, 0, 0, 0, 0, time.UTC),
"Software Engineer",
"San Francisco",
},
[]string{"Go", "Docker", "Kubernetes"},
}
d.PrintName()
d.PrintDetails()
Listing 3-18.Create an Instance of Developer Type and Call Methods
运行该程序时,您应该会看到以下输出:
Steve John
Date of Birth: 1990-02-17 00:00:00 +0000 UTC, Job: Software Engineer, Location: San Francisco
输出显示了在 Employee 结构中定义的方法可以通过 Developer 结构的实例进行访问。
与Employee
类型相比,Developer
结构更像是TeamMember
接口的具体实现。Employee
类型是为类型嵌入而定义的,用于更具体地实现TeamMember
接口,比如Developer
结构。此时,Developer
结构使用在Employee
结构中定义的方法。因为Developer
结构更像是一个具体的实现,它可能有自己的方法实现。这里的Developer
结构可能需要覆盖Employee
结构中定义的方法来提供额外的功能。清单 3-19 显示了覆盖Developer
结构的方法PrintDetails
的代码块。
// Overrides the PrintDetails
func (d Developer) PrintDetails() {
// Call Employee PrintDetails
d.Employee.PrintDetails()
fmt.Println("Technical Skills:")
for _, v := range d.Skills {
fmt.Println(v)
}
}
Listing 3-19.Overrides for the PrintDetails Method for the Developer struct
这里你调用了Employee
的PrintDetails
方法,并为Developer
结构提供了一个额外的功能。
让我们创建另一个struct
类型来提供TeamMember
接口的不同实现。清单 3-20 显示了一个名为Manager
的结构,它通过嵌入Employee
类型和覆盖PrintDetails
方法来实现TeamMember
接口。
type Manager struct {
Employee //type embedding for composition
Projects []string
Locations []string
}
// Overrides the PrintDetails
func (m Manager) PrintDetails() {
// Call Employee PrintDetails
m.Employee.PrintDetails()
fmt.Println("Projects:")
for _, v := range m.Projects {
fmt.Println(v)
}
fmt.Println("Managing teams for the locations:")
for _, v := range m.Locations {
fmt.Println(v)
}
}
Listing 3-20.Type Manager Implements the TeamMember Interface
除了Employee
的嵌入类型之外,Manager
结构还提供了Projects
和Locations
字段来表示经理管理的项目和位置。
到目前为止,您已经创建了一个名为TeamMember
的接口类型,以及实现TeamMember
接口的三个具体类型:Employee
、Developer
和Manager
。让我们创建一个示例程序来探索这些类型并演示interface
类型。清单 3-21 显示了一个示例程序,它通过使用我们在本节中讨论过的类型来演示interface
。
package main
import (
"fmt"
"time"
)
type TeamMember interface {
PrintName()
PrintDetails()
}
type Employee struct {
FirstName, LastName string
Dob time.Time
JobTitle, Location string
}
func (e Employee) PrintName() {
fmt.Printf("\n%s %s\n", e.FirstName, e.LastName)
}
func (e Employee) PrintDetails() {
fmt.Printf("Date of Birth: %s, Job: %s, Location: %s\n", e.Dob.String(), e.JobTitle, e.Location)
}
type Developer struct {
Employee //type embedding for composition
Skills []string
}
// Overrides the PrintDetails
func (d Developer) PrintDetails() {
// Call Employee PrintDetails
d.Employee.PrintDetails()
fmt.Println("Technical Skills:")
for _, v := range d.Skills {
fmt.Println(v)
}
}
type Manager struct {
Employee //type embedding for composition
Projects []string
Locations []string
}
// Overrides the PrintDetails
func (m Manager) PrintDetails() {
// Call Employee PrintDetails
m.Employee.PrintDetails()
fmt.Println("Projects:")
for _, v := range m.Projects {
fmt.Println(v)
}
fmt.Println("Managing teams for the locations:")
for _, v := range m.Locations {
fmt.Println(v)
}
}
type Team struct {
Name, Description string
TeamMembers []TeamMember
}
func (t Team) PrintTeamDetails() {
fmt.Printf("Team: %s - %s\n", t.Name, t.Description)
fmt.Println("Details of the team members:")
for _, v := range t.TeamMembers {
v.PrintName()
v.PrintDetails()
}
}
func main() {
steve := Developer{
Employee{
"Steve",
"John",
time.Date(1990, time.February, 17, 0, 0, 0, 0, time.UTC),
"Software Engineer",
"San Francisco",
},
[]string{"Go", "Docker", "Kubernetes"},
}
irene := Developer{
Employee{
"Irene",
"Rose",
time.Date(1991, time.January, 13, 0, 0, 0, 0, time.UTC),
"Software Engineer",
"Santa Clara",
},
[]string{"Go", "MongoDB"},
}
alex := Manager{
Employee{
"Alex",
"Williams",
time.Date(1979, time.February, 17, 0, 0, 0, 0, time.UTC),
"Program Manger",
"Santa Clara",
},
[]string{"CRM", "e-Commerce"},
[]string{"San Francisco", "Santa Clara"},
}
// Create team
team := Team{
"Go",
"Golang Engineering Team",
[]TeamMember{steve, irene, alex},
}
// Get details of Team
team.PrintTeamDetails()
}
Listing 3-21.Example Program Demonstrates Interface with Type Embedding and Method Overriding
一个名为Team
的结构被声明为代表一个雇员团队,团队成员的雇员由字段TeamMembers
组织,字段的类型为TeamMember
接口的切片。因为TeamMembers
字段的类型使用了TeamMember
接口的一部分,所以您可以提供TeamMember
接口的任何实现作为值。类型Employee
仅用于嵌入到Developer
和Manager
结构中,这些结构更多的是作为团队成员的雇员的具体实现。
type Team struct {
Name, Description string
TeamMembers []TeamMember
}
Team
的PrintTeamDetails
方法打印一个Team
对象的信息。在PrintTeamDetails
方法中,它遍历TeamMembers
集合的元素,并调用PrintName
和PrintDetails
方法来获取每个团队成员的信息。
func (t Team) PrintTeamDetails() {
fmt.Printf("Team: %s - %s\n", t.Name, t.Description)
fmt.Println("Details of the team members:")
for _, v := range t.TeamMembers {
v.PrintName()
v.PrintDetails()
}
}
在main
函数内部,通过提供实现了TeamMember
接口的三个对象的值,创建了一个 team struct 实例。在三个TeamMember
类型的对象中,两个是用Developer
类型创建的,另一个是用Manager
类型创建的。TeamMembers
字段的值包含不同类型的值;所有对象的连接因素是TeamMember
接口。您只需提供TeamMember
接口的不同实现。最后调用Team
结构的PrintTeamDetails
方法来获取关于Team
类型的值的信息。
func main() {
steve := Developer{
Employee{
"Steve",
"John",
time.Date(1990, time.February, 17, 0, 0, 0, 0, time.UTC),
"Software Engineer",
"San Francisco",
},
[]string{"Go", "Docker", "Kubernetes"},
}
irene := Developer{
Employee{
"Irene",
"Rose",
time.Date(1991, time.January, 13, 0, 0, 0, 0, time.UTC),
"Software Engineer",
"Santa Clara",
},
[]string{"Go", "MongoDB"},
}
alex := Manager{
Employee{
"Alex",
"Williams",
time.Date(1979, time.February, 17, 0, 0, 0, 0, time.UTC),
"Program Manger",
"Santa Clara",
},
[]string{"CRM", "e-Commerce"},
[]string{"San Francisco", "Santa Clara"},
}
// Create team
team := Team{
"Go",
"Golang Engineering Team",
[]TeamMember{steve, irene, alex},
}
// Get details of Team
team.PrintTeamDetails()
}
运行该程序时,您应该会看到以下输出:
Team: Go - Golang Engineering Team
Details of the team members:
Steve John
Date of Birth: 1990-02-17 00:00:00 +0000 UTC, Job: Software Engineer, Location: San Francisco
Technical Skills:
Go
Docker
Kubernetes
Irene Rose
Date of Birth: 1991-01-13 00:00:00 +0000 UTC, Job: Software Engineer, Location: Santa Clara
Technical Skills:
Go
MongoDB
Alex Williams
Date of Birth: 1979-02-17 00:00:00 +0000 UTC, Job: Program Manger, Location: Santa Clara
Projects:
CRM
e-Commerce
Managing teams for the locations:
San Francisco
Santa Clara
四、并发
我们生活在云计算时代,在这个时代,您可以在高性能服务器中快速配置虚拟机。尽管我们的现代计算机发展到现在有了更多的 CPU 内核,但当我们运行应用程序时,我们仍然不能充分利用现代服务器的全部能力。有时我们的应用程序运行缓慢,但当我们查看 CPU 利用率时,它可能没有得到充分利用。问题是我们仍然在使用一些为单核机器时代设计的工具。我们可以通过编写并发程序来提高许多应用程序的性能,并发程序允许您将程序编写为几个自治活动的组合。我们现有的一些编程语言通过使用框架或库来提供对并发性的支持,但不是核心语言的内置特性。
Go 对并发的支持是其主要卖点之一。并发是 Go 的一个内置特性,Go 运行时对使用其并发特性运行的程序有很大的控制力。Go 通过两种范例提供并发性:goroutine 和 channel。Goroutines
让您运行相互独立的功能。Go 中并发执行的函数称为 goroutine,每个函数都被视为执行特定任务的工作单元。您可以通过组合这些自治任务来编写并发程序。除了运行彼此独立的功能之外,Go 还具有通过使用通道在 Go routine 之间发送和接收数据来同步 Go routine 的能力。通道是在 goroutines 之间发送和接收数据的通信机制。
4-1.编写并发程序
问题
您希望通过将函数作为自主活动运行来编写并发程序。
解决办法
Go 能够通过作为goroutine
运行来并发运行功能。Goroutines 是通过调用go
语句创建的,后面跟着您希望作为自治活动运行的函数或方法。
它是如何工作的
在前几章的例子中,所有的程序都是顺序程序。这意味着,在程序中,您按顺序调用函数:每个函数调用都会阻止程序完成该函数的执行,然后调用下一个函数。比如说你写一个程序,需要从main
函数中调用两个函数。这里你可能需要调用第一个函数,然后调用下一个函数。第二个函数的执行将发生在第一个函数执行之后。使用 Go 提供的并发功能,通过 goroutines,您可以同时执行这两个功能,彼此独立
要将一个函数作为goroutine
运行,调用带有go
语句前缀的函数。下面是示例代码块:
f() // A normal function call that executes f synchronously and waits for completing it
go f() // A goroutine that executes f asynchronously and doesn't wait for completing it
普通函数调用和goroutine
的唯一区别是goroutine
是用go
语句创建的。一个可执行的 Go 程序确实至少有一个goroutine
;调用main
函数的goroutine
被称为main goroutine
。清单 4-1 显示了一个示例程序,它创建了两个 goroutines 来打印一个加法表和一个乘法表。这个程序在执行 goroutines 时也使用sync.WaitGroup
同步执行;这里,函数main
正在等待使用sync.WaitGroup
完成 goroutines 的执行。
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
// WaitGroup is used to wait for the program to finish goroutines.
var wg sync.WaitGroup
func main() {
// Add a count of two, one for each goroutine.
wg.Add(2)
fmt.Println("Start Goroutines")
// Launch functions as goroutines
go addTable()
go multiTable()
// Wait for the goroutines to finish.
fmt.Println("Waiting To Finish")
wg.Wait()
fmt.Println("\nTerminating Program")
}
func addTable() {
// Schedule the call to WaitGroup's Done to tell goroutine is completed.
defer wg.Done()
for i := 1; i <= 10; i++ {
sleep := rand.Int63n(1000)
time.Sleep(time.Duration(sleep) * time.Millisecond)
fmt.Println("Addition Table for:", i)
for j := 1; j <= 10; j++ {
fmt.Printf("%d+%d=%d\t", i, j, i+j)
}
fmt.Println("\n")
}
}
func multiTable() {
// Schedule the call to WaitGroup's Done to tell goroutine is completed.
defer wg.Done()
for i := 1; i <= 10; i++ {
sleep := rand.Int63n(1000)
time.Sleep(time.Duration(sleep) * time.Millisecond)
fmt.Println("Multiplication Table for:", i)
for j := 1; j <= 10; j++ {
//res = i + j
fmt.Printf("%d*%d=%d\t", i, j, i*j)
}
fmt.Println("\n")
}
}
Listing 4-1.Example Program Demonstrates how to Create Goroutines
该程序创建了两个 goroutines:一个函数用于打印加法表,另一个函数用于打印乘法表。因为这两个函数同时运行,所以都将输出打印到控制台窗口中。go
语句用于启动函数作为 goroutines。
go addTable()
go multiTable()
程序使用WaitGroup
类型的sync
包,用于等待程序完成从main
功能启动的所有 goroutines。否则,goroutines 将从main
功能启动,然后在 goroutines 执行完成之前终止程序。WaitGroup
类型的Wait
方法等待程序完成所有 goroutines。WaitGroup
类型使用一个counter
来指定 goroutines 的数量,而Wait
阻塞程序的执行,直到WaitGroup counter
为零。
var wg sync.WaitGroup
wg.Add(2)
Add
方法用于给WaitGroup
增加一个计数器,这样对Wait
方法的调用就会阻塞执行,直到WaitGroup
计数器为零。在这里,两个计数器被添加到WaitGroup
中,每个 goroutine 一个计数器。在作为 goroutines 启动的addTable
和multiTable
函数中,WaitGroup
的Done
方法被调度使用defer
语句来递减WaitGroup
计数器。因此,在执行每个 goroutine 后,WaitGroup
计数器减 1。
func addTable() {
// Schedule the call to WaitGroup's Done to tell goroutine is completed.
defer wg.Done()
for i := 1; i <= 10; i++ {
sleep := rand.Int63n(1000)
time.Sleep(time.Duration(sleep) * time.Millisecond)
fmt.Println("Addition Table for:", i)
for j := 1; j <= 10; j++ {
//res = i + j
fmt.Printf("%d+%d=%d\t", i, j, i+j)
}
fmt.Println("\n")
}
}
当在main
函数中调用Wait
方法时,它会阻止执行,直到WaitGroup
计数器达到零值,并确保所有的 goroutines 都被执行。
func main() {
// Add a count of two, one for each goroutine.
wg.Add(2)
fmt.Println("Start Goroutines")
// Launch functions as goroutines
go addTable()
go multiTable()
// Wait for the goroutines to finish.
fmt.Println("Waiting To Finish")
wg.Wait()
fmt.Println("\nTerminating Program")
}
您应该会看到类似如下的输出:
Start Goroutines
Waiting To Finish
Addition Table for: 1
1+1=2 1+2=3 1+3=4 1+4=5 1+5=6 1+6=7 1+7=8 1+8=9 1+9=10 1+10=11
Multiplication Table for: 1
1*1=1 1*2=2 1*3=3 1*4=4 1*5=5 1*6=6 1*7=7 1*8=8 1*9=9 1*10=10
Multiplication Table for: 2
2*1=2 2*2=4 2*3=6 2*4=8 2*5=10 2*6=12 2*7=14 2*8=16 2*9=18 2*10=20
Addition Table for: 2
2+1=3 2+2=4 2+3=5 2+4=6 2+5=7 2+6=8 2+7=9 2+8=10 2+9=11 2+10=12
Multiplication Table for: 3
3*1=3 3*2=6 3*3=9 3*4=12 3*5=15 3*6=18 3*7=21 3*8=24 3*9=27 3*10=30
Addition Table for: 3
3+1=4 3+2=5 3+3=6 3+4=7 3+5=8 3+6=9 3+7=10 3+8=11 3+9=12 3+10=13
Addition Table for: 4
4+1=5 4+2=6 4+3=7 4+4=8 4+5=9 4+6=10 4+7=11 4+8=12 4+9=13 4+10=14
Addition Table for: 5
5+1=6 5+2=7 5+3=8 5+4=9 5+5=10 5+6=11 5+7=12 5+8=13 5+9=14 5+10=15
Multiplication Table for: 4
4*1=4 4*2=8 4*3=12 4*4=16 4*5=20 4*6=24 4*7=28 4*8=32 4*9=36 4*10=40
Addition Table for: 6
6+1=7 6+2=8 6+3=9 6+4=10 6+5=11 6+6=12 6+7=13 6+8=14 6+9=15 6+10=16
Multiplication Table for: 5
5*1=5 5*2=10 5*3=15 5*4=20 5*5=25 5*6=30 5*7=35 5*8=40 5*9=45 5*10=50
Addition Table for: 7
7+1=8 7+2=9 7+3=10 7+4=11 7+5=12 7+6=13 7+7=14 7+8=15 7+9=16 7+10=17
Multiplication Table for: 6
6*1=6 6*2=12 6*3=18 6*4=24 6*5=30 6*6=36 6*7=42 6*8=48 6*9=54 6*10=60
Multiplication Table for: 7
7*1=7 7*2=14 7*3=21 7*4=28 7*5=35 7*6=42 7*7=49 7*8=56 7*9=63 7*10=70
Addition Table for: 8
8+1=9 8+2=10 8+3=11 8+4=12 8+5=13 8+6=14 8+7=15 8+8=16 8+9=17 8+10=18
Multiplication Table for: 8
8*1=8 8*2=16 8*3=24 8*4=32 8*5=40 8*6=48 8*7=56 8*8=64 8*9=72 8*10=80
Multiplication Table for: 9
9*1=9 9*2=18 9*3=27 9*4=36 9*5=45 9*6=54 9*7=63 9*8=72 9*9=81 9*10=90
Addition Table for: 9
9+1=10 9+2=11 9+3=12 9+4=13 9+5=14 9+6=15 9+7=16 9+8=17 9+9=18 9+10=19
Addition Table for: 10
10+1=11 10+2=12 10+3=13 10+4=14 10+5=15 10+6=16 10+7=17 10+8=18 10+9=19 10+10=20
Multiplication Table for: 10
10*1=10 10*2=20 10*3=30 10*4=40 10*5=50 10*6=60 10*7=70 10*8=80 10*9=90 10*10=100
Terminating Program
您可以看到,addTable
和multiTable
函数同时在控制台窗口中生成输出,因为它们是并发执行的。在addTable
和multiTable
函数中,为了演示起见,执行会延迟一段随机生成的时间。当您运行程序时,输出的顺序每次都会不同,因为函数内部的执行是随机延迟的。
4-2.管理并发的 CPU 数量
问题
您希望管理用于在 Go 运行时执行 goroutines 的 CPU 数量,以便管理并发编程的行为。
解决办法
运行时包的GOMAXPROCS
函数用于改变用于运行并发程序的 CPU 数量。
它是如何工作的
Go 运行时提供了一个调度器,在执行期间管理 goroutines。调度程序与操作系统紧密合作,并在一个 goroutine 的执行过程中控制一切。它调度所有 goroutines 在逻辑处理器上运行,其中每个逻辑处理器都绑定了一个在物理处理器上运行的操作系统线程。简而言之,Go runtime scheduler 针对一个逻辑处理器运行 goroutines,这个逻辑处理器与一个可用的物理处理器中的操作系统线程绑定在一起。请记住,带有操作系统线程的单个逻辑处理器可以同时执行数万个 goroutines。
在执行程序时,Go runtime scheduler 采用GOMAXPROCS
设置的值来找出有多少操作系统线程将试图同时执行代码。比如说,如果GOMAXPROCS
的值是 8,那么程序一次只会在 8 个操作系统线程上执行 goroutines。从 Go 1.5 开始,GOMAXPROCS
的默认值是可用的 CPU 数量,由runtime
包的NumCPU
函数决定。NumCPU
函数返回当前进程可用的逻辑 CPU 数量。在 Go 1.5 之前,GOMAXPROCS
的默认值是 1。使用GOMAXPROCS
环境变量或从程序内调用runtime
包的GOMAXPROCS
函数可以修改GOMAXPROCS
的值。下面的代码块将GOMAXPROCS
的值设置为 1,这样程序将一次在一个操作系统线程上执行 goroutines:
import "runtime"
// Sets the value of GOMAXPROCS
runtime.GOMAXPROCS(1)
4-3.创建频道
问题
您希望在 goroutine 之间发送和接收数据,以便一个 go routine 可以与其他 go routine 通信。
解决办法
Go 提供了一种称为通道的机制,用于在 goroutines 之间共享数据。基于它们的行为,有两种类型的通道:无缓冲通道和缓冲通道。无缓冲信道用于执行 goroutines 之间的同步通信;缓冲通道用于执行异步通信。
它是如何工作的
Goroutines 是在并发编程中用来执行并发活动的一种很好的机制。当您作为一个 goroutine 执行并发活动时,您可能需要将数据从一个 goroutine 发送到另一个 go routine。通道通过充当 goroutines 之间的管道来处理这种通信。根据数据交换的行为,通道分为无缓冲通道和缓冲通道。无缓冲通道用于执行数据的同步交换。另一方面,缓冲通道用于异步执行数据交换。
创建频道
通道由make
函数创建,它指定了chan
关键字和通道的元素类型。下面是创建无缓冲通道的代码块:
// Unbuffered channel of integer type
counter := make(chan int)
使用内置函数make
创建一个integer
类型的无缓冲通道。渠道counter
可以充当integer
类型的价值观的管道。您可以使用内置类型和用户定义类型作为通道元素的类型。
通过指定缓冲通道的容量来创建缓冲通道。下面是声明缓冲通道的代码块:
// Buffered channel of integer type buffering up to 3 values
nums := make(chan int,3)
创建一个integer
类型的缓冲通道,其capacity
为 3。通道nums
能够缓冲多达三个元素的integer
值。
渠道沟通
一个通道有三种操作:send
、receive
和close
。send
操作向通道发送一个值或指针,当执行相应的receive
操作时,该值或指针从通道中读取。通信操作符<-
用于send
和receive
操作:
counter <- 10
前面的语句显示了一个向名为counter
的通道发送值的send
操作。当你写一个值或指针到一个通道时,操作符<-
被放在通道变量的右边。
num = <- counter
前面的语句显示了一个从名为counter
的通道接收值的receive
操作。当你从一个通道接收一个值或指针时,操作符<-
被放在通道变量的左边。
通道有一个关闭通道的close
操作,因此通道上的send
操作不能发生。在封闭通道上的send
操作将导致panic
。在关闭的通道上的receive
操作返回在通道关闭前已经发送到通道中的值;之后,receive
语句返回通道元素类型的零值。
清单 4-2 显示了一个用无缓冲和缓冲通道发送和接收的示例程序。
package main
import (
"fmt"
)
func main() {
// Declare a unbuffered channel
counter := make(chan int)
// Declare a buffered channel with capacity of 3
nums := make(chan int, 3)
go func() {
// Send value to the unbuffered channel
counter <- 1
close(counter) // Closes the channel
}()
go func() {
// Send values to the buffered channel
nums <- 10
nums <- 30
nums <- 50
}()
// Read the value from unbuffered channel
fmt.Println(<-counter)
val, ok := <-counter // Trying to read from closed channel
if ok {
fmt.Println(val) // This won't execute
}
// Read the 3 buffered values from the buffered channel
fmt.Println(<-nums)
fmt.Println(<-nums)
fmt.Println(<-nums)
close(nums) // Closes the channel
}
Listing 4-2.Send and Receive Values with Unbuffered and Buffered Channels
名为counter
的无缓冲通道是用元素类型integer
创建的。名为nums
的缓冲通道也是用元素类型integer
和capacity
3 创建的,这意味着它最多可以缓冲三个值。从main
函数启动一个匿名函数作为 goroutine,并向其写入一个值。通道counter
在写入一个值后关闭。请注意,无缓冲通道上的send
操作会阻止该通道上的执行,直到执行相应的receive
操作,因此该通道将等待另一个 goroutine 的receive
操作。这里receive
操作从main
goroutine 执行。
go func() {
// Send value to the unbuffered channel
counter <- 1
close(counter) // Closes the channel
}()
另一个匿名函数作为 goroutine 启动,将值写入缓冲通道。与无缓冲通道不同,缓冲通道上的send
操作不会阻止执行,您可以缓冲最高达其capacity
的值,此处为 3。
go func() {
// Send values to the buffered channel
nums <- 10
nums <- 30
nums <- 50
}()
该程序从无缓冲通道产生值。在关闭通道counter
之前,一个值被发送到其中,因此程序可以执行一个receive
操作。此后,信道将是空的。
// Read the value from unbuffered channel
fmt.Println(<-counter)
通道上的receive
操作可以识别通道是否为空。下面的代码块检查通道是否为空。
val, ok := <-counter // Trying to read from closed channel
if ok {
fmt.Println(val) // This won't execute
}
receive
操作可以返回两个值。它返回一个额外的boolean
值,指示通信是否成功。在前面的代码块中,如果成功的send
操作将receive
操作传递给通道,则ok
的值将返回true
,如果由于通道关闭且为空而生成零值,则返回false
。在这个程序中,ok
的值将是false
,因为通道是关闭的和空的。
缓冲通道缓冲三个值,因此程序可以执行三个receive
操作来从通道产生值。最后,缓冲通道被关闭,因此不能再对其执行send
操作。
// Read the 3 values from the buffered channel
fmt.Println(<-nums)
fmt.Println(<-nums)
fmt.Println(<-nums)
close(nums) // Closes the channel
在这个简单的例子中,我们没有使用WaitGroup
类型来同步执行,因为我们关注的是通道的行为。如果您的程序想要等待执行完成,请使用WaitGroup
类型来同步执行。运行该程序时,您应该会看到以下输出:
1
10
30
50
缓冲和非缓冲通道的send
和receive
操作具有不同的行为。在接下来的部分中,我们将详细研究缓冲通道和无缓冲通道。
4-4.使用通道进行同步通信
问题
您希望以同步的方式通过通道在 goroutines 之间交换数据,这样您就可以确保一个send
操作能够成功地通过相应的receive
操作传递数据。
解决办法
无缓冲通道以同步方式提供数据交换,确保来自一个 goroutine 的通道上的send
操作成功传递到另一个 goroutine,同时在同一通道上有相应的receive
操作。
它是如何工作的
无缓冲通道确保发送和接收路由器之间的数据交换。当一个send
操作在一个 goroutine 的无缓冲通道上执行时,必须在另一个 goroutine 的相同通道上执行相应的receive
操作,以完成send
操作。因此,send
操作阻塞发送 goroutine,直到另一个 goroutine 执行相应的receive
操作。在执行send
操作之前,可能会尝试receive
操作。如果receive
操作首先执行,接收 goroutine 将被阻塞,直到另一个 goroutine 执行相应的send
操作。简而言之,完成一个 goroutine 中的send
或receive operation
需要执行另一个 goroutine 中相应的send
或receive
操作。这种通信机制确保了数据从一个路由器传递到另一个路由器。
僵局
为了理解无缓冲信道上通信操作的阻塞行为,让我们写一个程序。清单 4-3 显示了一个将创建死锁的示例程序;因此,它将在运行程序时失败。
package main
import (
"fmt"
)
func main() {
// Declare an unbuffered channel
counter := make(chan int)
// This will create a deadlock
counter <- 10 // Send operation to a channel from main goroutine
fmt.Println(<-counter) // Receive operation from the channel
}
Listing 4-3.Example Program That Creates a Deadlock so That the Program Will Fail
运行该程序时,您应该会看到以下错误:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
当执行通信操作时,由于无缓冲通道的阻塞行为,该程序将由于死锁而失败。这里,从主 goroutine 执行send
操作,同时通道试图从同一主 goroutine 执行receive
操作。在执行完send
操作后,定义了receive
操作。当send
操作执行时,它阻塞主 goroutine,这意味着它阻塞整个程序的执行,因为send
操作正在等待同一通道上相应的receive
操作。因为send
操作阻塞执行,所以receive
操作无法执行,导致死锁。在清单 4-4 中,我们通过在 goroutine 中编写send
操作来解决死锁问题。
package main
import (
"fmt"
)
func main() {
// Declare an unbuffered channel
counter := make(chan int)
// Perform send operation by launching new goroutine
go func() {
counter <- 10
}()
fmt.Println(<-counter) // Receive operation from the channel
}
Listing 4-4.Example Program That Fixes the Deadlock Caused in Listing 4-3
该程序将成功运行,不会出现任何问题,因为它通过启动新的 goroutine 来执行send
操作,而receive
操作是在主 goroutine 中执行的。
示例程序
让我们编写一个示例程序来理解无缓冲通道的通信机制,如清单 4-5 所示。
package main
import (
"fmt"
"sync"
)
// wg is used to wait for the program to finish.
var wg sync.WaitGroup
func main() {
count := make(chan int)
// Add a count of two, one for each goroutine.
wg.Add(2)
fmt.Println("Start Goroutines")
// Launch a goroutine with label "Goroutine-1"
go printCounts("Goroutine-1", count)
// Launch a goroutine with label "Goroutine-2"
go printCounts("Goroutine-2", count)
fmt.Println("Communication of channel begins")Sticky
count <- 1
// Wait for the goroutines to finish.
fmt.Println("Waiting To Finish")
wg.Wait()
fmt.Println("\nTerminating the Program")
}
func printCounts(label string, count chan int) {
// Schedule the call to WaitGroup's Done to tell goroutine is completed.
defer wg.Done()
for {
// Receives message from Channel
val, ok := <-count
if !ok {
fmt.Println("Channel was closed")
return
}
fmt.Printf("Count: %d received from %s \n", val, label)
if val == 10 {
fmt.Printf("Channel Closed from %s \n", label)
// Close the channel
close(count)
return
}
val++
// Send count back to the other goroutine.
count <- val
}
}
Listing 4-5.Example Program Demonstrating Unbuffered Channels
创建一个名为count
的integer
类型的无缓冲通道,并启动两个 goroutines。两个 goroutines 都通过提供通道count
和一个string label
来执行printCounts
功能。两个 goroutines 启动后,在通道count
上执行send
操作。这将等待在同一通道上获得相应的receive
操作。
// Launch a goroutine with label "Goroutine-1"
go printCounts("Goroutine-1", count)
// Launch a goroutine with label "Goroutine-2"
go printCounts("Goroutine-2", count)
fmt.Println("Communication of channel begins")
count <- 1
printCounts
函数打印从通道count
接收的值,并通过向count
提供新值在同一通道上执行send
操作,以与其他 goroutines 共享数据。在两个 goroutine 启动后,初始值 1 被发送到通道,因此一个 go routine 可以receive
初始值,并且可以完成send
操作。在从通道接收到一个值后,接收 goroutine sends
向通道增加一个值,因此它阻塞 goroutine,直到另一个 goroutine 从通道接收到该值。send
和receive
继续运行,直到count
的值达到 10。当通道count
的值达到 10 时,通道关闭,因此不能再执行send
操作。
func printCounts(label string, count chan int) {
// Schedule the call to WaitGroup's Done to tell goroutine is completed.
defer wg.Done()
for {
// Receives message from Channel
val, ok := <-count
if !ok {
fmt.Println("Channel was closed")
return
}
fmt.Printf("Count: %d received from %s \n", val, label)
if val == 10 {
fmt.Printf("Channel Closed from %s \n", label)
// Close the channel
close(count)
return
}
val++
// Send count back to the other goroutine.
count <- val
}
}
当在通道上执行receive
操作时,我们检查通道是否关闭,如果通道关闭,则从 goroutine 退出。
val, ok := <-count
if !ok {
fmt.Println("Channel was closed")
return
}
您应该会看到类似如下的输出:
Start Goroutines
Communication of channel begins
Waiting To Finish
Count: 1 received from Goroutine-1
Count: 2 received from Goroutine-2
Count: 3 received from Goroutine-1
Count: 4 received from Goroutine-2
Count: 5 received from Goroutine-1
Count: 6 received from Goroutine-2
Count: 7 received from Goroutine-1
Count: 8 received from Goroutine-2
Count: 9 received from Goroutine-1
Count: 10 received from Goroutine-2
Channel Closed from Goroutine-2
Channel was closed
Terminating the Program
请注意,每次运行程序时,goroutines 的顺序可能会改变。
使用范围表达式接收值
在清单 4-5 中,您使用通信操作符<-
从通道中读取值,并检查通道是否关闭。您已经使用了range
表达式来迭代各种数据结构的元素,比如数组、切片和映射。range
表达式也可以用来从通道中产生值,这对于大多数用例来说会更方便。通道上的range
表达式产生值,直到通道关闭。清单 4-6 用range
表达式重写了清单 4-5 的代码。
package main
import (
"fmt"
"sync"
)
// wg is used to wait for the program to finish.
var wg sync.WaitGroup
func main() {
count := make(chan int)
// Add a count of two, one for each goroutine.
wg.Add(2)
fmt.Println("Start Goroutines")
// Launch a goroutine with label "Goroutine-1"
go printCounts("Goroutine-1", count)
// Launch a goroutine with label "Goroutine-2"
go printCounts("Goroutine-2", count)
fmt.Println("Communication of channel begins")
count <- 1
// Wait for the goroutines to finish.
fmt.Println("Waiting To Finish")
wg.Wait()
fmt.Println("\nTerminating the Program")
}
func printCounts(label string, count chan int) {
// Schedule the call to WaitGroup's Done to tell goroutine is completed.
defer wg.Done()
for val := range count {
fmt.Printf("Count: %d received from %s \n", val, label)
if val == 10 {
fmt.Printf("Channel Closed from %s \n", label)
// Close the channel
close(count)
return
}
val++
// Send count back to the other goroutine.
count <- val
}
}
Listing 4-6.Example Program Demonstrates Unbuffered Channel and range Expression on Channel
range
表达式产生来自通道count
的值,直到通道关闭。
for val := range count {
fmt.Printf("Count: %d received from %s \n", val, label)
}
您应该会看到类似如下的输出:
Start Goroutines
Communication of channel begins
Waiting To Finish
Count: 1 received from Goroutine-1
Count: 2 received from Goroutine-2
Count: 3 received from Goroutine-1
Count: 4 received from Goroutine-2
Count: 5 received from Goroutine-1
Count: 6 received from Goroutine-2
Count: 7 received from Goroutine-1
Count: 8 received from Goroutine-2
Count: 9 received from Goroutine-1
Count: 10 received from Goroutine-2
Channel Closed from Goroutine-2
Terminating the Program
4-5.使用一个例程的输出作为另一个例程的输入
问题
您希望使用一个 goroutine 的输出作为另一个 goroutine 的输入,依此类推。
解决办法
Pipeline 是一种并发模式,指的是通过通道连接的一系列 goroutine 阶段,其中一个 goroutine 的输出是另一个 go routine 的输入,依此类推。
它是如何工作的
让我们编写一个示例程序来探索管道。清单 4-7 显示了一个用 goroutines 和通道演示管道的示例程序。示例程序有一个三级管道,其中三个 goroutines 由两个通道连接。在这个管道中,第一级的 goroutine 用于随机生成上限为 50 的值。管道有一个出站通道,向第二级的 goroutine 提供入站值。第二级的 goroutine 有一个入站通道和一个出站通道。当入站通道随机生成每个值并找出 Fibonacci 值时,它从第一个 goroutine 接收值。然后,它将得到的 Fibonacci 值提供给第三阶段的 goroutine,后者只打印第二阶段的 goroutine 的出站值。下面是示例程序。
package main
import (
"fmt"
"math"
"math/rand"
"sync"
)
type fibvalue struct {
input, value int
}
var wg sync.WaitGroup
// Generates random values
func randomCounter(out chan int) {
defer wg.Done()
var random int
for x := 0; x < 10; x++ {
random = rand.Intn(50)
out <- random
}
close(out)
}
// Produces Fibonacci values of inputs provided by randomCounter
func generateFibonacci(out chan fibvalue, in chan int) {
defer wg.Done()
var input float64
for v := range in {
input = float64(v)
// Fibonacci using Binet's formula
Phi := (1 + math.Sqrt(5)) / 2
phi := (1 - math.Sqrt(5)) / 2
result := (math.Pow(Phi, input) - math.Pow(phi, input)) / math.Sqrt(5)
out <- fibvalue{
input: v,
value: int(result),
}
}
close(out)
}
// Print Fibonacci values generated by generateFibonacci
func printFibonacci(in chan fibvalue) {
defer wg.Done()
for v := range in {
fmt.Printf("Fibonacci value of %d is %d\n", v.input, v.value)
}
}
func main() {
// Add 3 into WaitGroup Counter
wg.Add(3)
// Declare Channels
randoms := make(chan int)
fibs := make(chan fibvalue)
// Launching 3 goroutines
go randomCounter(randoms) // First stage of pipeline
go generateFibonacci(fibs, randoms) // Second stage of pipeline
go printFibonacci(fibs) // Third stage of pipeline
// Wait for completing all goroutines
wg.Wait()
}
Listing 4-7.A Three-Stage Pipeline with Three Goroutines Connected by Two Channels
该程序打印 10 个随机生成的斐波那契值。两个无缓冲通道用作三级流水线的入站和出站通道。通道randoms
的元素类型是integer
,通道fibs
的元素类型是一个名为fibvalue
的结构类型,由两个字段组成,用于保存一个随机数及其斐波那契值。三个 goroutines 用于完成该流水线。
go randomCounter(randoms) // First stage of pipeline
go generateFibonacci(fibs, randoms) // Second stage of pipeline
go printFibonacci(fibs) // Third stage of pipeline
第一阶段的 goroutine 随机生成上限为 50 的值。
func randomCounter(out chan int) {
defer wg.Done()
var random int
for x := 0; x < 10; x++ {
random = rand.Intn(50)
out <- random
}
close(out)
}
在三级流水线的第一级中,randomCounter
函数向第二级提供输入,第二级在generateFibonacci
函数中实现。randomCounter
功能使用一个用于send
10 个随机生成值的integer
通道,此后该通道关闭。
func generateFibonacci(out chan fibvalue, in chan int) {
defer wg.Done()
var input float64
for v := range in {
input = float64(v)
// Fibonacci using Binet's formula
Phi := (1 + math.Sqrt(5)) / 2
phi := (1 - math.Sqrt(5)) / 2
result := (math.Pow(Phi, input) - math.Pow(phi, input)) / math.Sqrt(5)
out <- fibvalue{
input: v,
value: int(result),
}
}
close(out)
}
generateFibonacci
功能使用两个通道:一个用于从第一级的 goroutine 接收输入,另一个用于向第三级的 goroutine 提供输入。在generateFibonacci
函数中,receive
操作在入站通道上执行,该通道从randomCounter
函数中获取值。可以发送generateFibonacci
的输入值,直到通过randomCounter
功能关闭通道。generateFibonacci
函数为每个输入值生成斐波那契值。这些值被发送到出站通道,以向第三级的 goroutine 提供输入。
func printFibonacci(in chan fibvalue) {
defer wg.Done()
for v := range in {
fmt.Printf("Fibonacci value of %d is %d\n", v.input, v.value)
}
}
流水线的最后阶段在printFibonacci
函数中实现,它打印从generateFibonacci
函数的出站通道接收的斐波那契值。在从generateFibonacci
功能关闭通道之前,可以输出printFibonacci
功能的输入值。
在这个示例程序中,第一级的输出用作第二级的输入,然后第二级的输出用作第三级的输入。您应该会看到类似如下的输出:
Fibonacci value of 31 is 1346268
Fibonacci value of 37 is 24157816
Fibonacci value of 47 is 2971215072
Fibonacci value of 9 is 34
Fibonacci value of 31 is 1346268
Fibonacci value of 18 is 2584
Fibonacci value of 25 is 75025
Fibonacci value of 40 is 102334154
Fibonacci value of 6 is 8
Fibonacci value of 0 is 0
渠道方向
在清单 4-7 中,您使用了由两个通道连接的三个 goroutines。在这些 goroutine 中,一个 goroutine 对一个通道执行send
操作,另一个 go routine 从相同的通道接收值。这里,goroutine 中的一个通道用于send
操作或receive
操作,这样当您将通道指定为参数时,您可以指定通道方向(send
或receive
)。
func generateFibonacci(out chan<- fibvalue, in <-chan int) {
}
这里声明out chan<- fibvalue
指定通道out
用于send
操作,而in <-chan int
指定通道in
用于receive
操作。放置在chan
关键字右侧的通信运算符<-
指定了一个channel
仅用于send
操作;放在chan
关键字的左边,同一个操作符指定一个通道仅用于receive
操作。
通道方向示例
清单 4-8 通过明确指定通道方向重写了清单 4-7 的示例代码。
package main
import (
"fmt"
"math"
"math/rand"
"sync"
)
type fibvalue struct {
input, value int
}
var wg sync.WaitGroup
func randomCounter(out chan<- int) {
defer wg.Done()
var random int
for x := 0; x < 10; x++ {
random = rand.Intn(50)
out <- random
}
close(out)
}
func generateFibonacci(out chan<- fibvalue, in <-chan int) {
defer wg.Done()
var input float64
for v := range in {
input = float64(v)
// Fibonacci using Binet's formula
Phi := (1 + math.Sqrt(5)) / 2
phi := (1 - math.Sqrt(5)) / 2
result := (math.Pow(Phi, input) - math.Pow(phi, input)) / math.Sqrt(5)
out <- fibvalue{
input: v,
value: int(result),
}
}
close(out)
}
func printFibonacci(in <-chan fibvalue) {
defer wg.Done()
for v := range in {
fmt.Printf("Fibonacci value of %d is %d\n", v.input, v.value)
}
}
func main() {
// Add 3 into WaitGroup Counter
wg.Add(3)
// Declare Channels
randoms := make(chan int)
fibs := make(chan fibvalue)
// Launching 3 goroutines
go randomCounter(randoms)
go generateFibonacci(fibs, randoms)
go printFibonacci(fibs)
// Wait for completing all goroutines
wg.Wait()
}
Listing 4-8.A Three-Stage Pipeline with Three Goroutines Connected by Two Channels
在randomCounter
功能中,通道out
仅用于send
操作。generateFibonacci
功能使用两个通道:通道in
用于receive
操作,通道out
用于send
操作。通道in``printFibonacci
功能仅用于receive
操作。
4-6.使用通道进行异步通信
问题
您希望以异步方式通过通道在 goroutines 之间交换数据,并且通道应该能够缓冲值。
解决办法
缓冲通道能够缓冲最大容量的值,并为数据交换提供异步通信。
它是如何工作的
与无缓冲通道不同,缓冲通道可以容纳最大容量的值。缓冲通道就像一个队列,在这个队列上,send
操作不会阻塞任何 goroutine,因为它具有保存元素的能力。只有当通道已满时,缓冲通道上的send
操作才会被阻止,这意味着通道已达到其缓冲容量。缓冲通道的capacity
在使用make
功能创建时确定。下面的语句创建了一个缓冲通道,能够保存三个integer
值的元素。
nums := make(chan int, 3)
下面是对通道nums
进行三个send
操作的代码块:
nums <- 10
nums <- 30
nums <- 50
缓冲通道上的send
操作不会阻止发送 goroutine。这里通道nums
能够保存三个integer
值的元素。一个send
操作在通道的后面插入一个元素,一个receive
操作从通道的前面移除一个元素。这种模式确保缓冲通道上的send
和receive
操作基于先进先出(FIFO)原则。通过send
操作插入的第一个元素将为通道上的第一个receive
操作产生。
以下代码块从通道nums
接收三个值:
fmt.Println(<-nums) // Print 10 (first inserted item)
fmt.Println(<-nums) // Print 30 (second inserted item)
fmt.Println(<-nums) // Print 50 (third inserted item)
一个缓冲通道可以容纳最多的元素。如果一个 goroutine 在缓冲通道上进行的send
操作超过了它的capacity
,这意味着该通道已满,并试图在同一通道上执行另一个send
操作,它会阻塞发送 goroutine,直到有空间可以通过另一个 goroutine 的receive
操作在该通道上插入新元素。同样,在一个空缓冲通道上的receive
操作阻塞接收 goroutine,直到一个元素被另一个 goroutine 的send
操作插入到通道中。
让我们通过编写一个示例程序来探索缓冲通道,如清单 4-9 所示。在这个例子中,一个缓冲通道用于保存来自多个 goroutines 的要执行的任务的信息。缓冲通道能够容纳 10 个指针的元素,这些元素包含关于要完成的作业的信息。正在使用预定义数量的 goroutines 执行这些作业;这是三个。这三个 goroutines 同时从缓冲通道接收值,然后执行作业。
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
type Task struct {
Id int
JobId int
Status string
CreatedOn time.Time
}
func (t *Task) Run() {
sleep := rand.Int63n(1000)
// Delaying the execution for the sake of example
time.Sleep(time.Duration(sleep) * time.Millisecond)
t.Status = "Completed"
}
// wg is used to wait for the program to finish.
var wg sync.WaitGroup
const noOfWorkers = 3
// main is the entry point for all Go programs.
func main() {
// Create a buffered channel to manage the task queue.
taskQueue := make(chan *Task, 10)
// Launch goroutines to handle the work.
// The worker process is distributing with the value of noOfWorkers.
wg.Add(noOfWorkers)
for gr := 1; gr <= noOfWorkers; gr++ {
go worker(taskQueue, gr)
}
// Add Tasks into Buffered channel.
for i := 1; i <= 10; i++ {
taskQueue <- &Task{
Id: i,
JobId: 100 + i,
CreatedOn: time.Now(),
}
}
// Close the channel
close(taskQueue)
// Wait for all the work to get done.
wg.Wait()
}
// worker is launched as a goroutine to process Tasks from
// the buffered channel.
func worker(taskQueue <-chan *Task, workerId int) {
// Schedule the call to Done method of WaitGroup.
defer wg.Done()
for v := range taskQueue {
fmt.Printf("Worker%d: received request for Task:%d - Job:%d\n", workerId, v.Id, v.JobId)
v.Run()
// Display we finished the work.
fmt.Printf("Worker%d: Status:%s for Task:%d - Job:%d\n", workerId, v.Status, v.Id, v.JobId)
}
}
Listing 4-9.Example Demonstrating Buffered Channels
名为Task
的结构类型被定义用于表示要执行的任务。名为Run
的方法被添加到Task
类型中,以复制运行一个任务,该任务将从 goroutines 中执行。
type Task struct {
Id int
JobId int
Status string
CreatedOn time.Time
}
func (t *Task) Run() {
sleep := rand.Int63n(1000)
// Delaying the execution for the sake of example
time.Sleep(time.Duration(sleep) * time.Millisecond)
t.Status = "Completed"
}
通过将指向Task
类型的指针指定为元素类型并将capacity
指定为 10 来创建缓冲通道。
taskQueue := make(chan *Task, 10)
缓冲通道taskQueue
保存要从预定义数量的 goroutines 中执行的任务。通过main
功能,程序启动预定义数量的 goroutines 来分配工作,完成任务的信息可从taskQueue
通道获得。在启动三个 goroutines 之后,缓冲通道被填充了指向Task
值的指针的 10 个元素。
// wg is used to wait for the program to finish.
var wg sync.WaitGroup
const noOfWorkers = 3 // number of goroutines to be used for executing the worker
// main is the entry point for all Go programs.
func main() {
// Create a buffered channel to manage the task queue.
taskQueue := make(chan *Task, 10)
// Launch goroutines to handle the work.
// The worker process is distributing with the value of noOfWorkers.
wg.Add(noOfWorkers)
for gr := 1; gr <= noOfWorkers; gr++ {
go worker(taskQueue, gr)
}
// Add Tasks into Buffered channel.
for i := 1; i <= 10; i++ {
taskQueue <- &Task{
Id: i,
JobId: 100 + i,
CreatedOn: time.Now(),
}
}
// Close the channel
close(taskQueue)
// Wait for all the work to get done.
wg.Wait()
}
函数worker
用于启动 goroutines,通过从缓冲通道接收值来执行任务。该通道包含 10 个任务的信息,通过将worker
函数作为 goroutines 启动,这些任务从三个 go routine 中分配和执行。worker
函数从通道接收元素(指向Task
的指针),然后执行Task
类型的Run
方法来完成任务。
func worker(taskQueue <-chan *Task, workerId int) {
// Schedule the call to Done method of WaitGroup.
defer wg.Done()
for v := range taskQueue {
fmt.Printf("Worker%d: received request for Task:%d - Job:%d\n", workerId, v.Id, v.JobId)
v.Run()
// Display we finished the work.
fmt.Printf("Worker%d: Status:%s for Task:%d - Job:%d\n", workerId, v.Status, v.Id, v.JobId)
}
}
简而言之,在这个例子中,一个缓冲通道被用来发送 10 个任务,这些任务被执行以完成一些工作。因为缓冲通道像队列一样工作,所以通道可以容纳最大容量的值,并且通道上的send
操作不会阻塞 goroutine。这里,在启动一个功能之后,10 个任务的工作由三个 go routine 执行,以便完成 10 个任务的工作可以从多个 go routine 中同时执行。
您应该会看到类似如下的输出:
Worker1: received request for Task:2 - Job:102
Worker3: received request for Task:1 - Job:101
Worker2: received request for Task:3 - Job:103
Worker1: Status:Completed for Task:2 - Job:102
Worker1: received request for Task:4 - Job:104
Worker1: Status:Completed for Task:4 - Job:104
Worker1: received request for Task:5 - Job:105
Worker3: Status:Completed for Task:1 - Job:101
Worker3: received request for Task:6 - Job:106
Worker2: Status:Completed for Task:3 - Job:103
Worker2: received request for Task:7 - Job:107
Worker3: Status:Completed for Task:6 - Job:106
Worker3: received request for Task:8 - Job:108
Worker3: Status:Completed for Task:8 - Job:108
Worker3: received request for Task:9 - Job:109
Worker3: Status:Completed for Task:9 - Job:109
Worker3: received request for Task:10 - Job:110
Worker1: Status:Completed for Task:5 - Job:105
Worker2: Status:Completed for Task:7 - Job:107
Worker3: Status:Completed for Task:10 - Job:110
输出显示,执行 10 个任务的工作是由作为 goroutines 启动的三个 workers 分配的。
4-7.在多个渠道上交流
问题
您希望在多个通道上执行通信操作。
解决办法
Go 提供了一个select
语句,让 goroutine 在多个通道上执行通信操作。
它是如何工作的
当您使用 Go 构建真实世界的并发程序时,您可能需要在一个 goroutine 中处理多个通道,这可能需要您在多个通道上执行通信操作。当与多个通道结合使用时,select
语句是一种强大的通信机制。一个select
块用多个 case 语句编写,让一个 goroutine 等待,直到其中一个 case 可以运行;然后,它执行该案例的代码块。如果有多个 case 块准备好执行,它会随机选择其中一个并执行该 case 的代码块。
清单 4-10 显示了一个示例程序,它执行一个select
块来从一个 goroutine 中的多个通道读取值。
package main
import (
"fmt"
"math"
"math/rand"
"sync"
)
type (
fibvalue struct {
input, value int
}
squarevalue struct {
input, value int
}
)
func generateSquare(sqrs chan<- squarevalue) {
defer wg.Done()
for i := 1; i <= 10; i++ {
num := rand.Intn(50)
sqrs <- squarevalue{
input: num,
value: num * num,
}
}
}
func generateFibonacci(fibs chan<- fibvalue) {
defer wg.Done()
for i := 1; i <= 10; i++ {
num := float64(rand.Intn(50))
// Fibonacci using Binet's formula
Phi := (1 + math.Sqrt(5)) / 2
phi := (1 - math.Sqrt(5)) / 2
result := (math.Pow(Phi, num) - math.Pow(phi, num)) / math.Sqrt(5)
fibs <- fibvalue{
input: int(num),
value: int(result),
}
}
}
func printValues(fibs <-chan fibvalue, sqrs <-chan squarevalue) {
defer wg.Done()
for i := 1; i <= 20; i++ {
select {
case fib := <-fibs:
fmt.Printf("Fibonacci value of %d is %d\n", fib.input, fib.value)
case sqr := <-sqrs:
fmt.Printf("Square value of %d is %d\n", sqr.input, sqr.value)
}
}
}
// wg is used to wait for the program to finish.
var wg sync.WaitGroup
func main() {
wg.Add(3)
// Create Channels
fibs := make(chan fibvalue)
sqrs := make(chan squarevalue)
// Launching 3 goroutines
go generateFibonacci(fibs)
go generateSquare(sqrs)
go printValues(fibs, sqrs)
// Wait for completing all goroutines
wg.Wait()
}
Listing 4-10.A select Block for Reading Values from Multiple Channels
该程序启动了三个 goroutines:一个用于生成 10 个随机生成的数字的斐波那契值;另一个用于产生 10 个随机产生的数字的平方值;最后一个用于打印第一个和第二个 goroutines 生成的结果值。从main
函数中,创建了两个通道,用于传输相应 goroutines 生成的 Fibonacci 值和平方值。函数generateFibonacci
作为 goroutine 启动,它对通道fibs
执行send
操作,以提供斐波那契的值。函数generateSquare
作为 goroutine 启动,它对通道sqrs
执行send
操作,以提供平方值。函数printValues
作为一个 goroutine 启动,它在fibs
和sqrs
通道上轮询,以便在值可以从两个通道receive
输出时打印结果值。
在printValues
函数中,一个select
表达式与两个 case 块一起使用。使用一个for
循环表达式,select
块是 20 次。我们使用 20 次来打印 10 个斐波那契值和 10 个平方值。在真实的场景中,您可能会在一个无限循环中运行它,在这个循环中,您可能会不断地与通道进行通信。
func printValues(fibs <-chan fibvalue, sqrs <-chan squarevalue) {
defer wg.Done()
for i := 1; i <= 20; i++ {
select {
case fib := <-fibs:
fmt.Printf("Fibonacci value of %d is %d\n", fib.input, fib.value)
case sqr := <-sqrs:
fmt.Printf("Square value of %d is %d\n", sqr.input, sqr.value)
}
}
}
这里的select
表达式由两个case
块组成:一个用于fibs
通道上的receive
操作,另一个用于sqrs
通道上的receive
操作。select
语句阻塞 goroutine,直到这些块中的任何一个可以运行,然后它执行那个case
块。如果所有的case
程序块都没有准备好执行,它将一直阻塞,直到一个值sent
进入该程序使用的两个通道中的任何一个。如果有多个case
块准备好执行,它会随机选取一个case
块,然后执行它。
您还可以在一个select
表达式中添加一个缺省块,如果所有其他的 case 块都没有准备好执行,那么它就会执行。还可以在select
块中实现一个超时表达式,如下所示:
select {
case fib := <-fibs:
fmt.Printf("Fibonacci value of %d is %d\n", fib.input, fib.value)
case sqr := <-sqrs:
fmt.Printf("Square value of %d is %d\n", sqr.input, sqr.value)
case <-time.After(time.Second * 3):
fmt.Println("timed out")
}
在前面的代码块中,超时表达式被添加到select
块中。如果select
语句不能在指定的超时时间内运行任何一个case
块,在本例中是 3 秒,那么超时块将被执行。The time.After
函数返回一个通道(<-chan time.Time
,该通道等待给定的持续时间过去,然后在返回的通道上发送当前时间。
您应该会看到类似如下的输出:
Fibonacci value of 31 is 1346268
Square value of 47 is 2209
Fibonacci value of 37 is 24157816
Square value of 9 is 81
Square value of 31 is 961
Square value of 18 is 324
Fibonacci value of 25 is 75025
Fibonacci value of 40 is 102334154
Square value of 0 is 0
Fibonacci value of 6 is 8
Fibonacci value of 44 is 701408732
Square value of 12 is 144
Fibonacci value of 11 is 89
Square value of 39 is 1521
Square value of 28 is 784
Fibonacci value of 11 is 89
Square value of 24 is 576
Square value of 45 is 2025
Fibonacci value of 37 is 24157816
Fibonacci value of 6 is 8
五、使用标准库包
包是 Go 生态系统中非常重要的组成部分。Go 代码被组织成包,使你的 Go 程序具有可重用性和可组合性。Go 安装附带了许多可重用的包,称为标准库包。这些包扩展了 Go 语言,并为构建各种应用程序提供了可重用的库。它们可以帮助您快速构建应用程序,因为您不需要为许多常见功能编写自己的包。如果您想扩展标准库包,您可以创建自己的包,也可以获得 Go 开发者社区提供的第三方包。标准库包的功能非常丰富。您可以只使用标准库包来构建成熟的 web 应用程序,而不使用任何第三方包。本章介绍了如何使用标准库包来实现一些常见的功能,例如编码和解码 JavaScript 对象符号(JSON)对象、解析命令行标志、记录 Go 程序和归档文件。标准库包的文档可从 https://golang.org/pkg/
获得。
5-1.编码和解码 JSON
问题
您希望将 Go 类型的值编码成 JSON 对象,并将 JSON 对象解码成 Go 类型的值。
解决办法
标准库包encoding/json
用于编码和解码 JSON 对象。
它是如何工作的
JSON 是一种数据交换格式,广泛用于 web 后端服务器与 web 和移动应用程序前端之间的通信。当您使用 Go 构建 RESTful 应用程序编程接口(API)时,您可能需要从 HTTP 请求体中解码 JSON 值,并将这些数据解析为 Go 值,并将 Go 值编码为 JSON 值以发送到 HTTP 响应。
编码 JSON
json
包的Marshal
函数用于将 Go 值编码成 JSON 值。要使用json
包,您必须将包encoding/json
添加到导入列表中。
import (
"encoding/json"
)
下面是函数Marshal
的签名:
func Marshal(v interface{}) ([]byte, error)
函数Marshal
返回两个值:作为slice byte
的编码 JSON 数据和一个error
值。
让我们声明一个 struct 类型来演示将 struct 类型的值解析到 JSON 中:
type Employee struct {
ID int
FirstName, LastName, JobTitle string
}
下面的代码块创建了一个Employee
struct 的实例,并将值解析成 JSON。
emp := Employee{
ID: 100,
FirstName: "Shiju",
LastName: "Varghese",
JobTitle: "Architect",
}
// Encoding to JSON
data, err := json.Marshal(emp)
函数Marshal
返回Employee
结构值的 JSON 编码。当您构建基于 JSON 的 RESTful APIs 时,您主要是将 struct 类型的值解析到 JSON 对象中。使用Marshal
,您可以轻松地将 struct 类型的值编码为 JSON 值,这将帮助您快速构建基于 JSON 的 API。
解码 JSON
json
包的函数Unmarshal
用于将 JSON 值解码成 Go 值。下面是函数Unmarshal
的签名:
func Unmarshal(data []byte, v interface{}) error
函数Unmarshal
解析 JSON 编码的数据,并将结果存储到第二个参数中(v interface{}
)。下面的代码块解码 JSON 数据,并将结果存储到Employee
结构的值中:
b := []byte(`{"ID":101,"FirstName":"Irene","LastName":"Rose","JobTitle":"Developer"}`)
var emp1 Employee
// Decoding JSON data into the value of Employee struct
err = json.Unmarshal(b, &emp1)
前面的语句解析变量b
的 JSON 数据,并将结果存储到变量emp1
中。JSON 数据是使用反引号作为原始字符串提供的。在反引号中,除了反引号之外,任何字符都是有效的。现在,您可以像读取普通结构值一样读取Employee
结构的字段,如下所示:
fmt.Printf("ID:%d, Name:%s %s, JobTitle:%s", emp1.ID, emp1.FirstName, emp1.LastName, emp1.JobTitle)
示例:编码和解码
清单 5-1 显示了一个示例程序,该程序演示了将 struct 类型的值编码到 JSON 对象中,以及将 JSON 对象解码成 struct 类型的值。
package main
import (
"encoding/json"
"fmt"
)
// Employee struct
type Employee struct {
ID int
FirstName, LastName, JobTitle string
}
func main() {
emp := Employee{
ID: 100,
FirstName: "Shiju",
LastName: "Varghese",
JobTitle: "Architect",
}
// Encoding to JSON
data, err := json.Marshal(emp)
if err != nil {
fmt.Println(err.Error())
return
}
jsonStr := string(data)
fmt.Println("The JSON data is:")
fmt.Println(jsonStr)
b := []byte(`{"ID":101,"FirstName":"Irene","LastName":"Rose","JobTitle":"Developer"}`)
var emp1 Employee
// Decoding JSON data to a value of struct type
err = json.Unmarshal(b, &emp1)
if err != nil {
fmt.Println(err.Error())
return
}
fmt.Println("The Employee value is:")
fmt.Printf("ID:%d, Name:%s %s, JobTitle:%s", emp1.ID, emp1.FirstName, emp1.LastName, emp1.JobTitle)
}
Listing 5-1.Encoding and Decoding of JSON with a Struct Type
运行该程序时,您应该会看到以下输出:
The JSON data is:
{"ID":100,"FirstName":"Shiju","LastName":"Varghese","JobTitle":"Architect"}
The Employee value is:
ID:101, Name:Irene Rose, JobTitle:Developer
Note
当使用 struct 类型的值对 JSON 数据进行编码和解码时,必须将 struct 类型的所有字段指定为导出字段(标识符名称以大写字母开头),因为在调用Marshal
和Unmarshal
函数时,json
包正在使用 struct 字段的值。
结构标记
当您将 struct 类型的值编码到 JSON 中时,您可能需要在 JSON 编码中使用与 struct 类型的字段不同的字段。例如,您可以以大写字母开头来指定 struct 字段的名称,以将它们标记为导出字段,但是在 JSON 中,元素通常以小写字母开头。在这里,我们可以使用 struct 标记将 struct 字段的名称与 JSON 中的字段名称进行映射,以便在编码和解码 JSON 对象时使用。
下面是用 JSON 编码中要使用的标签和不同名称指定的Employee
结构:
type Employee struct {
ID int `json:"id,omitempty"`
FirstName string `json:"firstname"`
LastName string `json:"lastname"`
JobTitle string `json:"job"`
}
注意,反引号(`
)用于指定标签。在引号中,您将包json
的元数据称为标签。在引号内,除了另一个反引号之外,任何字符都是有效的。结构字段ID
用id
标记,用于 JSON 表示。omitempty
标志指定如果该字段有默认值,则该字段不包含在 JSON 表示中。如果您没有为Employee
结构的ID
字段提供值,那么当您将Employee
值解析到 JSON 时,JSON 对象的输出不包括id
字段。对于 JSON 数据,Employee
结构的所有字段都用不同的名称标记。
如果你想从结构中跳过字段,你可以给标签名为"-"
。这里显示的User
结构指定在编码和解码 JSON 对象时必须跳过字段Password
:
type User struct {
UserName string `json:"user"`
Password string `json:"-"`
}
示例:使用 Struct 标记进行编码和解码
清单 5-2 展示了一个示例程序,演示了用 struct 标签对 JSON 对象进行编码和解码。
package main
import (
"encoding/json"
"fmt"
)
// Employee struct with struct tags
type Employee struct {
ID int `json:"id,omitempty"`
FirstName string `json:"firstname"`
LastName string `json:"lastname"`
JobTitle string `json:"job"`
}
func main() {
emp := Employee{
FirstName: "Shiju",
LastName: "Varghese",
JobTitle: "Architect",
}
// Encoding to JSON
data, err := json.Marshal(emp)
if err != nil {
fmt.Println(err.Error())
return
}
jsonStr := string(data)
fmt.Println("The JSON data is:")
fmt.Println(jsonStr)
b := []byte(`{"id":101,"firstname":"Irene","lastname":"Rose","job":"Developer"}`)
var emp1 Employee
// Decoding JSON to a struct type
err = json.Unmarshal(b, &emp1)
if err != nil {
fmt.Println(err.Error())
return
}
fmt.Println("The Employee value is:")
fmt.Printf("ID:%d, Name:%s %s, JobTitle:%s", emp1.ID, emp1.FirstName, emp1.LastName, emp1.JobTitle)
}
Listing 5-2.Encoding and Decoding of JSON with Struct Tags
运行该程序时,您应该会看到以下输出:
The JSON data is:
{"firstname":"Shiju","lastname":"Varghese","job":"Architect"}
The Employee value is:
ID:101, Name:Irene Rose, JobTitle:Developer
Employee
结构用字段名称标记,用于 JSON 对象。在没有指定ID
字段的情况下创建了Employee
结构的值,因此在将Employee
结构的值编码到 JSON 中时,JSON 对象不包括id
字段。JSON 输出还显示了在 struct 声明中标记的相应 JSON 字段名。当我们解码 JSON 对象时,id
字段不为空,因此它被解析到Employee
结构的ID
字段中。
5-2.使用命令行标志
问题
您希望解析命令行标志,以便为 Go 程序提供一些值。
解决办法
标准库包flag
用于解析命令行标志。
它是如何工作的
有时,在运行程序时,您可能需要通过命令行从最终用户那里接收值。这是构建命令行应用程序时的一个基本特性。命令行选项,也称为标志,可用于在运行程序时向程序提供值。标准库包flag
提供了解析命令行标志的函数。包flag
提供了使用flag.String()
、flag.Bool()
和flag.Int()
解析string
、integer
和boolean
值的函数。
要使用flag
包,您必须将其添加到导入列表中:
import (
"flag"
)
清单 5-3 显示了一个示例程序,演示了如何在 Go 程序中定义标志。
package main
import (
"flag"
"fmt"
)
func main() {
fileName := flag.String("filename", "logfile", "File name for the log file")
logLevel := flag.Int("loglevel", 0, "An integer value for Level (0-4)")
isEnable := flag.Bool("enable", false, "A boolean value for enabling log options")
var num int
// Bind the flag to a variable.
flag.IntVar(&num, "num", 25, "An integer value")
// Parse parses flag definitions from the argument list.
flag.Parse()
// Get the values from pointers
fmt.Println("filename:", *fileName)
fmt.Println("loglevel:", *logLevel)
fmt.Println("enable:", *isEnable)
// Get the value from a variable
fmt.Println("num:", num)
// Args returns the non-flag command-line arguments.
args := flag.Args()
if len(args) > 0 {
fmt.Println("The non-flag command-line arguments are:")
// Print the arguments
for _, v := range args {
fmt.Println(v)
}
}
}
Listing 5-3.Defining Flags Using Package flag
函数flag.String
用于定义通过命令行获取string
值的标志。
fileName := flag.String("filename", "logfile", "File name for the log file")
前面的语句声明了一个string
标志,标志名为filename
,并提供了一个默认值"logfile "
。filename
标志(-filename
的用户输入存储在指针fileName
中,类型为*string
。第三个参数描述了标志的用法。函数flag.Bool()
和flag.Int()
用于声明boolean
和integer
值的标志。
logLevel := flag.Int("loglevel", 0, "An integer value for Level (0-4)")
isEnable := flag.Bool("enable", false, "A boolean value for enabling log options")
如果您想将标志绑定到一个现有的变量,您可以使用函数flag.IntVar
、flag.BoolVar
和flag.StringVar
。下面的代码块将标志num
( -num
)绑定到integer
变量num
。
var num int
// Bind the flag to a variable.
flag.IntVar(&num, "num", 25, "An integer value")
函数flag.Parse()
从命令行解析标志定义。因为函数flag.String()
、flag.Bool()
和flag.Int()
是返回指针,所以我们解引用这些指针来获取值。
fmt.Println("name:", *fileName)
fmt.Println("num:", *logLevel)
fmt.Println("enable:", *isEnable)
函数flag.IntVar
返回一个integer
值,而不是一个指针,这样就可以在不引用指针的情况下读取该值。
fmt.Println("num:", num)
包flag
提供了一个名为Args
的函数,可以用来读取非 flag 命令行参数。如果您提供非 flag 命令行参数,这个函数调用将返回一个string
的slice
。命令行参数位于命令行标志之后。如果用户提供的话,命令行参数会打印到控制台中。
args := flag.Args()
if len(args) > 0 {
fmt.Println("The non-flag command-line arguments are:")
// Print the arguments
for _, v := range args {
fmt.Println(v)
}
}
让我们构建程序,并使用不同的命令行选项运行它:
$ go build
首先,让我们通过提供所有的标志和参数来运行程序。
$ ./ cmdflags -filename=applog -loglevel=2 -enable -num=50 10 20 30 test
filename: applog
loglevel: 2
enable: true
num: 50
The non-flag command-line arguments are:
10
20
30
test
必须在给出标志后提供非标志命令行参数。标志-h
或--help
为命令行程序的使用提供帮助。该帮助文本将由程序中定义的标志定义生成。让我们通过提供-h
标志来运行程序。
$ ./ cmdflags -h
Usage of cmdflags:
-enable
A boolean value for enabling log options
-filename string
File name for the log file (default "logfile")
-loglevel int
An integer value for Level (0-4)
-num int
An integer value (default 25)
现在让我们通过提供几个不带非标志参数的标志来运行程序:
$ ./ cmdflags -filename=applog -loglevel=1
filename: applog
loglevel: 1
enable: false
num: 25
如果用户没有为标志提供值,将采用默认值。
5-3.记录 Go 程序
问题
您希望为您的 Go 程序实现日志记录。
解决办法
标准库包log
提供了一个基本的日志基础设施,可以用来记录你的 Go 程序。
它是如何工作的
尽管有许多第三方包可用于日志记录,但如果您想继续使用标准库或使用简单的包,标准库包log
应该是您的选择。包log
允许你将日志信息写入所有支持io.Writer
接口的标准输出设备。struct type log.Logger
是包log,
中的主要组件,它提供了几种日志记录方法,也支持格式化日志数据。
要使用包log
,您必须将其添加到导入列表:
import (
"log"
)
示例:一个基本的记录器
清单 5-4 显示了一个使用log.Logger
类型提供基本日志实现的示例程序。日志消息分为跟踪、信息、警告和错误,每个日志类别使用四个log.Logger
对象。
package main
import (
"errors"
"io"
"io/ioutil"
"log"
"os"
)
// Package level variables, which are pointers to log.Logger.
var (
Trace *log.Logger
Info *log.Logger
Warning *log.Logger
Error *log.Logger
)
// initLog initializes log.Logger objects
func initLog(
traceHandle io.Writer,
infoHandle io.Writer,
warningHandle io.Writer,
errorHandle io.Writer) {
// Flags for defineing the logging properties, to log.New
flag := log.Ldate | log.Ltime | log.Lshortfile
// Create log.Logger objects
Trace = log.New(traceHandle, "TRACE: ", flag)
Info = log.New(infoHandle, "INFO: ", flag)
Warning = log.New(warningHandle, "WARNING: ", flag)
Error = log.New(errorHandle, "ERROR: ", flag)
}
func main() {
initLog(ioutil.Discard, os.Stdout, os.Stdout, os.Stderr)
Trace.Println("Main started")
loop()
err := errors.New("Sample Error")
Error.Println(err.Error())
Trace.Println("Main completed")
}
func loop() {
Trace.Println("Loop started")
for i := 0; i < 10; i++ {
Info.Println("Counter value is:", i)
}
Warning.Println("The counter variable is not being used")
Trace.Println("Loop completed")
}
Listing 5-4.A Basic Logging Implementation with Categorized Logging for Trace, Information, Warning, and Error Messages
为跟踪、信息、警告和错误的分类日志记录声明了四个指向类型log.Logger
的指针。通过调用函数initLog
来创建log.Logger
对象,该函数接收接口io.Writer
的参数来设置日志消息的目的地。
// Package level variables, which are pointers to log.Logger.
var (
Trace *log.Logger
Info *log.Logger
Warning *log.Logger
Error *log.Logger
)
// initLog initializes log.Logger objects
func initLog(
traceHandle io.Writer,
infoHandle io.Writer,
warningHandle io.Writer,
errorHandle io.Writer) {
// Flags for defining the logging properties, to log.New
flag := log.Ldate | log.Ltime | log.Lshortfile
// Create log.Logger objects
Trace = log.New(traceHandle, "TRACE: ", flag)
Info = log.New(infoHandle, "INFO: ", flag)
Warning = log.New(warningHandle, "WARNING: ", flag)
Error = log.New(errorHandle, "ERROR: ", flag)
}
函数log.New
创建一个新的log.Logger
。在函数New
中,第一个参数设置日志数据的目的地,第二个参数设置出现在每个生成的日志行开头的前缀,第三个参数定义日志属性。给定的日志记录属性在日志数据中提供日期、时间和短文件名。日志数据可以写入任何支持接口io.Writer
的目的地。从功能main
调用功能initLog
。
initLog(ioutil.Discard, os.Stdout, os.Stdout, os.Stderr)
将ioutil.Discard
提供给 Trace 的目的地,这是一个空设备,因此这个目的地的所有日志写调用都将成功,而无需做任何事情。os.Stdout
是给目的地的信息和警告,因此该目的地的所有日志写调用都将出现在控制台窗口中。将os.Stderr
赋予错误的目的地,以便该目的地的所有日志写调用将作为标准错误出现在控制台窗口中。在这个示例程序中,Logger
跟踪、信息、警告和错误的对象用于记录消息。因为跟踪的目的地被配置为ioutil.Discard
,日志数据不会出现在控制台窗口中。
您应该会看到类似如下的输出:
INFO: 2016/06/11 18:47:28 main.go:48: Counter value is: 0
INFO: 2016/06/11 18:47:28 main.go:48: Counter value is: 1
INFO: 2016/06/11 18:47:28 main.go:48: Counter value is: 2
INFO: 2016/06/11 18:47:28 main.go:48: Counter value is: 3
INFO: 2016/06/11 18:47:28 main.go:48: Counter value is: 4
INFO: 2016/06/11 18:47:28 main.go:48: Counter value is: 5
INFO: 2016/06/11 18:47:28 main.go:48: Counter value is: 6
INFO: 2016/06/11 18:47:28 main.go:48: Counter value is: 7
INFO: 2016/06/11 18:47:28 main.go:48: Counter value is: 8
INFO: 2016/06/11 18:47:28 main.go:48: Counter value is: 9
WARNING: 2016/06/11 18:47:28 main.go:50: The counter variable is not being used
ERROR: 2016/06/11 18:47:28 main.go:42: Sample Error
示例:可配置的记录器
在前面的例子中,日志数据被写入Stdout
和Stderr
接口。然而,当您开发真实世界的应用程序时,您可能会使用持久化存储作为日志数据的目的地。您可能还需要一个可配置的选项来指定跟踪、信息、警告或错误的日志级别。这使您可以随时更改日志级别。例如,您可能将日志级别设置为跟踪,但在将应用程序投入生产时,您可能不需要跟踪级别的日志。
清单 5-5 显示了一个示例程序,它提供了一个日志基础设施,允许您将日志级别配置为跟踪、信息、警告或错误,然后将日志数据写入一个文本文件。可以使用命令行标志来配置日志级别选项。
package main
import (
"io"
"io/ioutil"
"log"
"os"
)
const (
// UNSPECIFIED logs nothing
UNSPECIFIED Level = iota // 0 :
// TRACE logs everything
TRACE // 1
// INFO logs Info, Warnings and Errors
INFO // 2
// WARNING logs Warning and Errors
WARNING // 3
// ERROR just logs Errors
ERROR // 4
)
// Level holds the log level.
type Level int
// Package level variables, which are pointers to log.Logger.
var (
Trace *log.Logger
Info *log.Logger
Warning *log.Logger
Error *log.Logger
)
// initLog initializes log.Logger objects
func initLog(
traceHandle io.Writer,
infoHandle io.Writer,
warningHandle io.Writer,
errorHandle io.Writer,
isFlag bool) {
// Flags for defining the logging properties, to log.New
flag := 0
if isFlag {
flag = log.Ldate | log.Ltime | log.Lshortfile
}
// Create log.Logger objects.
Trace = log.New(traceHandle, "TRACE: ", flag)
Info = log.New(infoHandle, "INFO: ", flag)
Warning = log.New(warningHandle, "WARNING: ", flag)
Error = log.New(errorHandle, "ERROR: ", flag)
}
// SetLogLevel sets the logging level preference
func SetLogLevel(level Level) {
// Creates os.*File, which has implemented io.Writer interface
f, err := os.OpenFile("logs.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
log.Fatalf("Error opening log file: %s", err.Error())
}
// Calls function initLog by specifying log level preference.
switch level {
case TRACE:
initLog(f, f, f, f, true)
return
case INFO:
initLog(ioutil.Discard, f, f, f, true)
return
case WARNING:
initLog(ioutil.Discard, ioutil.Discard, f, f, true)
return
case ERROR:
initLog(ioutil.Discard, ioutil.Discard, ioutil.Discard, f, true)
return
default:
initLog(ioutil.Discard, ioutil.Discard, ioutil.Discard, ioutil.Discard, false)
f.Close()
return
}
}
Listing 5-5.A Logging Infrastructure with an Option to Set the Log Level and Write Log Data into a Text File, in logger.go
logger.go
源提供两个功能:initLog
和SetLogLevel
。函数SetLogLevel
通过调用标准库包os
的函数OpenFile
来创建文件对象,然后调用函数initLog
通过提供日志级别首选项来初始化Logger
对象。它打开带有指定标志的命名文件。函数initLog
根据函数提供的日志首选项创建Logger
对象。
声明常量变量是为了指定不同级别的日志级别首选项。标识符iota
用于构造一组相关的常数;在这里,它用于组织应用程序中可用的日志级别,这将产生一个自动递增的integer
常量。每当const
出现在源代码中时,它将值重置为 0,并在常量声明中的每个值之后递增。
const (
// UNSPECIFIED logs nothing
UNSPECIFIED Level = iota // 0 :
// TRACE logs everything
TRACE // 1
// INFO logs Info, Warnings and Errors
INFO // 2
// WARNING logs Warning and Errors
WARNING // 3
// ERROR just logs Errors
ERROR // 4
)
// Level holds the log level.
type Level int
在许多编程语言中,枚举或简单的枚举是声明具有相似行为的常数的惯用方式。与某些编程语言不同,Go 不支持使用关键字来声明枚举。在 Go 中声明枚举的惯用方式是用iota
声明常量。这里,名为Level
的类型和类型int
用于指定常量的类型。常量UNSPECIFIED
的值重置为 0,然后它自动递增每个常量声明,1 代表TRACE
,2 代表INFO
,依此类推。
清单 5-6 显示了一个使用在logger.go
中实现的日志基础设施的 Go 源文件(参见清单 5-5 )。
package main
import (
"errors"
"flag"
)
func main() {
// Parse log level from command line
logLevel := flag.Int("loglevel", 0, "an integer value (0-4)")
flag.Parse()
// Calling the SetLogLevel with the command-line argument
SetLogLevel(Level(*logLevel))
Trace.Println("Main started")
loop()
err := errors.New("Sample Error")
Error.Println(err.Error())
Trace.Println("Main completed")
}
// A simple function for the logging demo
func loop() {
Trace.Println("Loop started")
for i := 0; i < 10; i++ {
Info.Println("Counter value is:", i)
}
Warning.Println("The counter variable is not being used")
Trace.Println("Loop completed")
}
Listing 5-6.Logging Demo in main.go, Using logger.go
在函数main
中,从命令行标志接受日志级别首选项的值,并调用logger.go
的函数SetLogLevel
通过指定日志级别首选项来创建Logger
对象。
logLevel := flag.Int("loglevel", 0, "an integer value (0-4)")
flag.Parse()
// Calling the SetLogLevel with the command-line argument
SetLogLevel(Level(*logLevel))
在本例中,使用Logger
对象记录跟踪、信息、警告和错误。让我们通过为 Trace 提供日志级别首选项(值 1)来运行程序。
$ go build
$ ./log -loglevel=1
这会将日志数据写入名为logs.txt
的文本文件。要跟踪的日志级别写入了Trace
、Information
、Warning
和Error
的日志数据。您应该会在logs.txt
中看到类似如下的日志数据。
TRACE: 2016/06/13 22:04:28 main.go:14: Main started
TRACE: 2016/06/13 22:04:28 main.go:23: Loop started
INFO: 2016/06/13 22:04:28 main.go:25: Counter value is: 0
INFO: 2016/06/13 22:04:28 main.go:25: Counter value is: 1
INFO: 2016/06/13 22:04:28 main.go:25: Counter value is: 2
INFO: 2016/06/13 22:04:28 main.go:25: Counter value is: 3
INFO: 2016/06/13 22:04:28 main.go:25: Counter value is: 4
INFO: 2016/06/13 22:04:28 main.go:25: Counter value is: 5
INFO: 2016/06/13 22:04:28 main.go:25: Counter value is: 6
INFO: 2016/06/13 22:04:28 main.go:25: Counter value is: 7
INFO: 2016/06/13 22:04:28 main.go:25: Counter value is: 8
INFO: 2016/06/13 22:04:28 main.go:25: Counter value is: 9
WARNING: 2016/06/13 22:04:28 main.go:27: The counter variable is not being used
TRACE: 2016/06/13 22:04:28 main.go:28: Loop completed
ERROR: 2016/06/13 22:04:28 main.go:17: Sample Error
TRACE: 2016/06/13 22:04:28 main.go:18: Main completed
让我们通过指定信息的日志级别来运行程序(loglevel
的值为 2)。
$ ./log -loglevel=2
您应该会看到类似下面的日志数据附加到logs.txt
中。
INFO: 2016/06/13 22:13:25 main.go:25: Counter value is: 0
INFO: 2016/06/13 22:13:25 main.go:25: Counter value is: 1
INFO: 2016/06/13 22:13:25 main.go:25: Counter value is: 2
INFO: 2016/06/13 22:13:25 main.go:25: Counter value is: 3
INFO: 2016/06/13 22:13:25 main.go:25: Counter value is: 4
INFO: 2016/06/13 22:13:25 main.go:25: Counter value is: 5
INFO: 2016/06/13 22:13:25 main.go:25: Counter value is: 6
INFO: 2016/06/13 22:13:25 main.go:25: Counter value is: 7
INFO: 2016/06/13 22:13:25 main.go:25: Counter value is: 8
INFO: 2016/06/13 22:13:25 main.go:25: Counter value is: 9
WARNING: 2016/06/13 22:13:25 main.go:27: The counter variable is not being used
ERROR: 2016/06/13 22:13:25 main.go:17: Sample Error
因为我们将日志级别指定为 Information,所以 Information、Warning 和 Error 的日志数据被附加到输出文件logs.txt
中,但是 Trace 的日志数据被写入空设备中。
5-4.以 Tar 和 Zip 格式存档文件
问题
你想读写 tar 和 zip 格式的文件。
解决办法
标准库包archive
包含两个子包——包archive/tar
和包archive/zip,
,用于读写 tar 和 zip 格式的归档文件。
它是如何工作的
标准库包archive
支持以两种文件格式归档文件。为了支持 tar 和 zip 格式的归档功能,它提供了两个独立的包:archive/tar
和archive/zip
。archive/tar
和archive/zip
包分别为 tar 和 zip 格式的读写提供支持。
io。作家和木卫一。阅读器界面
在开始写入和读取归档文件之前,让我们先来看看io.Writer
和io.Reader
接口。标准库包io
提供了执行 I/O 操作的基本接口。包io
的Writer
接口为写操作提供了一个抽象。Writer
接口声明了一个名为Write
的方法,该方法接受一个值byte slice
作为参数。
下面是接口io.Writer
的声明:
type Writer interface {
Write(p []byte) (n int, err error)
}
下面是Write
方法的 Go 文档:
Write
将len(p)
字节从p
写入底层数据流。它返回从p (0 <= n <= len(p))
写入的字节数,以及遇到的导致写入提前停止的任何error
。Write
如果返回n < len(p)
则必须返回一个non-nil error
。写入不得修改slice
数据,即使是暂时的。
包io
的Reader
接口为读操作提供了一个抽象。Reader
接口声明了一个名为Read
的方法,该方法接受一个值byte slice
作为参数。
下面是io.Reader
接口的声明:
type Reader interface {
Read(p []byte) (n int, err error)
}
下面是关于Read
方法的 Go 文档:
Read 将最多len(p)
个字节读入p
。它返回读取的字节数(0 <= n <= len(p)
和遇到的任何error
)。即使Read
返回n < len(p
,它也可能在调用过程中使用所有的p
作为暂存空间。如果有些数据可用,但没有len(p)
字节,Read
通常会返回可用的数据,而不是等待更多数据。当Read
在成功读取n > 0
字节后遇到error
或文件结束条件时,它返回读取的字节数。它可以从同一个调用返回(non-nil
) error
,或者从后续调用返回error
(和n == 0
)。这种一般情况的一个例子是,在输入流末尾返回非零字节数的Reader
可能返回err == EOF
或err == nil
。下一个Read
应该会返回0, EOF
。
当您读写归档文件时,您将利用io.Writer
和io.Reader
接口。
写入和读取 Tar 文件
包archive/tar
用于读写 tar 文件。Tar(磁带归档)文件是在基于 Unix 的系统中使用的归档文件。tar 存档的文件后缀是.tar
。tar
Unix shell 命令从多个指定的文件中创建一个归档文件,或者从归档文件中提取文件。要使用包archive/tar
,您必须将它添加到导入列表中:
import (
"archive/tar"
)
结构类型tar.Writer
用于将文件写入 tar 文件。通过调用接受类型为io.Writer
的值的函数tar.NewWriter
来创建Writer
对象,您可以将 tar 存档文件作为类型为os.File
的对象传递给该函数,以写入所提供的 tar 文件。结构类型os.File
已经实现了io.Writer
接口,因此它可以用作调用函数tar.NewWriter
的参数。
结构类型tar.Reader
用于从 tar 文件中读取文件。通过调用函数tar.NewReader
来创建Reader
对象,该函数接受类型为io.Reader
的值作为参数,您可以将 tar 存档文件作为类型为os.File
的对象传递给该参数,以读取 tar 文件的内容。结构类型os.File
已经实现了接口io.Reader
,因此它可以用作调用函数tar.NewReader
的参数。
清单 5-7 显示了一个示例程序,它演示了如何通过将两个文件写入一个 tar 文件,然后通过遍历 tar 文件并读取每个文件的内容来读取 tar 文件,从而对文件进行归档。
package main
import (
"archive/tar"
"fmt"
"io"
"log"
"os"
)
// addToArchive writes a given file into a .tar file
// Returns nill if the operation is succeeded
func addToArchive(filename string, tw *tar.Writer) error {
// Open the file to archive into tar file.
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
// Get the FileInfo struct that describes the file.
fileinfo, err := file.Stat()
// Create a pointer to tar.Header struct
hdr := &tar.Header{
ModTime: fileinfo.ModTime(), // modified time
Name: filename, // name of header
Size: fileinfo.Size(), // length in bytes
Mode: int64(fileinfo.Mode().Perm()), // permission and mode bits
}
// WriteHeader writes tar.Header and prepares to accept the file's contents.
if err := tw.WriteHeader(hdr); err != nil {
return err
}
// Write the file contents to the tar file.
copied, err := io.Copy(tw, file)
if err != nil {
return err
}
// Check the size of copied file with the source file.
if copied < fileinfo.Size() {
return fmt.Errorf("Size of the copied file doesn't match with source file %s: %s", filename, err)
}
return nil
}
// archiveFiles archives a group of given files into a tar file.
func archiveFiles(files []string, archive string) error {
// Flags for open the tar file.
flags := os.O_WRONLY | os.O_CREATE | os.O_TRUNC
// Open the tar file
file, err := os.OpenFile(archive, flags, 0644)
if err != nil {
return err
}
defer file.Close()
// Creates a new Writer writing to given file object.
// Writer provides sequential writing of a tar archive in POSIX.1 format.
tw := tar.NewWriter(file)
defer tw.Close()
// Iterate through the files to write each file into the tar file.
for _, filename := range files {
// Write the file into tar file.
if err := addToArchive(filename, tw); err != nil {
return err
}
}
return nil
}
// readArchive reads the file contents from tar file.
func readArchive(archive string) error {
// Open the tar archive file.
file, err := os.Open(archive)
if err != nil {
return err
}
defer file.Close()
// Create the tar.Reader to read the tar archive.
// A Reader provides sequential access to the contents of a tar archive.
tr := tar.NewReader(file)
// Iterate through the files in the tar archive.
for {
hdr, err := tr.Next()
if err == io.EOF {
// End of tar archive
break
}
if err != nil {
return err
}
size := hdr.Size
contents := make([]byte, size)
read, err := io.ReadFull(tr, contents)
// Check the size of file contents
if int64(read) != size {
return fmt.Errorf("Size of the opened file doesn't match with the file %s", hdr.Name)
}
fmt.Printf("Contents of the file %s:\n", hdr.Name)
// Writing the file contents into Stdout.
fmt.Fprintf(os.Stdout, "\n%s", contents)
}
return nil
}
func main() {
// Name of the tar file
archive := "source.tar"
// Files to be archived in tar format
files := []string{"main.go", "readme.txt"}
// Archive files into tar format
err := archiveFiles(files, archive)
if err != nil {
log.Fatalf("Error while writing to tar file:%s", err)
}
// Archiving is successful.
fmt.Println("The tar file source.tar has been created")
// Read the file contents of tar file
err = readArchive(archive)
if err != nil {
log.Fatalf("Error while reading the tar file:%s", err)
}
}
Listing 5-7.Writing and Reading a Tar File
在函数main
中,声明了一个变量archive
来为 tar 文件提供文件名。声明一个变量files
来提供文件名作为string slice
来将提供的文件写入 tar 文件。调用函数archiveFiles
来归档文件,调用另一个函数readArchive
来读取 tar 文件的内容,该文件是使用函数archiveFiles
写入的。
func main() {
// Name of the tar file
archive := "source.tar"
// Files to be archived in tar format
files := []string{"main.go", "readme.txt"}
// Archive files into tar format
err := archiveFiles(files, archive)
if err != nil {
log.Fatalf("Error while writing to tar file:%s", err)
}
// Archiving is successful.
fmt.Println("The tar file source.tar has been created")
// Read the file contents of tar file
err = readArchive(archive)
if err != nil {
log.Fatalf("Error while reading the tar file:%s", err)
}
}
在函数archiveFiles
内部,通过打开 tar 文件创建一个os.File
对象,然后通过向函数tar.NewWriter
传递一个File
对象来创建一个新的tar.Writer
。Writer
用于将文件写入 tar 文件。
// Open the tar file
file, err := os.OpenFile(archive, flags, 0644)
if err != nil {
return err
}
defer file.Close()
// Create a new Writer writing to given file object.
// Writer provides sequential writing of a tar archive in POSIX.1 format.
tw := tar.NewWriter(file)
要将文件集合写入 tar 文件,您需要遍历变量files
,它将文件名保存为值string slice
,并调用函数addToArchive
将提供的文件写入 tar 文件。
for _, filename := range files {
// Write the file into tar file.
if err := addToArchive(filename, tw); err != nil {
return err
}
}
函数addToArchive
使用tar.Writer
将提供的文件写入 tar 文件。为了向 tar 文件写入一个新文件,通过提供tar.Header
的值来调用tar.Writer
对象的函数WriteHeader
。然后它调用io.Copy
将文件的数据写入 tar 文件。值tar.Header
包含正在写入 tar 文件的文件的元数据。
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
// Get the FileInfo struct that describes the file.
fileinfo, err := file.Stat()
// Create a pointer to tar.Header struct
hdr := &tar.Header{
ModTime: fileinfo.ModTime(), // modified time
Name: filename, // name of header
Size: fileinfo.Size(), // length in bytes
Mode: int64(fileinfo.Mode().Perm()), // permission and mode bits
}
// WriteHeader writes tar.Header and prepares to accept the file's contents.
if err := tw.WriteHeader(hdr); err != nil {
return err
}
// Write the file contents to the tar file.
copied, err := io.Copy(tw, file)
函数readArchive
用于读取 tar 文件的文件内容。指向tar.Reader
的指针用于读取 tar 文件,它是通过调用函数tar.NewReader
并传递值os.File
来创建的。
// Open the tar archive file.
file, err := os.Open(archive)
if err != nil {
return err
}
defer file.Close()
// Create the tar.Reader to read the tar archive.
// A Reader provides sequential access to the contents of a tar archive.
tr := tar.NewReader(file)
使用tar.Reader
遍历 tar 文件中的文件,并读取写入os.Stdout
的内容。tar.Reader
的函数Next
前进到文件中的下一个条目,并在文件末尾返回一个io.EOF
的error
值。当对函数Next
的调用返回io.EOF
时,您可以退出读取操作,因为这表明您已经遍历了所有文件内容并到达了文件的末尾。
// Iterate through the files in the tar archive.
for {
hdr, err := tr.Next()
if err == io.EOF {
// End of tar archive
fmt.Println("end")
break
}
if err != nil {
return err
}
size := hdr.Size
contents := make([]byte, size)
read, err := io.ReadFull(tr, contents)
// Check the size of file contents
if int64(read) != size {
return fmt.Errorf("Size of the opened file doesn't match with the file %s", hdr.Name)
}
// hdr.Name returns the file name.
fmt.Printf("Contents of the file %s:\n", hdr.Name)
// Writing the file contents into Stdout.
fmt.Fprintf(os.Stdout, "\n%s", contents)
}
在这个例子中,您试图将源文件main.go
和readme.txt
归档到source.tar
文件中。当您运行程序时,您应该看到应用程序目录中的归档文件source.tar
作为写操作的输出,文件main.go
和readme.txt
的内容作为读操作的输出。
编写和读取 Zip 文件
archive/zip
包用于读写 zip 文件。要使用包archive/zip
,您必须将其添加到导入列表中:
import (
"archive/zip"
)
包archive/zip
提供了与package archive/tar
相似的功能,用包zip
读写 zip 文件的过程与处理 tar 文件的过程相似。struct type zip.Writer
用于将文件写入 zip 文件。通过调用接受类型为io.Writer
的值的函数zip.NewWriter
来创建一个新的zip.Writer
。
结构类型zip.ReadCloser
可用于从 zip 文件中读取文件。通过调用函数zip.OpenReader
可以创建Reader
对象,该函数将打开 name 给出的 zip 文件并返回一个zip.ReadCloser
。包zip
还提供了一个类型Reader
;通过调用函数zip.NewReader
创建一个新的Reader
。
清单 5-8 显示了一个示例程序,它演示了如何通过将两个文件写入一个 zip 文件,然后通过遍历 zip 文件中包含的文件并读取每个文件的内容来读取 zip 文件,从而对文件进行归档。
package main
import (
"archive/zip"
"fmt"
"io"
"log"
"os"
)
// addToArchive writes a given file into a zip file.
func addToArchive(filename string, zw *zip.Writer) error {
// Open the given file to archive into a zip file.
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
// Create adds a file to the zip file using the given name/
// Create returns a io.Writer to which the file contents should be written.
wr, err := zw.Create(filename)
if err != nil {
return err
}
// Write the file contents to the zip file.
if _, err := io.Copy(wr, file); err != nil {
return err
}
return nil
}
// archiveFiles archives a group of given files into a zip file.
func archiveFiles(files []string, archive string) error {
flags := os.O_WRONLY | os.O_CREATE | os.O_TRUNC
// Open the tar file
file, err := os.OpenFile(archive, flags, 0644)
if err != nil {
return err
}
defer file.Close()
// Create zip.Writer that implements a zip file writer.
zw := zip.NewWriter(file)
defer zw.Close()
// Iterate through the files to write each file into the zip file.
for _, filename := range files {
// Write the file into tar file.
if err := addToArchive(filename, zw); err != nil {
return err
}
}
return nil
}
// readArchive reads the file contents from tar file.
func readArchive(archive string) error {
// Open the zip file specified by name and return a ReadCloser.
rc, err := zip.OpenReader(archive)
if err != nil {
return err
}
defer rc.Close()
// Iterate through the files in the zip file to read the file contents.
for _, file := range rc.File {
frc, err := file.Open()
if err != nil {
return err
}
defer frc.Close()
fmt.Fprintf(os.Stdout, "Contents of the file %s:\n", file.Name)
// Write the contents into Stdout
copied, err := io.Copy(os.Stdout, frc)
if err != nil {
return err
}
// Check the size of the file.
if uint64(copied) != file.UncompressedSize64 {
return fmt.Errorf("Length of the file contents doesn't match with the file %s", file.Name)
}
fmt.Println()
}
return nil
}
func main() {
// Name of the zip file
archive := "source.zip"
// Files to be archived in zip format.
files := []string{"main.go", "readme.txt"}
// Archive files into zip format.
err := archiveFiles(files, archive)
if err != nil {
log.Fatalf("Error while writing to zip file:%s\n", err)
}
// Read the file contents of tar file.
err = readArchive(archive)
if err != nil {
log.Fatalf("Error while reading the zip file:%s\n", err)
}
}
Listing 5-8.Writing and Reading a Zip File
这个例子类似于清单 8-7,只是在读写 tar 和 zip 格式文件的实现上有一些不同。当您运行程序时,您应该看到应用程序目录中的归档文件source.zip
作为写操作的输出,文件main.go
和readme.txt
的内容作为读操作的输出。
六、数据持久化
当您构建真实世界的应用程序时,您可能需要将应用程序数据保存到持久存储中。您可以使用各种 Go 类型定义应用程序的数据模型,尤其是结构。在大多数用例中,您可能需要将应用程序数据保存到数据库中。本章向您展示了如何将应用程序数据持久化到数据库中,如 MongoDB、RethinkDB、InfluxDB 和 PostgreSQL。MongoDB 是一个流行的 NoSQL 数据库,广泛用于许多现代应用程序。RethinkDB 是另一个带有实时功能的 NoSQL 数据库,允许您构建实时 web 应用程序。时间序列数据库正在成为数据管理技术中的下一个大事件,因此本章包括使用 InfluxDB 的方法,这是一个用 Go 编写的流行的时间序列数据库。本章还提供了使用传统 SQL 数据库的方法。
6-1.用 MongoDB 持久化数据
问题
您希望使用 MongoDB 作为 Go 应用程序的数据库。
解决办法
第三方包mgo
为 Go 提供了一个全功能的 MongoDB 驱动程序,它允许您从 Go 应用程序中使用 MongoDB。mgo
驱动程序已广泛用于生产 Go 应用。
它是如何工作的
MongoDB 是一个流行的 NoSQL 数据库,被广泛用作各种现代应用程序的数据库,包括 web 和移动应用程序。MongoDB 是一个开源文档数据库,它提供了高性能、高可用性和自动伸缩。MongoDB 将数据作为文档存储在一种称为二进制 JSON (BSON)的二进制表示中。简而言之,MongoDB 是 BSON 文档的数据存储。如果您想将 MongoDB 与关系数据库管理系统(RDBMS)进行比较,BSON 文档的集合类似于关系数据库中的数据库表,集合中的单个文档类似于关系数据库中的一行表。因为 MongoDB 将数据存储为文档,所以不能将集合与表进行比较。例如,您可以在一个文档中嵌入文档来实现父子关系,而您可以通过在关系数据库中指定外键来将数据保存在两个单独的表中。甚至 NoSQL 数据库也不支持其数据模型中的约束。像大多数 NoSQL 数据库一样,MongoDB 是一个无模式数据库,这意味着数据库在集合内的每个文档中可以有不同的字段集,并且每个字段可以有不同的类型。要获得关于 MongoDB 的更多细节,以及下载和安装的说明,请访问 MongoDB 网站 https://www.mongodb.org/
。
Note
NoSQL(通常不仅仅指 SQL)数据库提供了一种存储和检索数据的机制,它提供了一种设计数据模型的方法,而不是关系数据库中使用的表格关系。NoSQL 数据库旨在应对现代应用程序开发挑战,例如以更容易的可伸缩性和更好的性能处理大量数据。与关系数据库相比,NoSQL 数据库可以提供高性能、更好的可伸缩性和更便宜的存储。NoSQL 数据库有不同的类型:文档数据库、图形存储、键/值存储和宽列存储。
第三方包mgo
,发音为“mango”,提供了对使用 MongoDB 数据库的支持,它的子包bson
实现了使用 BSON 文档的 BSON 规范。诸如slice
、map
和struct
之类的 Go 类型的值可以保存到 MongoDB 中。当对 MongoDB 执行写操作时,包mgo
自动将 Go 类型的值序列化为 BSON 文档。在大多数用例中,您可以通过使用结构来定义您的数据模型,并对其执行 CRUD 操作。
安装 mgo
要安装软件包mgo
,运行以下命令:
go get gopkg.in/mgo.v2
这将获取包mgo
及其子包bson
。要使用mgo
包,您必须将gopkg.in/mgo.v2
添加到导入列表中。
import "gopkg.in/mgo.v2"
如果您想使用bson
包,您必须将gopkg.in/mgo.v2/bson
添加到导入列表中:
import (
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
正在连接到 MongoDB
要使用 MongoDB 执行 CRUD 操作,首先要使用函数Dial
获得一个 MongoDB 会话,如下所示:
session, err := mgo.Dial("localhost")
函数Dial
建立到由url
参数标识的 MongoDB 服务器集群的连接,并返回指向mgo.Session
的指针,该指针用于对 MongoDB 数据库执行 CRUD 操作。功能Dial
支持与服务器集群的连接,如下图所示:
session, err := mgo.Dial("server1.mongolab.com,server2.mongolab.com")
您还可以使用函数DialWithInfo
建立到一个或一个服务器集群的连接,该函数返回mgo.Session
。此函数允许您使用类型mgo.DialInfo
向服务器传递定制信息,如下所示:
mongoDialInfo := &mgo.DialInfo{
Addrs: []string{"localhost"},
Timeout: 60 * time.Second,
Database: "bookmarkdb",
Username: "shijuvar",
Password: "password123",
}
session, err := mgo.DialWithInfo(mongoDialInfo)
所有会话方法都是并发安全的,因此您可以从多个 goroutines 调用它们。根据通过mgo.Session
指定的一致性模式执行读取操作。会话的方法SetMode
用于改变会话对象的一致性模式。有三种类型的可用一致性模式:最终、单调和强。如果没有明确指定一致性模式,默认模式是强模式。在强一致性模式下,将始终使用唯一的连接对主服务器进行读取和写入,以便它们完全一致、有序,并观察最新的数据。
使用集合
MongoDB 将数据存储为文档,文档被组织成集合。CRUD 操作是针对一个集合执行的,该集合被映射到包mgo
中的类型mgo.Collection
。类型为mgo.Database
的方法C
用于创建一个mgo.Collection
对象。mgo.Database
类型表示 MongoDB 的命名数据库,它是通过调用类型mgo.Session
的方法DB
创建的。
下面的语句创建了一个指向mgo.Collection
的指针,它表示在"bookmarkdb"
数据库中名为"bookmarks"
的 MongoDB 集合。
collection := session.DB("bookmarkdb").C("bookmarks")
执行 CRUD 操作
一旦获得了一个Session
,就可以对一个Collection
值执行 CRUD 操作。让我们编写一个示例程序来演示针对Collection
值的持久化和读取操作。首先,我们在两个源文件中编写示例程序:bookmark_store.go
和main.go
。清单 6-1 显示了bookmark_store.go
文件的源代码,该文件包含一个用于定义数据模型的名为Bookmark
的结构,以及一个为执行 CRUD 操作提供持久化逻辑的结构类型BookmarkStore
。
package main
import (
"time"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
// Bookmark type represents the metadata of a bookmark.
type Bookmark struct {
ID bson.ObjectId `bson:"_id,omitempty"`
Name, Description, Location string
Priority int // Priority (1 -5)
CreatedOn time.Time
Tags []string
}
// BookmarkStore provides CRUD operations against the collection "bookmarks".
type BookmarkStore struct {
C *mgo.Collection
}
// Create inserts the value of struct Bookmark into collection.
func (store BookmarkStore) Create(b *Bookmark) error {
// Assign a new bson.ObjectId
b.ID = bson.NewObjectId()
err := store.C.Insert(b)
return err
}
//Update modifies an existing value of a collection.
func (store BookmarkStore) Update(b Bookmark) error {
// partial update on MogoDB
err := store.C.Update(bson.M{"_id": b.ID},
bson.M{"$set": bson.M{
"name": b.Name,
"description": b.Description,
"location": b.Location,
"priority": b.Priority,
"tags": b.Tags,
}})
return err
}
// Delete removes an existing value from the collection.
func (store BookmarkStore) Delete(id string) error {
err := store.C.Remove(bson.M{"_id": bson.ObjectIdHex(id)})
return err
}
// GetAll returns all documents from the collection.
func (store BookmarkStore) GetAll() []Bookmark {
var b []Bookmark
iter := store.C.Find(nil).Sort("priority", "-createdon").Iter()
result := Bookmark{}
for iter.Next(&result) {
b = append(b, result)
}
return b
}
// GetByID returns single document from the collection.
func (store BookmarkStore) GetByID(id string) (Bookmark, error) {
var b Bookmark
err := store.C.FindId(bson.ObjectIdHex(id)).One(&b)
return b, err
}
// GetByTag returns all documents from the collection filtering by tags.
func (store BookmarkStore) GetByTag(tags []string) []Bookmark {
var b []Bookmark
iter := store.C.Find(bson.M{"tags": bson.M{"$in": tags}}).Sort("priority", "-createdon").Iter()
result := Bookmark{}
for iter.Next(&result) {
b = append(b, result)
}
return b
}
Listing 6-1.Data Model and Persistence Logic in bookmark_store.go
名为Bookmark
的结构被声明为示例程序的数据模型。
type Bookmark struct {
ID bson.ObjectId `bson:"_id,omitempty"`
Name, Description, Location string
Priority int // Priority (1 -5)
CreatedOn time.Time
Tags []string
}
字段ID
的类型被指定为bson.ObjectId
,它是一个 12 字节的值,并且用 BSON 表示法中的_id
映射这个字段。当您插入一个文档时,您需要为字段_id
提供一个唯一的值ObjectId
,作为主键。如果在插入操作期间,文档在其根级别(顶层字段)中不包含字段_id
,则mgo
驱动程序通过提供唯一值ObjectId
来添加字段_id
。
名为BookmarkStore
的结构被声明用于提供持久化逻辑,该逻辑使用结构Bookmark;
进行插入和更新操作,它接受值it
,对于读取操作,它返回相同类型的值。结构BookmarkStore
有一个带type mgo.Collection
的字段C
。通过访问字段C
执行所有 CRUD 操作。从源文件main.go
(见清单 6-2),Collection
对象被提供给BookmarkStore
值,并通过访问BookmarkStore
的方法执行 CRUD 操作。
type BookmarkStore struct {
C *mgo.Collection
}
在集合中创建文档
BookmarkStore
的方法Create
用于将值插入名为"bookmarks"
的 MongoDB 集合中。它接受指向Bookmark
的指针,并使用Collection
的Insert
方法将Bookmark
的值插入到 MongoDB 集合中。当执行插入操作时,包mgo
自动将 Go 类型的值编码成 BSON 规范。
func (store BookmarkStore) Create(b *Bookmark) error {
// Assign a new bson.ObjectId
b.ID = bson.NewObjectId()
err := store.C.Insert(b)
return err
}
通过调用函数bson.NewObjectId
生成唯一值ObjectId
,并将其分配给字段ID
,该字段在 BSON 文档中标记为字段_i
d。类型Collection
的函数Insert
用于将文档插入到集合中。
更新集合中的文档
类型为Collection
的函数Update
用于更新现有文档。方法Update
从集合中查找与所提供的选择器文档匹配的单个文档,并用所提供的值修改该文档。关键字"$set"
用于对文档进行部分更新。
func (store BookmarkStore) Update(b Bookmark) error {
// partial update on MogoDB
err := store.C.Update(bson.M{"_id": b.ID},
bson.M{"$set": bson.M{
"name": b.Name,
"description": b.Description,
"location": b.Location,
"priority": b.Priority,
"tags": b.Tags,
}})
return err
}
类型bson.M
用于为Collection
的方法Update
提供值。这个类型是带有map[string]interface{}
签名的类型map
的一个方便的别名,对于以本地方式处理 BSON 很有用。每当您想以本机方式处理 BSON 文档时,可以提供bson.M
的值,这对Collection
对象的Update
、Read
和Delete
操作很有用。
从集合中删除文档
Collection
的Remove
功能用于从集合中删除文档。这里的文档是为给定的id
移除的。
func (store BookmarkStore) Delete(id string) error {
err := store.C.Remove(bson.M{"_id": bson.ObjectIdHex(id)})
return err
}
从集合中读取文档
BookmarkStore
的方法GetAll
返回集合中的所有文档。Collection
的Find
方法用于从集合中查询文档。方法Find
返回一个指向mgo.Query
的指针,稍后可以使用函数One
、For
、Iter
或Tail
来检索文档。
func (store BookmarkStore) GetAll() []Bookmark {
var b []Bookmark
iter := store.C.Find(nil).Sort("priority", "-createdon").Iter()
result := Bookmark{}
for iter.Next(&result) {
b = append(b, result)
}
return b
}
一个nil
值作为选择器文档被提供给方法Find
以从集合中获取所有文档。产生的mgo.Query
值表示对给定选择器文档执行的结果集。使用函数Sort
,得到的Query
值可用于根据字段值对文档进行排序。这里,对字段priority
按升序进行排序操作,对createdon
按降序进行排序操作。要按降序排序,只需将"-"
作为字段名的前缀,如下所示的字段createdon
。
iter := store.C.Find(nil).Sort("priority", "-createdon").Iter()
Query
的方法Iter
返回一个迭代器,能够迭代所有生成的结果,函数Next
从结果集中检索下一个文档。
BookmarkStore
的方法GetByID
为 BSON 文档中给定的id (_id
返回单个文档。这里的id
是作为string
提供的,因此使用函数bson.ObjectIdHex
将其转换为bson.ObjectId
。
func (store BookmarkStore) GetByID(id string) (Bookmark, error) {
var b Bookmark
err := store.C.FindId(bson.ObjectIdHex(id)).One(&b)
return b, err
}
在这个例子中,Collection
中的文档也被查询给定的标签,作为string
的slice
,它返回与给定标签匹配的文档。
func (store BookmarkStore) GetByTag(tags []string) []Bookmark {
var b []Bookmark
iter := store.C.Find(bson.M{"tags": bson.M{"$in": tags}}).Sort("priority", "-createdon").Iter()
result := Bookmark{}
for iter.Next(&result) {
b = append(b, result)
}
return b
}
查询操作符$in
允许您使用匹配值列表中任何值的表达式来过滤文档。这里的$in
操作符用于过滤tag
字段中的文档。这里,如果任何给定的标签与字段tags
匹配,查询将返回所有文档。
让我们重用bookmark_store.go
的函数对 MongoDB 数据库执行 CRUD 操作。清单 6-2 显示了源文件main.go
,它通过提供一个mgo.Collection
值并调用其方法来创建一个BookmarkStore
类型的实例。
package main
import (
"fmt"
"log"
"time"
"gopkg.in/mgo.v2"
)
var store BookmarkStore
var id string
// init will invoke before the function main.
func init() {
session, err := mgo.DialWithInfo(&mgo.DialInfo{
Addrs: []string{"127.0.0.1"},
Timeout: 60 * time.Second,
})
if err != nil {
log.Fatalf("[MongoDB Session]: %s\n", err)
}
collection := session.DB("bookmarkdb").C("bookmarks")
store = BookmarkStore{
C: collection,
}
}
// Create and update documents.
func createUpdate() {
bookmark := Bookmark{
Name: "mgo",
Description: "Go driver for MongoDB",
Location: "https://github.com/go-mgo/mgo",
Priority: 2,
CreatedOn: time.Now(),
Tags: []string{"go", "nosql", "mongodb"},
}
// Insert a new document.
if err := store.Create(&bookmark); err != nil {
log.Fatalf("[Create]: %s\n", err)
}
id = bookmark.ID.Hex()
fmt.Printf("New bookmark has been inserted with ID: %s\n", id)
// Update an existing document.
bookmark.Priority = 1
if err := store.Update(bookmark); err != nil {
log.Fatalf("[Update]: %s\n", err)
}
fmt.Println("The value after update:")
// Retrieve the updated document
getByID(id)
bookmark = Bookmark{
Name: "gorethink",
Description: "Go driver for RethinkDB",
Location: "https://github.com/dancannon/gorethink",
Priority: 3,
CreatedOn: time.Now(),
Tags: []string{"go", "nosql", "rethinkdb"},
}
// Insert a new document.
if err := store.Create(&bookmark); err != nil {
log.Fatalf("[Create]: %s\n", err)
}
id = bookmark.ID.Hex()
fmt.Printf("New bookmark has been inserted with ID: %s\n", id)
}
// Get a document by given id.
func getByID(id string) {
bookmark, err := store.GetByID(id)
if err != nil {
log.Fatalf("[GetByID]: %s\n", err)
}
fmt.Printf("Name:%s, Description:%s, Priority:%d\n",
bookmark.Name, bookmark.Description, bookmark.Priority)
}
// Get all documents from the collection.
func getAll() {
// Layout for formatting dates.
layout := "2006-01-02 15:04:05"
// Retrieve all documents.
bookmarks := store.GetAll()
fmt.Println("Read all documents")
for _, v := range bookmarks {
fmt.Printf("Name:%s, Description:%s, Priority:%d, CreatedOn:%s\n",
v.Name, v.Description, v.Priority, v.CreatedOn.Format(layout))
}
}
// Get documents by tags.
func getByTags() {
layout := "2006-01-02 15:04:05"
fmt.Println("Query with Tags - 'go, nosql'")
bookmarks := store.GetByTag([]string{"go", "nosql"})
for _, v := range bookmarks {
fmt.Printf("Name:%s, Description:%s, Priority:%d, CreatedOn:%s\n",
v.Name, v.Description, v.Priority, v.CreatedOn.Format(layout))
}
fmt.Println("Query with Tags - 'mongodb'")
bookmarks = store.GetByTag([]string{"mongodb"})
for _, v := range bookmarks {
fmt.Printf("Name:%s, Description:%s, Priority:%d, CreatedOn:%s\n",
v.Name, v.Description, v.Priority, v.CreatedOn.Format(layout))
}
}
// Delete an existing document from the collection.
func delete() {
if err := store.Delete(id); err != nil {
log.Fatalf("[Delete]: %s\n", err)
}
bookmarks := store.GetAll()
fmt.Printf("Number of documents in the collection after delete:%d\n", len(bookmarks))
}
// main - entry point of the program.
func main() {
createUpdate()
getAll()
getByTags()
delete()
}
Listing 6-2.Perform CRUD Operations on a MongoDB Collection by Using the Type BookmarkStore, in main.go
在调用函数main
之前执行的函数init
中,使用函数DialWithInfo
获得一个mgo.Sessi
on 值,然后创建一个mgo.Collection
值以提供类型BookmarkStore
。BookmarkStore
的值用于对数据库"bookmarkdb"
中名为"bookmarks"
的集合执行 CRUD 操作。
var store BookmarkStore
var id string
func init() {
session, err := mgo.DialWithInfo(&mgo.DialInfo{
Addrs: []string{"127.0.0.1"},
Timeout: 60 * time.Second,
})
if err != nil {
log.Fatalf("[MongoDB Session]: %s\n", err)
}
collection := session.DB("bookmarkdb").C("bookmarks")
store = BookmarkStore{
C: collection,
}
}
创建和更新操作在函数createUpdate
中实现,其中两个文档被插入到集合中,一个现有文档被更新。
func createUpdate() {
bookmark := Bookmark{
Name: "mgo",
Description: "Go driver for MongoDB",
Location: "https://github.com/go-mgo/mgo",
Priority: 2,
CreatedOn: time.Now(),
Tags: []string{"go", "nosql", "mongodb"},
}
// Insert a new document.
if err := store.Create(&bookmark); err != nil {
log.Fatalf("[Create]: %s\n", err)
}
id = bookmark.ID.Hex()
fmt.Printf("New bookmark has been inserted with ID: %s\n", id)
// Update an existing document.
bookmark.Priority = 1
if err := store.Update(bookmark); err != nil {
log.Fatalf("[Update]: %s\n", err)
}
fmt.Println("The value after update:")
// Retrieve the updated document.
getByID(id)
bookmark = Bookmark{
Name: "gorethink",
Description: "Go driver for RethinkDB",
Location: "https://github.com/dancannon/gorethink",
Priority: 3,
CreatedOn: time.Now(),
Tags: []string{"go", "nosql", "rethinkdb"},
}
// Insert a new document.
if err := store.Create(&bookmark); err != nil {
log.Fatalf("[Create]: %s\n", err)
}
id = bookmark.ID.Hex()
fmt.Printf("New bookmark has been inserted with ID: %s\n", id)
}
函数getByID
用于通过给定的id
检索现有文档。这个函数是从函数createUpdate
中调用的,以获取更新操作后的值。
func getByID(id string) {
bookmark, err := store.GetByID(id)
if err != nil {
log.Fatalf("[GetByID]: %s\n", err)
}
fmt.Printf("Name:%s, Description:%s, Priority:%d\n", bookmark.Name, bookmark.Description, bookmark.Priority)
}
函数getAll
从集合中检索所有文档,分别按照priority
升序和createdon
降序排序。
func getAll() {
// Layout for formatting dates.
layout := "2006-01-02 15:04:05"
// Retrieve all documents.
bookmarks := store.GetAll()
fmt.Println("Read all documents")
for _, v := range bookmarks {
fmt.Printf("Name:%s, Description:%s, Priority:%d, CreatedOn:%s\n",
v.Name, v.Description, v.Priority, v.CreatedOn.Format(layout))
}
}
函数getByTags
通过使用tags
进行过滤来检索文档。MongoDB 查询操作符$in
用于过滤文档。BookmarkStore
的功能GetByTag
执行两次。第一次,它是通过提供标签、go
和nosql,
来执行的,因此您将获得提供了任何标签的所有文档;在这里你会得到两份文件。第二次,它通过提供标签mongodb
来执行,因此您将得到一个文档作为结果,因为只有一个文档具有给定的标签。
func getByTags() {
layout := "2006-01-02 15:04:05"
fmt.Println("Query with Tags - 'go, nosql'")
bookmarks := store.GetByTag([]string{"go", "nosql"})
for _, v := range bookmarks {
fmt.Printf("Name:%s, Description:%s, Priority:%d, CreatedOn:%s\n",
v.Name, v.Description, v.Priority, v.CreatedOn.Format(layout))
}
fmt.Println("Query with Tags - 'mongodb'")
bookmarks = store.GetByTag([]string{"mongodb"})
for _, v := range bookmarks {
fmt.Printf("Name:%s, Description:%s, Priority:%d, CreatedOn:%s\n",
v.Name, v.Description, v.Priority, v.CreatedOn.Format(layout))
}
}
函数delete
用于通过给定的id
删除已有的文档。
func delete() {
if err := store.Delete(id); err != nil {
log.Fatalf("[Delete]: %s\n", err)
}
bookmarks, err := store.GetAll()
if err != nil {
log.Fatalf("[GetAll]: %s\n", err)
}
fmt.Printf("Number of documents in the table after delete:%d\n", len(bookmarks))
}
从函数main
中,调用函数来演示 CRUD 操作。
func main() {
createUpdate()
getAll()
getByTags()
delete()
}
让我们运行示例程序。您应该会看到类似如下的输出:
New bookmark has been inserted with ID: 57809514f7e02124b042281d
The value after update:
Name:mgo, Description:Go driver for MongoDB, Priority:1
New bookmark has been inserted with ID: 57809514f7e02124b042281e
Read all documents
Name:mgo, Description:Go driver for MongoDB, Priority:1, CreatedOn:2016-07-09 11:39:24
Name:gorethink, Description:Go driver for RethinkDB, Priority:3, CreatedOn:2016-07-09 11:39:24
Query with Tags - 'go, nosql'
Name:mgo, Description:Go driver for MongoDB, Priority:1, CreatedOn:2016-07-09 11:39:24
Name:gorethink, Description:Go driver for RethinkDB, Priority:3, CreatedOn:2016-07-09 11:39:24
Query with Tags - 'mongodb'
Name:mgo, Description:Go driver for MongoDB, Priority:1, CreatedOn:2016-07-09 11:39:24
Number of documents in the collection after delete:1
6-2.用 RethinkDB 保存数据
问题
您希望使用 RethinkDB 作为 Go 应用程序的数据库。您还想使用 RethinkDB 的实时功能。
解决办法
第三方软件包gorethink
为 Go 提供了一个全功能的 RethinkDB 驱动程序,允许您从 Go 应用程序中使用 RethinkDB。该软件包还允许您使用 RethinkDB 实时订阅和更改数据馈送。
它是如何工作的
RethinkDB 是一个 NoSQL 的、可伸缩的 JSON 数据库,它提供了许多类似于 MongoDB 的功能。RethinkDB 将 JSON 文档组织成表格存储。RethinkDB 中的一个Table
是 JSON 文档的集合。除了人们熟悉的 NoSQL 数据库的功能之外,RethinkDB 还为其数据库引擎提供了实时功能,从而大大简化了实时 web 应用程序的构建。实时 web 应用程序可以将实时更新推送到客户端应用程序,而不是客户端应用程序定期检查服务器的新更新。当您编写实时应用程序时,这可以提高工作效率。使用 WebSocket 协议的 Go 实现,您可以使您的 web 应用程序成为实时应用程序。通过将这一点与 RethinkDB 数据库的实时功能相结合,您可以创建优秀的实时 web 应用程序。当您的实时 web 应用程序使用 RethinkDB 时,您可以订阅实时变更提要;当数据库中有任何变化时,您可以将这些变化推送到您的客户端应用程序。有关 RethinkDB 的更多详细信息,包括安装说明,请访问网站 https://www.rethinkdb.com/
。
Note
Go 包golang.org/x/net/websocket
和github.com/gorilla/websocket
实现了 RFC 6455 ( https://tools.ietf.org/html/rfc6455
)中指定的 WebSocket 协议的客户端和服务器。
安装 gorethink
要安装软件包gorethink
,运行以下命令:
go get github.com/dancannon/gorethink
要使用包gorethink
,您必须将github.com/dancannon/gorethink
添加到导入列表中。
import " github.com/dancannon/gorethink"
连接到 RethinkDB
要使用 RethinkDB 执行 CRUD 操作,首先要使用函数Connect
获得一个 RethinkDB 会话,如下所示:
session, err := gorethink.Connect(r.ConnectOpts{
Address: "localhost:28015",
Database: "bookmarkdb",
})
要配置连接池,可以在调用函数Connect
时指定ConnectOpts
类型的属性,如MaxIdle
、MaxOpen
和Timeout
,如下所示:
session, err := gorethink.Connect(gorethink.ConnectOpts{
Address: "localhost:28015",
Database: "bookmarkdb",
MaxIdle: 10,
MaxOpen: 10,
})
您可以通过调用Session
的方法来更改MaxIdle
和MaxOpen
属性,如下所示:
session.SetMaxIdleConns(57)
session.SetMaxOpenConns(5)
要连接到具有多个节点的 RethinkDB 服务器群集,可以使用以下语法。当连接到具有多个节点的集群时,查询分布在这些节点中。
session, err := gorethink.Connect(gorethink.ConnectOpts{
Addresses: []string{"localhost:28015", "localhost:28016"},
Database: " bookmarkdb",
AuthKey: "14daak1cad13dj",
DiscoverHosts: true,
})
AuthKey
用于保护 RethinkDB 集群。
执行 CRUD 操作
一旦获得了一个Session
对象,就可以对代表 JSON 文档集合的Table
执行 CRUD 操作。
让我们编写一个示例程序来演示使用 RethinkDB 的持久化和读操作。让我们在两个源文件中编写示例程序:bookmark_store.go
和main.go
。清单 6-3 显示了bookmark_store.go
文件的源代码,该文件包含一个用于定义数据模型的名为Bookmark
的结构,以及一个为针对名为"bookmarks"
的表执行 CRUD 操作提供持久化逻辑的结构类型BookmarkStore
。
package main
import (
"time"
r "github.com/dancannon/gorethink"
)
// Bookmark type represents the metadata of a bookmark.
type Bookmark struct {
ID string `gorethink:"id,omitempty" json:"id"`
Name, Description, Location string
Priority int // Priority (1 -5)
CreatedOn time.Time
Tags []string
}
// BookmarkStore provides CRUD operations against the Table "bookmarks".
type BookmarkStore struct {
Session *r.Session
}
// Create inserts the value of struct Bookmark into Table.
func (store BookmarkStore) Create(b *Bookmark) error {
resp, err := r.Table("bookmarks").Insert(b).RunWrite(store.Session)
if err == nil {
b.ID = resp.GeneratedKeys[0]
}
return err
}
// Update modifies an existing value of a Table.
func (store BookmarkStore) Update(b *Bookmark) error {
var data = map[string]interface{}{
"name": b.Name,
"description": b.Description,
"location": b.Location,
"priority": b.Priority,
"tags": b.Tags,
}
// partial update on RethinkDB
_, err := r.Table("bookmarks").Get(b.ID).Update(data).RunWrite(store.Session)
return err
}
// Delete removes an existing value from the Table.
func (store BookmarkStore) Delete(id string) error {
_, err := r.Table("bookmarks").Get(id).Delete().RunWrite(store.Session)
return err
}
// GetAll returns all documents from the Table.
func (store BookmarkStore) GetAll() ([]Bookmark, error) {
bookmarks := []Bookmark{}
res, err := r.Table("bookmarks").OrderBy("priority", r.Desc("date")).Run(store.Session)
err = res.All(&bookmarks)
return bookmarks, err
}
// GetByID returns single document from the Table.
func (store BookmarkStore) GetByID(id string) (Bookmark, error) {
var b Bookmark
res, err := r.Table("bookmarks").Get(id).Run(store.Session)
res.One(&b)
return b, err
}
Listing 6-3.Data Model and Persistence Logic in bookmark_store.go
名为Bookmark
的结构被声明为示例程序的数据模型。
type Bookmark struct {
ID string `gorethink:"id,omitempty" json:"id"`
Name, Description, Location string
Priority int // Priority (1 -5)
CreatedOn time.Time
Tags []string
}
结构字段ID
在文档的 JSON 表示中用 RethinkDB Table,
的id
和id
标记。RethinkDB 将为字段id
自动生成一个UUID
。
名为BookmarkStore
的结构被声明用于提供使用数据模型结构Bookmark
的持久化逻辑。结构BookmarkStore
有一个类型为gorethink.Session
的字段Session
。所有 CRUD 操作都是通过访问使用字段Session
的BookmarkStore
的方法来执行的。
type BookmarkStore struct {
Session *r.Session
}
在表格中创建文档
BookmarkStore
的方法Create
用于将值插入名为bookmarks
的表中。它接受一个指向Bookmark
的指针,并将Bookmark
的值插入表中。当执行插入和更新操作时,gorethink
包在发送到服务器之前将结构值编码到一个map
中。
func (store BookmarkStore) Create(b *Bookmark) error {
resp, err := r.Table("bookmarks").Insert(b).RunWrite(store.Session)
if err == nil {
b.ID = resp.GeneratedKeys[0]
}
return err
}
类型gorethink.Term
表示写和读查询。在包gorethink
中,方法是可链接的,因此您可以轻松地构造查询。在前面的方法中,函数Table
和方法Insert
返回一个gorethink.Term
值。函数RunWrite
运行一个查询,然后返回一个WriteResponse
类型的值。通过访问WriteResponse
值的GeneratedKeys
字段,可以得到id
值。函数RunWrite
用于执行写查询,如Insert
、Update
、Delete
、DBCreate
、TableCreate
等。
更新表格中的文档
为了更新一个现有的文档,提供一个带有签名map[string]interface{}
的值map
作为表中要更新的值。
func (store BookmarkStore) Update(b *Bookmark) error {
var data = map[string]interface{}{
"name": b.Name,
"description": b.Description,
"location": b.Location,
"priority": b.Priority,
"tags": b.Tags,
}
// partial update on RethinkDB
_, err := r.Table("bookmarks").Get(b.ID).Update(data).RunWrite(store.Session)
return err
}
从表格中删除文档
类型为Term
的方法Delete
用于运行删除查询,从表中删除现有文档。
func (store BookmarkStore) Delete(id string) error {
_, err := r.Table("bookmarks").Get(id).Delete().RunWrite(store.Session)
return err
}
从桌上阅读文件
函数Run
用于运行读取查询。函数Run
返回一个作为查询结果的gorethink.Cursor
值。通过使用One
、All
、Next
和NextResponse
等方法,您可以将文档检索到您的 Go 类型中。类型为BookmarkStore
的方法GetAll
从bookmarks
表中返回所有文档,该表按priority
升序和createdon
降序排序。默认情况下,排序是按升序执行的,所以如果您想按降序排序,可以使用函数Desc
。
func (store BookmarkStore) GetAll() ([]Bookmark, error) {
bookmarks := []Bookmark{}
res, err := r.Table("bookmarks").OrderBy("priority", r.Desc("createdon")).Run(store.Session)
err = res.All(&bookmarks)
return bookmarks, err
}
类型为BookmarkStore
的方法GetByID
为给定的id
返回一个文档。
func (store BookmarkStore) GetByID(id string) (Bookmark, error) {
var b Bookmark
res, err := r.Table("bookmarks").Get(id).Run(store.Session)
res.One(&b)
return b, err
}
让我们重用bookmark_store.go
的函数来对 RethinkDB Table
执行 CRUD 操作。清单 6-4 显示了main.go
文件中的源代码,它通过提供一个gorethink.Session
值来创建一个BookmarkStore
类型的实例,并调用其方法来执行 CRUD 操作。这个main.go
还通过订阅表的变更提要来提供 RethinkDB 实时功能的实现。
package main
import (
"fmt"
"log"
"time"
r "github.com/dancannon/gorethink"
)
var store BookmarkStore
var id string
// initDB creates new database and
func initDB(session *r.Session) {
var err error
// Create Database
_, err = r.DBCreate("bookmarkdb").RunWrite(session)
if err != nil {
log.Fatalf("[initDB]: %s\n", err)
}
// Create Table
_, err = r.DB("bookmarkdb").TableCreate("bookmarks").RunWrite(session)
if err != nil {
log.Fatalf("[initDB]: %s\n", err)
}
}
// changeFeeds subscribes real-time changes on table bookmarks.
func changeFeeds(session *r.Session) {
bookmarks, _ := r.Table("bookmarks").Changes().Field("new_val").Run(session)
if err != nil {
log.Fatalf("[changeFeeds]: %s\n", err)
}
// Launch a goroutine to print real-time updates.
go func() {
var bookmark Bookmark
for bookmarks.Next(&bookmark) {
if bookmark.ID == "" { // for delete, new_val will be null.
fmt.Println("Real-time update: Document has been deleted")
} else {
fmt.Printf("Real-time update: Name:%s, Description:%s, Priority:%d\n",
bookmark.Name, bookmark.Description, bookmark.Priority)
}
}
}()
}
// init will invoke before the function main
func init() {
session, err := r.Connect(r.ConnectOpts{
Address: "localhost:28015",
Database: "bookmarkdb",
MaxIdle: 10,
MaxOpen: 10,
})
if err != nil {
log.Fatalf("[RethinkDB Session]: %s\n", err)
}
// Create Database and Table.
initDB(session)
store = BookmarkStore{
Session: session,
}
// Subscribe real-time changes
changeFeeds(session)
}
// Create and update documents.
func createUpdate() {
bookmark := Bookmark{
Name: "mgo",
Description: "Go driver for MongoDB",
Location: "https://github.com/go-mgo/mgo",
Priority: 1,
CreatedOn: time.Now(),
Tags: []string{"go", "nosql", "mongodb"},
}
// Insert a new document.
if err := store.Create(&bookmark); err != nil {
log.Fatalf("[Create]: %s\n", err)
}
id = bookmark.ID
fmt.Printf("New bookmark has been inserted with ID: %s\n", id)
// Update an existing document.
bookmark.Priority = 2
if err := store.Update(bookmark); err != nil {
log.Fatalf("[Update]: %s\n", err)
}
fmt.Println("The value after update:")
// Retrieve the updated document.
getByID(id)
bookmark = Bookmark{
Name: "gorethink",
Description: "Go driver for RethinkDB",
Location: "https://github.com/dancannon/gorethink",
Priority: 1,
CreatedOn: time.Now(),
Tags: []string{"go", "nosql", "rethinkdb"},
}
// Insert a new document.
if err := store.Create(&bookmark); err != nil {
log.Fatalf("[Create]: %s\n", err)
}
id = bookmark.ID
fmt.Printf("New bookmark has been inserted with ID: %s\n", id)
}
// Get a document by given id.
func getByID(id string) {
bookmark, err := store.GetByID(id)
if err != nil {
log.Fatalf("[GetByID]: %s\n", err)
}
fmt.Printf("Name:%s, Description:%s, Priority:%d\n", bookmark.Name, bookmark.Description, bookmark.Priority)
}
// Get all documents from bookmarks table.
func getAll() {
// Layout for formatting dates.
layout := "2006-01-02 15:04:05"
// Retrieve all documents.
bookmarks, err := store.GetAll()
if err != nil {
log.Fatalf("[GetAll]: %s\n", err)
}
fmt.Println("Read all documents")
for _, v := range bookmarks {
fmt.Printf("Name:%s, Description:%s, Priority:%d, CreatedOn:%s\n", v.Name, v.Description, v.Priority, v.CreatedOn.Format(layout))
}
}
// Delete an existing document from bookmarks table.
func delete() {
if err := store.Delete(id); err != nil {
log.Fatalf("[Delete]: %s\n", err)
}
bookmarks, err := store.GetAll()
if err != nil {
log.Fatalf("[GetAll]: %s\n", err)
}
fmt.Printf("Number of documents in the table after delete:%d\n", len(bookmarks))
}
// main - entry point of the program
func main() {
createUpdate()
getAll()
delete()
}
Listing 6-4.Perform CRUD Operations on a RethinkDB Table Using the Type BookmarkStore, in main.go
在init
函数中,通过使用函数Connect
连接到 RethinkDB 服务器来获得一个Session
值。与 MongoDB 不同,在 RethinkDB 中,您必须手动创建数据库和表。从 Go 代码本身,通过调用函数initDB
创建一个名为bookmarkdb
的数据库和一个名为bookmarks
的表。如果你多次执行函数initDB
,你会得到一个异常。用于演示 RethinkDB 实时功能的函数changeFeeds
也是从init
中调用的。我们将在本节稍后研究函数changeFeeds
。函数init
将在函数main
之前被调用。
func init() {
session, err := r.Connect(r.ConnectOpts{
Address: "localhost:28015",
Database: "bookmarkdb",
MaxIdle: 10,
MaxOpen: 10,
})
if err != nil {
log.Fatalf("[RethinkDB Session]: %s\n", err)
}
// Create Database and Table.
initDB(session)
store = BookmarkStore{
Session: session,
}
// Subscribe real-time changes
changeFeeds(session)
}
创建和更新操作在函数createUpdate
中实现,其中两个文档被插入到bookmarks
表中,一个现有文档被更新。
func createUpdate() {
bookmark := Bookmark{
Name: "mgo",
Description: "Go driver for MongoDB",
Location: "https://github.com/go-mgo/mgo",
Priority: 1,
CreatedOn: time.Now(),
Tags: []string{"go", "nosql", "mongodb"},
}
// Insert a new document.
if err := store.Create(&bookmark); err != nil {
log.Fatalf("[Create]: %s\n", err)
}
id = bookmark.ID
fmt.Printf("New bookmark has been inserted with ID: %s\n", id)
// Update an existing document.
bookmark.Priority = 2
if err := store.Update(bookmark); err != nil {
log.Fatalf("[Update]: %s\n", err)
}
fmt.Println("The value after update:")
// Retrieve the updated document.
getByID(id)
bookmark = Bookmark{
Name: "gorethink",
Description: "Go driver for RethinkDB",
Location: "https://github.com/dancannon/gorethink",
Priority: 1,
CreatedOn: time.Now(),
Tags: []string{"go", "nosql", "rethinkdb"},
}
// Insert a new document.
if err := store.Create(&bookmark); err != nil {
log.Fatalf("[Create]: %s\n", err)
}
id = bookmark.ID
fmt.Printf("New bookmark has been inserted with ID: %s\n", id)
}
函数getByID
用于通过给定的id
检索现有文档。这个函数从函数createUpdate
中被调用,以在更新操作后获取值。
func getByID(id string) {
bookmark, err := store.GetByID(id)
if err != nil {
log.Fatalf("[GetByID]: %s\n", err)
}
fmt.Printf("Name:%s, Description:%s, Priority:%d\n", bookmark.Name, bookmark.Description, bookmark.Priority)
}
函数getAll
分别从按照priority
升序和createdon
降序排序的表中检索所有文档。
func getAll() {
// Layout for formatting dates.
layout := "2006-01-02 15:04:05"
// Retrieve all documents.
bookmarks, err := store.GetAll()
if err != nil {
log.Fatalf("[GetAll]: %s\n", err)
}
fmt.Println("Read all documents")
for _, v := range bookmarks {
fmt.Printf("Name:%s, Description:%s, Priority:%d, CreatedOn:%s\n", v.Name, v.Description, v.Priority, v.CreatedOn.Format(layout))
}
}
函数delete
用于通过给定的id
删除现有的文档。
func delete() {
if err := store.Delete(id); err != nil {
log.Fatalf("[Delete]: %s\n", err)
}
bookmarks, err := store.GetAll()
if err != nil {
log.Fatalf("[GetAll]: %s\n", err)
}
fmt.Printf("Number of documents in the table after delete:%d\n", len(bookmarks))
}
从函数main
中,调用函数来演示 CRUD 操作。
func main() {
createUpdate()
getAll()
delete()
}
RethinkDB 中的更改源
RethinkDB 的实时功能是使用Changefeeds
实现的,它允许 RethinkDB 数据库的客户端实时接收对表的更改。使用gorethink
驱动程序,您可以通过对Table
值调用函数Changes
来订阅变更数据的提要。在清单 6-4 中,main.go
中的函数changeFeeds
实现了 RethinkDB 的Changefeeds
来订阅表bookmarks
上的数据,以便应用程序可以在表bookmarks
上执行任何插入、更新或删除操作时接收这些提要。
func changeFeeds(session *r.Session) {
bookmarks, _ := r.Table("bookmarks").Changes().Field("new_val").Run(session)
if err != nil {
log.Fatalf("[changeFeeds]: %s\n", err)
}
// Launch a goroutine to print real-time updates.
go func() {
var bookmark Bookmark
for bookmarks.Next(&bookmark) {
if bookmark.ID == "" { // for delete, new_val will be null.
fmt.Println("Real-time update: Document has been deleted")
} else {
fmt.Printf("Real-time update: Name:%s, Description:%s, Priority:%d\n",
bookmark.Name, bookmark.Description, bookmark.Priority)
}
}
}()
}
调用函数Changes
来订阅字段new_val
的重新思考数据库的Changefeeds
。当对表执行任何更新时,Changefeeds
功能可以提供两个值:old_val
和new_val
。old_val
是文档的旧版本,而new_val
是文档的新版本。在 insert 上,old_val
将是null
;在删除时,new_val
将成为null
。更新时,old_val
和new_val
都存在。在功能changeFeeds
中,订阅了new_val
。可以在处理函数中订阅Changefeeds
的输出,以对Changefeeds
提供的值执行操作。这里,处理函数是在一个 goroutine 中实现的,因此它将在后台异步执行,而不会阻塞任何执行。通过与 goroutines 和 channels 结合,您可以使用 Go 和 RethinkDB 创建高效的实时应用程序。这里,控制台窗口中打印出字段new_val
结果的Changefeeds
。对于删除操作,new_val
将是null
,这样您就不会在删除操作中访问Changefeeds
的任何值。当对表bookmarks
执行任何插入、更新或删除操作时,Changefeeds
功能将提供提要。通过执行Run
,可从函数Changes
提供的Cursor
值访问Changefeeds
的new_val
值。通过调用Cursor
value 的函数Next
,可以检索Changefeeds
提供的值。
让我们运行清单 6-4 中编写的程序。您应该会看到类似如下的输出:
Real-time update: Name:mgo, Description:Go driver for MongoDB, Priority:1
New bookmark has been inserted with ID: f487b133-6f19-4b3b-8dfa-4d652b2f1c1b
Real-time update: Name:mgo, Description:Go driver for MongoDB, Priority:2
The value after update:
Name:mgo, Description:Go driver for MongoDB, Priority:2
Real-time update: Name:gorethink, Description:Go driver for RethinkDB, Priority:1
New bookmark has been inserted with ID: ee6a19c8-efa5-4672-ae62-37d8b0ea060f
Read all documents
Name:gorethink, Description:Go driver for RethinkDB, Priority:1, CreatedOn:2016-07-08 20:03:50
Name:mgo, Description:Go driver for MongoDB, Priority:2, CreatedOn:2016-07-08 20:03:49
Real-time update: Document has been deleted
Number of documents in the table after delete:1
输出显示Changefeeds
功能提供了对表书签的实时更新。
6-3.使用 InfluxDB 处理时间序列数据
问题
您希望使用时间序列数据来构建时间序列图表和实时数据分析。
解决办法
InfluxDB 是一个用 Go 编写的时间序列数据库。InfluxDB 提供了一个本地 Go 客户端库(github.com/influxdata/influxdb/client/v2
)来处理来自 Go 应用程序的 InfluxDB。
它是如何工作的
时间序列数据处理和实时数据分析是大数据和数据管理技术的下一件大事。InfluxDB 是 InfluxData 平台的一部分,是一个时间序列数据库,允许您有效地存储时间序列数据。InfluxDB 包括一个本地 Go 客户端库,它提供了读写时间序列数据的便利函数。它使用 HTTP 协议与您的 InfluxDB 集群通信。
时间序列数据库
时间序列数据是一系列数据点,通常由一段时间间隔内的连续测量值组成。当您基于时间序列数据构建图表时,其中一个轴将始终是时间(年、日、小时、分钟)。时间序列数据处理是建立预测模型和预测的重要数据管理方法。时序数据库(TSDB)是一种用于管理和存储时序数据的数据库。InfluxDB 由 InfluxData 平台提供,是市场上最流行的 TSDBs 之一。
主要概念 InfluxDB
InfluxDB 中的数据管理不同于传统的数据管理系统。以下是 InfluxDB 中关键概念的总结:
- 数据库:InfluxDB 中的高层实体。一个 InfluxDB 实例中可以有多个数据库。
- 度量:将时间序列数据保存到度量中。度量类似于关系表。当您基于时间序列数据构建图形时,测量值就是图形的名称。
- 点:度量包含点,就像关系表包含记录一样。点包含强制字段和时间戳。时间戳指定该点的时间,并且字段用于存储该时间戳的数据。一个点可以有标签,标签是时间序列数据的元数据。
- 时间戳:度量中的每个点都包含一个时间戳,因为 InfluxDB 是一个 TSDB。如果在创建新点时没有提供时间戳,InfluxDB 会自动为该点创建一个新的时间戳。点中的时间戳指定了它的创建时间。构建图表时,一个轴是时间,另一个轴是字段的值。
- 字段集:字段的集合称为字段集。
- 标签:点中的标签是被索引的元数据。请记住,测量是在标签上索引的,而不是在字段上。
- 标签集:所有标签的集合称为标签集。
- 系列:测量和标签的组合称为系列。
线路协议
线路协议是一种基于文本的格式,用于在 InfluxDB 中写入测量点。它由测量值、标签、字段和时间戳组成。当您使用 InfluxDB 的 HTTP API 向 InfluxDB 写入指针时,HTTP POST 的主体将是一个 line 协议,它表示要插入到 InfluxDB 的时序数据。线路协议中的每条线路定义一个点。多行必须用换行符\n
隔开。线路协议的格式由三部分组成:
[key] [fields] [timestamp]
线路协议中的每个部分都由空格分隔。它必须提供测量名称和至少一个字段。标签是可选的,但是在现实世界中,您应该包含标签。标记键和标记值是字符串。字段键是字符串,默认情况下,字段值是浮点数。如果一个点不包含时间戳,它将使用服务器的本地纳秒时间戳写入。除非提供了精度值,否则时间戳假定为纳秒。以下是代表单点的线路协议:
cpu,host=server01,region=uswest cpu_usage=46.26 1434055562000000000
这里cpu
是measurement
的名字,host
和region
是tags
的keys
,cpu_usage
是field
的名字,其value
为 46.26。value 1434055562000000000
就是timestamp
。当您使用 Go 客户端库将记录写入 InfluxDB 时,您不需要使用 line 协议格式制作数据,因为这是由客户端库完成的。
安装 InfluxDB
建议您使用 https://www.influxdata.com/downloads/#influxdb
中的一个预构建包来安装 InfluxDB。你也可以从 https://github.com/influxdata/influxdb
安装 InfluxDB。
在 macOS 中,您可以使用 brew 安装 InfluxDB:
brew install influxdb
在 InfluxDB 中创建数据库
让我们使用其命令行界面influx
在 InfluxDB 中创建一个数据库和用户帐户。influx
工具为数据库提供了一个交互式的 shell 来写数据,交互式地查询数据,以及查看不同格式的查询输出。要启动 InfluxDB 命令行界面,运行命令influx
:
$ influx
下一个命令创建一个名为opsadmin
的用户帐户:
> create user opsadmin with password 'pass123'
该命令向新创建的用户opsadmin
授予权限:
> grant all privileges to opsadmin
最后一个命令创建了一个名为metricsdb
的数据库
> create database metricsdb
使用 Go 客户端处理 InfluxDB
InfluxDB 的 Go 客户端库的v2
版本从github.com/influxdata/influxdb/client/v2
开始提供。Go 客户端库由 InfluxDB 团队维护。要安装软件包的v2
版本,请运行以下命令:
go get github.com/influxdata/influxdb/client/v2
要使用该包,您必须将github.com/influxdata/influxdb/client/v2
添加到导入列表中。
import "github.com/influxdata/influxdb/client/v2"
正在连接到英菲尼克斯数据库
默认情况下,InfluxDB 侦听端口 8086。以下代码块使用用户帐户opsadmin
连接到 InfluxDB。
c, err := client.NewHTTPClient(client.HTTPConfig{
Addr: "http://localhost:8086",
Username: “opsadmin”,
Password: “pass123”,
})
函数NewHTTPClient
从给定的配置中返回一个新的 InfluxDB Client
。结构类型HTTPConfig
用于为创建 InfluxDB Client
提供配置。下面是 struct HTTPConfig
的定义:
// HTTPConfig is the config data needed to create an HTTP Client
type HTTPConfig struct {
// Addr should be of the form "http://host:port"
// or "http://[ipv6-host%zone]:port".
Addr string
// Username is the influxdb username, optional
Username string
// Password is the influxdb password, optional
Password string
// UserAgent is the http User Agent, defaults to "InfluxDBClient"
UserAgent string
// Timeout for influxdb writes, defaults to no timeout
Timeout time.Duration
// InsecureSkipVerify gets passed to the http client, if true, it will
// skip https certificate verification. Defaults to false
InsecureSkipVerify bool
// TLSConfig allows the user to set their own TLS config for the HTTP
// Client. If set, this option overrides InsecureSkipVerify.
TLSConfig *tls.Config
}
一旦创建了 InfluxDB Client
,就可以使用它进行写和查询操作。
写入指向 InfluxDB 的点
当您向measurement
写入指针以将数据持久化到 InfluxDB 中时,您应该成批地这样做。要批量写入点,首先创建一个新的BatchPoints
值,如下所示:
bp, err := client.NewBatchPoints(client.BatchPointsConfig{
Database: “metricsdb”,
Precision: "s",
})
通过提供配置来创建一个BatchPoints
值。属性Precision
指定为每个point
创建的timestamp
的精度。默认情况下,Unix 中的所有时间戳都以纳秒为单位。如果您想以纳秒以外的任何单位提供时间戳,您必须提供适当的精度。分别用n
、u
、ms
、s
、m
和h
表示纳秒、微秒、毫秒、秒、分和小时。
下面的代码块通过向名为cpu
的measurement
提供tags
、fields
和timestamp
的值来创建一个新的point
。
// tagset – “host” and “region”
tags := map[string]string{
"host": "host1"
"region": "us-west"
}
// field - "cpu_usage"
fields := map[string]interface{}{
"cpu_usage": 46.22
}
// New point to measurement named “cpu”
pt, err := client.NewPoint("cpu ", tags, fields, time.Now())
if err != nil {
log.Fatalln("Error: ", err)
}
bp.AddPoint(pt)
因为你是批量写点的,所以用函数AddPoint
把n
的点数加到BatchPoints
上。一旦所有的点都添加到BatchPoints
中,调用 InfluxDB Client
实例的函数Write
来完成写操作。
// Write the batch
c.Write(bp) // c is the instance of InfluxDB Client instance
从 InfluxDB 读取点
InfluxDB 提供了使用熟悉的 SQL 结构查询数据的能力。该代码块决定了measurement cpu
中点的count
值。
command:= fmt.Sprintf("SELECT count(%s) FROM %s", "cpu_usage", "cpu")
q := client.Query{
Command: command,
Database: DB,
}
// Query the Database
if response, err := c.Query(q) // // c is the instance of InfluxDB Client instance
if err != nil {
log.Fatalln("Error: ", err)
}
count :=response.Results[0].Series[0].Values[0][1]
示例:在 InfluxDB 上读写
清单 6-5 显示了一个示例程序,它将点批量写入 InfluxDB,并从数据库中读取点。
package main
import (
"encoding/json"
"fmt"
"log"
"math/rand"
"time"
client "github.com/influxdata/influxdb/client/v2"
)
const (
// DB provides the database name of the InfluxDB
DB = "metricsdb"
username = "opsadmin"
password = "pass123"
)
func main() {
// Create client
c := influxDBClient()
// Write operations
// Create metrics data for measurement "cpu"
createMetrics(c)
// Read operations
// Read with limit of 10
readWithLimit(c, 10)
// Read mean value of "cpu_usage" for a region
meanCPUUsage(c, "us-west")
// Read count of records for a region
countRegion(c, "us-west")
}
// influxDBClient returns InfluxDB Client
func influxDBClient() client.Client {
c, err := client.NewHTTPClient(client.HTTPConfig{
Addr: "http://localhost:8086",
Username: username,
Password: password,
})
if err != nil {
log.Fatalln("Error: ", err)
}
return c
}
// createMetrics write batch points to create the metrics data
func createMetrics(clnt client.Client) {
batchCount := 100
rand.Seed(42)
// Create BatchPoints by giving config for InfluxDB
bp, _ := client.NewBatchPoints(client.BatchPointsConfig{
Database: DB,
Precision: "s",
})
// Batch update to adds Points
for i := 0; i < batchCount; i++ {
regions := []string{"us-west", "us-central", "us-north", "us-east"}
// tagset – “host” and “region”
tags := map[string]string{
"host": fmt.Sprintf("192.168.%d.%d", rand.Intn(100), rand.Intn(100)),
"region": regions[rand.Intn(len(regions))],
}
value := rand.Float64() * 100.0
// field - "cpu_usage"
fields := map[string]interface{}{
"cpu_usage": value,
}
pt, err := client.NewPoint("cpu", tags, fields, time.Now())
if err != nil {
log.Fatalln("Error: ", err)
}
// Add a Point
bp.AddPoint(pt)
}
// Writes the batch update to add points to measurement "cpu"
err := clnt.Write(bp)
if err != nil {
log.Fatalln("Error: ", err)
}
}
// queryDB query the database
func queryDB(clnt client.Client, command string) (res []client.Result, err error) {
// Create the query
q := client.Query{
Command: command,
Database: DB,
}
// Query the Database
if response, err := clnt.Query(q); err == nil {
if response.Error() != nil {
return res, response.Error()
}
res = response.Results
} else {
return res, err
}
return res, nil
}
// readWithLimit reads records with a given limit
func readWithLimit(clnt client.Client, limit int) {
q := fmt.Sprintf("SELECT * FROM %s LIMIT %d", "cpu", limit)
res, err := queryDB(clnt, q)
if err != nil {
log.Fatalln("Error: ", err)
}
for i, row := range res[0].Series[0].Values {
t, err := time.Parse(time.RFC3339, row[0].(string))
if err != nil {
log.Fatalln("Error: ", err)
}
val, err := row[1].(json.Number).Float64()
fmt.Printf("[%2d] %s: %f\n", i, t.Format(time.Stamp), val)
}
}
// meanCPUUsage reads the mean value of cpu_usage
func meanCPUUsage(clnt client.Client, region string) {
q := fmt.Sprintf("select mean(%s) from %s where region = '%s'", "cpu_usage", "cpu", region)
res, err := queryDB(clnt, q)
if err != nil {
log.Fatalln("Error: ", err)
}
value, err := res[0].Series[0].Values[0][1].(json.Number).Float64()
if err != nil {
log.Fatalln("Error: ", err)
}
fmt.Printf("Mean value of cpu_usage for region '%s':%f\n", region, value)
}
// countRegion reads the count of records for a given region
func countRegion(clnt client.Client, region string) {
q := fmt.Sprintf("SELECT count(%s) FROM %s where region = '%s'", "cpu_usage", "cpu", region)
res, err := queryDB(clnt, q)
if err != nil {
log.Fatalln("Error: ", err)
}
count := res[0].Series[0].Values[0][1]
fmt.Printf("Found a total of %v records for region '%s'\n", count, region)
}
Listing 6-5.Writing and Reading of Points to a Measurement “cpu” in InfluxDB
函数influxDBClient
返回一个Client
对象,该对象用于 InfluxDB 的读写操作。createMetrics
功能用于批量写点。为了举例,100 个点被插入到一个名为cpu
的measurement
中。tagset
中包含两个tags
:?? 和 ??。measurement cpu
有一个field
名为cpu_usage
。
为了执行读操作,函数queryDB
被用作助手函数,它在执行给定的查询命令后返回一部分client.Result
。在这个例子中,使用助手函数queryDB
执行了三个查询操作。函数readWithLimit
从measurement cpu
读取数据,限制为 10。函数meanCPUUsage
为region "us-west"
从measurement cpu
中读取cpu_usage
的mean
值。最后,函数countRegion
从measurement cpu
读取region us-west
的点数。
执行读取操作时,您应该会看到类似如下的输出:
[ 0] Sep 17 10:49:42: 11.901734
[ 1] Sep 17 10:49:42: 15.471216
[ 2] Sep 17 10:49:42: 32.904423
[ 3] Sep 17 10:49:42: 15.973031
[ 4] Sep 17 10:49:42: 88.648864
[ 5] Sep 17 10:49:42: 92.049809
[ 6] Sep 17 10:49:42: 83.304049
[ 7] Sep 17 10:49:42: 18.495674
[ 8] Sep 17 10:49:42: 23.389015
[ 9] Sep 17 10:49:42: 46.009337
Mean value of cpu_usage for region 'us-west':46.268998
Found a total of 27 records for region 'us-west'
您可以从influx
命令行界面工具执行查询操作。让我们运行该工具并执行一个查询:
$ influx
> select * from cpu limit 10
上述命令提供了类似于以下内容的数据:
name: cpu
---------
time cpu_usage host region
1474109382000000000 11.901733613473244 192.168.1.21 us-west
1474109382000000000 15.47121626535387 192.168.99.62 us-east
1474109382000000000 32.9044231821345 192.168.98.18 us-north
1474109382000000000 15.97303140480521 192.168.97.1 us-central
1474109382000000000 88.64886440612389 192.168.96.13 us-north
1474109382000000000 92.04980918501607 192.168.95.74 us-central
1474109382000000000 83.30404929547693 192.168.91.22 us-west
1474109382000000000 18.495673741297637 192.168.90.58 us-west
1474109382000000000 23.38901519689525 192.168.9.91 us-west
1474109382000000000 46.00933676790605 192.168.9.30 us-central
6-4.使用 SQL 数据库
问题
您希望在自己的 Go 应用程序中使用 PostgreSQL、MySQL 等关系数据库。
解决办法
标准库包database/sql
为使用 SQL 数据库提供了一个通用接口。要使用任何特定的 SQL 数据库,您必须使用特定于数据库的驱动程序和包database/sql
。可以在 http://golang.org/s/sqldrivers
找到与包database/sql
一起工作的第三方 SQL 驱动程序列表。
它是如何工作的
包database/sql
提供了一个通用接口,用于处理各种 SQL 数据库。虽然database/sql
为 SQL 数据库提供了一个通用接口,但是它不包含任何特定的数据库驱动程序。因此你必须使用一个第三方的包,它提供了包database/sql
的实现。例如,如果您想使用 PostgreSQL 数据库,您必须为database/sql
使用 PostgreSQL 的数据库驱动程序。
使用 PostgreSQL
第三方包pq
( github.com/lib/pq
)是database/sql
的 PostgreSQL 驱动,用 Go 写的。要安装软件包pq
,运行以下命令:
go get github.com/lib/pq
要使用包pq
,您只需要导入驱动程序,就可以使用包database/sql
提供的完整 API。下面代码块中的init
函数打开一个 PostgreSQL 数据库:
import (
"database/sql"
_ "github.com/lib/pq"
)
var db *sql.DB
func init() {
var err error
db, err = sql.Open("postgres", "postgres://user:pass@localhost/dbname")
if err != nil {
log.Fatal(err)
}
}
当您使用 SQL 数据库时,您通常使用包database/sql
的 API,但是您可能不需要直接访问特定数据库驱动程序的包的功能。在这里,您使用包pq
只是为了调用它的init
函数,将您的驱动程序"postgres"
注册到database/sql
。因为包pq
的导入只是为了调用它的init
函数,所以使用一个空白标识符(_)
作为包别名以避免编译错误。
包database/sql
的函数Open
打开一个由其数据库驱动程序名和驱动程序特定的数据源名指定的数据库,通常至少包含一个数据库名和连接信息。这里的数据库驱动名是"postgres"
。函数Open
返回*sql.DB
,它代表包sql
为您的数据库提供的连接池。
使用 MySQL
第三方包mysql
( github.com/go-sql-driver/mysql
)是database/sql
的 MySQL 驱动。要安装软件包mysql
,运行以下命令:
go get github.com/go-sql-driver/mysql
要使用包mysql
,您只需要导入驱动程序,就可以使用包database/sql
提供的完整 API。下面代码块中的init
函数打开一个 MySQL 数据库:
import (
"database/sql"
_ " github.com/go-sql-driver/mysql"
)
var db *sql.DB
func init() {
var err error
db, err = sql.Open("mysql", "user:password@/dbname")
if err != nil {
log.Fatal(err)
}
}
包database/sql
的函数Open
打开一个带有驱动名“mysql”
和给定数据源名的数据库。
PostgreSQL 数据库示例
让我们编写一个示例程序来演示如何使用包database/sql
和pq
使用 PostgreSQL 数据库。以下 SQL 语句用于为示例程序创建表结构:
create table products (
id serial primary key,
title varchar(255) NOT NUL,
description varchar(255) NOT NUL,
price decimal(5,2) NOT NULL
);
清单 6-6 展示了一个示例程序,该程序演示了在名为productstore
的数据库上使用 PostgreSQL 数据库进行插入和读取操作。
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/lib/pq"
)
// Product struct provides the data model for productstore
type Product struct {
ID int
Title string
Description string
Price float32
}
var db *sql.DB
func init() {
var err error
db, err = sql.Open("postgres", "postgres://user:pass@localhost/productstore")
if err != nil {
log.Fatal(err)
}
}
func main() {
product := Product{
Title: "Amazon Echo",
Description: "Amazon Echo - Black",
Price: 179.99,
}
// Insert a product
createProduct(product)
// Read all product records
getProducts()
}
// createProduct inserts product values into product table
func createProduct(prd Product) {
result, err := db.Exec("INSERT INTO products(title, description, price) VALUES($1, $2, $3)", prd.Title, prd.Description, prd.Price)
if err != nil {
log.Fatal(err)
}
lastInsertID, err := result.LastInsertId()
rowsAffected, err := result.RowsAffected()
fmt.Printf("Product with id=%d created successfully (%d row affected)\n", lastInsertID, rowsAffected)
}
// getProducts reads all records from the product table
func getProducts() {
rows, err := db.Query("SELECT * FROM products")
if err != nil {
if err == sql.ErrNoRows {
fmt.Println("No Records Found")
return
}
log.Fatal(err)
}
defer rows.Close()
var products []*Product
for rows.Next() {
prd := &Product{}
err := rows.Scan(&prd.Title, &prd.Description, &prd.Price)
if err != nil {
log.Fatal(err)
}
products = append(products, prd)
}
if err = rows.Err(); err != nil {
log.Fatal(err)
}
for _, pr := range products {
fmt.Printf("%s, %s, $%.2f\n", pr.Title, pr.Description, pr.Price)
}
}
Listing 6-6.Insert and Read Operation with a Database productstore in PostgreSQL
在函数init
中创建一个*sql.DB
对象,通过提供数据库驱动程序名称作为“postgres”
和数据源名称来使用 PostgreSQL 数据库。
var db *sql.DB
func init() {
var err error
db, err = sql.Open("postgres", "postgres://user:pass@localhost/productstore")
if err != nil {
log.Fatal(err)
}
}
*sql.DB
对象用于执行插入和读取操作。为了向数据库表中插入记录,sql.DB
对象的函数Exec
用于执行查询,而不返回任何行。插入记录的值使用占位符参数传递,占位符参数使用$N
符号。占位符参数的语法在不同的数据库中是不同的。例如,MySQL 和 SQL Server 使用字符?
作为占位符。函数Exec
返回一个sql.Result
值,有两个方法:LastInsertId
和RowsAffected. LastInsertId
返回数据库生成的整数值,可以用来获取的值,并在插入新行时自动递增列。RowsAffected
返回受更新、插入或删除操作影响的行数。
func createProduct(prd Product) {
result, err := db.Exec("INSERT INTO products(title, description, price) VALUES($1, $2, $3)", prd.Title, prd.Description, prd.Price)
if err != nil {
log.Fatal(err)
}
lastInsertID, err := result.LastInsertId()
rowsAffected, err := result.RowsAffected()
fmt.Printf("Product with id=%d created successfully (%d row affected)\n", lastInsertID, rowsAffected)
}
为了执行 SQL 语句SELECT
来查询数据,使用了sql.DB
对象的函数Query
,它返回一个 struct 类型的值Rows
。
rows, err := db.Query("SELECT * FROM products")
通过调用Rows
对象的方法Next
,可以使用Scan
方法读取下一行的值。
var products []*Product
for rows.Next() {
prd := &Product{}
err := rows.Scan(&prd.Title, &prd.Description, &prd.Price)
if err != nil {
log.Fatal(err)
}
products = append(products, prd)
}
if err = rows.Err(); err != nil {
log.Fatal(err)
}
当您执行一个获取单行的查询时,您可以使用函数QueryRow
来执行一个查询并返回一行。下面是一个使用QueryRow
获取一行的示例代码块:
id := 1
var product string
err := db.QueryRow("SELECT title FROM products WHERE id=$1", id).Scan(&product)
switch {
case err == sql.ErrNoRows:
log.Printf("No product with that ID.")
case err != nil:
log.Fatal(err)
default:
fmt.Printf("Product is %s\n", product)
}
当您运行清单 6-6 中的程序时,您应该会看到类似如下的输出:
Product with id=1 created successfully (1 row affected)
Amazon Echo, Amazon Echo - Black, $179.99
使用标准库包database/sql
和第三方的特定数据库驱动包,如 PostgreSQL 数据库的github.com/lib/pq
,您可以使用各种 SQL 数据库。使用包database/sql
的好处是你可以使用同一个接口来处理不同的数据库。
七、构建 HTTP 服务器
Go 是一种通用编程语言,可用于构建各种应用程序。说到 web 编程,Go 是构建后端 API 的一个很好的技术栈。Go 可能不是构建传统 web 应用程序的理想选择,在传统 web 应用程序中,web 应用程序使用服务器端模板执行 UI 呈现。当您为各种系统(包括 web 前端、移动应用程序和许多现代应用程序场景)构建支持后端系统的 RESTful APIs 时,Go 是最好的堆栈。一些现有的技术栈适合构建轻量级 RESTful APIs,但是当 HTTP 请求具有 CPU 密集型任务并且 API 在分布式环境中与其他后端系统通信时,这些系统最终会失败。Go 是构建大规模可伸缩后端系统和 RESTful APIs 的理想技术栈。在本章中,您将学习如何构建 HTTP 服务器来构建您的后端 API。
带有大量可扩展性的标准库包net/http
,为在 Go 中编写 web 应用程序提供了基础层。如果您想为您的 Go web 应用程序使用服务器端模板,您可以利用标准库包html/template
来呈现用户界面。在 Go 中,只需使用标准的库包,就可以构建全功能的 web 应用和 RESTful APIs,因此对于大多数 web 编程场景,尤其是构建 RESTful 服务,都不需要 web 框架。在大多数用例中,使用标准库包;每当您需要额外的功能时,请使用扩展标准库包的第三方库。
简而言之,web 编程基于请求-响应范例,其中客户端向 web 服务器发送 HTTP 请求,请求在 web 服务器上被处理,然后它向客户端发回 HTTP 响应。为了以这种方式处理 HTTP 请求和发送 HTTP 响应,包net/http
提供了两个主要组件:
- ServeMux 是一个 HTTP 请求多路复用器(HTTP 请求路由器),它将传入的 HTTP 请求的统一资源标识符(URIs)与预定义的 URI 模式列表进行比较,然后执行为 URI 模式配置的相关处理程序。struct type
http.ServeMux
提供了一个作为 HTTP 请求多路复用器的实现。 - 处理程序负责将消息头和消息体写入 HTTP 响应。在包
net/http
中,Handler
是一个接口,因此当您编写 HTTP 应用程序时,它提供了更高级别的可扩展性。因为处理程序实现只是寻找一个具体类型的Handler
接口,所以您可以提供自己的实现来服务 HTTP 请求。
包net/http
是为可扩展性和可组合性而设计的,所以通过扩展net/http
提供的功能,它为您编写 web 应用程序提供了很大的灵活性。Go 社区提供了很多第三方包来扩展包net/http
,可以用于你的 Go web 应用。
7-1.创建自定义 HTTP 处理程序
问题
如何为 HTTP 服务器创建服务于 HTTP 请求的定制处理程序?
解决办法
HTTP 处理程序是通过提供http.Handler
接口的实现来创建的。
它是如何工作的
在 Go 中,如果你能提供一个http.Handler
接口的实现,任何对象都可以是 HTTP 处理程序的实现。下面是http
包中Handler
接口的定义:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
接口http.Handler
有一个方法ServeHTTP
,它有两个参数:一个接口类型http.ResponseWriter
和一个指向结构类型http.Request
的指针。方法ServeHTTP
应该用于将报头和数据写入ResponseWriter
。
让我们通过向 struct 类型提供方法ServeHTTP
来创建一个定制的处理程序:
type textHandler struct {
responseText string
}
func (th *textHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, th.responseText)
}
声明了一个结构类型textHandler
,它有一个字段responseText
,将用于向ResponseWriter
写入数据。方法ServeHTTP
被附加到textHandler
上,因此它是接口http.Handler
的一个实现。通过将 struct type textHandler
配置为带有ServeMux
的处理程序,可以为 HTTP 请求提供服务。清单 7-1 展示了一个示例 HTTP 服务器,它使用两个定制处理程序来服务 HTTP 请求。
package main
import (
"fmt"
"log"
"net/http"
)
type textHandler struct {go get gopkg.in/mgo.v2
responseText string
}
func (th *textHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, th.responseText)
}
type indexHandler struct {
}
func (ih *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set(
"Content-Type",
"text/html",
)
html :=
`<doctype html>
<html>
<head>
<title>Hello Gopher</title>
</head>
<body>
<b>Hello Gopher!</b>
<p>
<a href="/welcome">Welcome</a> | <a href="/message">Message</a>
</p>
</body>
</html>`
fmt.Fprintf(w, html)
}
func main() {
mux := http.NewServeMux()
mux.Handle("/", &indexHandler{})
thWelcome := &textHandler{"Welcome to Go Web Programming"}
mux.Handle("/welcome", thWelcome)
thMessage := &textHandler{"net/http package is used to build web apps"}
mux.Handle("/message", thMessage)
log.Println("Listening...")
http.ListenAndServe(":8080", mux)
}
Listing 7-1.HTTP Server with Custom Handlers
HTTP 服务器使用两种处理程序实现:textHandler
和indexHandler
;两者都是通过提供方法ServeHTTP
的实现来实现http.Handler
接口的结构类型。textHandler
的方法ServeHTTP
使用函数fmt.Fprintf
将通过其属性访问的文本字符串写入ResponseWriter
。
func (th *textHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, th.responseText)
}
对于indexHandler
,方法ServeHTTP
声明一个 HTML 字符串,并将其写入ResponseWriter
。
func (ih *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set(
"Content-Type",
"text/html",
)
html :=
`<doctype html>
<html>
<head>
<title>Hello Gopher</title>
</head>
<body>
<b>Hello Gophe!</b>
<p>
<a href="/welcome">Welcome</a> | <a href="/message">Message</a>
</p>
</body>
</html>`
fmt.Fprintf(w, html)
}
在函数main
中,ServeMux
的对象被创建,然后通过提供统一资源定位符(URL)模式及其相应的处理程序值来配置 HTTP 请求多路复用器。
func main() {
mux := http.NewServeMux()
mux.Handle("/", &indexHandler{})
thWelcome := &textHandler{"Welcome to Go Web Programming"}
mux.Handle("/welcome", thWelcome)
thMessage := &textHandler{"net/http package is used to build web apps"}
mux.Handle("/message", thMessage)
log.Println("Listening...")
http.ListenAndServe(":8080", mux)
}
ServeMux
的功能Handle
允许您向相关的处理程序注册一个 URL 模式。在这里,URL "/"
被映射为一个indexHandler
值作为处理程序,URL"/welcome"
和"/message"
被映射为textHandler
值作为处理 HTTP 请求的处理程序。因为您已经实现了一个带有ServeMux
值的 HTTP 请求多路复用器,它使用了两个定制的处理程序来处理 HTTP 请求,所以现在您可以启动您的 HTTP 服务器了。函数ListenAndServe
使用给定的地址和处理程序启动 HTTP 服务器。
http.ListenAndServe(":8080", mux)
函数ListenAndServe
的第一个参数是 HTTP 服务器在给定传输控制协议(TCP)网络地址监听的地址,第二个参数是http.Handler
接口的实现。这里你给了一个ServeMux
值作为处理程序。结构类型ServeMux
也实现了方法ServeHTTP
,因此它可以作为调用函数ListenAndServe
的处理程序。通常,您提供一个ServeMux
值作为调用函数ListenAndServe
的第二个参数。我们将在本章后面更详细地讨论这一点。
函数http.ListenAndServe
通过使用给定的参数创建结构类型http.Server
的一个实例,调用它的(http.Server
值)ListenAndServe
方法监听 TCP 网络地址,然后用一个处理程序调用方法Serve
(属于http.Server
值)来处理传入连接的请求。http.Server
定义了运行 HTTP 服务器的参数。
让我们运行程序来启动一个 HTTP 服务器,该服务器将在端口号 8080 进行侦听。图 7-1 显示了 HTTP 服务器对"/"
请求的响应。
图 7-1。
Server response for the request to "/"
图 7-2 显示了 HTTP 服务器对"/welcome"
请求的响应。
图 7-2。
Server response for the request to "/welcome"
图 7-3 显示了 HTTP 服务器对"/message"
请求的响应。
图 7-3。
Server response for the request to "/message"
7-2.使用适配器将普通函数用作处理程序
问题
为 HTTP 请求创建定制的处理程序将是一项单调乏味的工作。如何使用适配器将普通函数用作 HTTP 处理程序,从而不需要创建自定义处理程序类型?
解决办法
通过使用 func 类型http.HandlerFunc
,您可以使用普通函数作为 HTTP 处理程序。HandlerFunc
有一个接口http.Handler
的实现,因此它可以被用作 HTTP 处理程序。您可以提供带有适当签名的普通函数,作为HandlerFunc
的参数,将其用作 HTTP 处理程序。在这里,func type HandlerFunc
作为普通函数的适配器,用作 HTTP 处理程序。
它是如何工作的
HandlerFunc
是一个适配器,允许您使用普通函数作为 HTTP 处理程序。下面是http
包中HandlerFunc
类型的声明:
type HandlerFunc func(ResponseWriter, *Request)
如果fn
是一个具有适当签名(func(ResponseWriter, *Request)
)的函数,HandlerFunc(fn)
是一个调用fn
的处理程序。清单 7-2 展示了一个示例 HTTP 服务器,它使用HandlerFunc
来使用普通函数作为 HTTP 处理程序。
package main
import (
"fmt"
"log"
"net/http"
)
func index(w http.ResponseWriter, r *http.Request) {
w.Header().Set(
"Content-Type",
"text/html",
)
html :=
`<doctype html>
<html>
<head>
<title>Hello Gopher</title>
</head>
<body>
<b>Hello Gopher!</b>
<p>
<a href="/welcome">Welcome</a> | <a href="/message">Message</a>
</p>
</body>
</html>`
fmt.Fprintf(w, html)
}
func welcome(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome to Go Web Programming")
}
func message(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "net/http package is used to build web apps")
}
func main() {
mux := http.NewServeMux()
mux.Handle("/", http.HandlerFunc(index))
mux.Handle("/welcome", http.HandlerFunc(welcome))
mux.Handle("/message", http.HandlerFunc(message))
log.Println("Listening...")
http.ListenAndServe(":8080", mux)
}
Listing 7-2.HTTP Server That Uses Normal Functions as HTTP Handlers
这里用签名func(ResponseWriter, *Request)
声明函数,通过将这些函数提供给HandlerFunc
来将它们用作 HTTP 处理程序。
mux := http.NewServeMux()
mux.Handle("/", http.HandlerFunc(index))
mux.Handle("/welcome", http.HandlerFunc(welcome))
mux.Handle("/message", http.HandlerFunc(message))
将这种方法与清单 7-1、中编写的程序进行比较,其中您创建了一个结构类型并提供了一个方法ServeHTTP
来实现接口http.Handler
,这种方法更容易,因为您可以简单地使用普通函数作为 HTTP 处理程序。
7-3.使用普通函数作为 HTTP 处理程序。HandleFunc
问题
如何在不显式调用http.HandlerFunc
类型的情况下使用普通函数作为 HTTP 处理程序?
解决办法
ServeMux
提供了一个方法HandleFunc
,允许你注册一个普通函数作为给定 URI 模式的处理程序,而不需要显式调用 func 类型http.HandlerFunc
。
它是如何工作的
ServeMux
的方法HandleFunc
是一个 helper 函数,内部调用ServeMux
的方法Handle
,其中给定的处理函数用于调用http.HandlerFunc
来提供http.Handler
的实现。下面是包http
中函数HandleFunc
的源代码:
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
mux.Handle(pattern, HandlerFunc(handler))
}
清单 7-3 展示了一个示例 HTTP 服务器,它使用ServeMux
的HandleFunc
来使用普通函数作为 HTTP 处理程序,而没有显式使用HandlerFunc
。
package main
import (
"fmt"
"log"
"net/http"
)
func index(w http.ResponseWriter, r *http.Request) {
w.Header().Set(
"Content-Type",
"text/html",
)
html :=
`<doctype html>
<html>
<head>
<title>Hello Gopher</title>
</head>
<body>
<b>Hello Gopher!</b>
<p>
<a href="/welcome">Welcome</a> | <a href="/message">Message</a>
</p>
</body>
</html>`
fmt.Fprintf(w, html)
}
func welcome(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome to Go Web Programming")
}
func message(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "net/http package is used to build web apps")
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", index)
mux.HandleFunc("/welcome", welcome)
mux.HandleFunc("/message", message)
log.Println("Listening...")
http.ListenAndServe(":8080", mux)
}
Listing 7-3.HTTP Server That Uses HandleFunc of ServeMux
HandleFunc
只是一个助手函数,它通过提供http.HandlerFunc
作为处理程序来调用ServeMux
的函数Handle
。
7-4.使用默认 ServeMux 值
问题
如何使用包http,
提供的默认ServeMux
值作为ServeMux,
,当使用默认ServeMux
值时,如何注册处理函数?
解决办法
包http
提供了一个名为DefaultServeMux
的默认ServeMux
值,它可以用作 HTTP 请求多路复用器,这样您就不需要从代码中创建一个ServeMux
。当使用DefaultServeMux
作为ServeMux
值时,可以使用函数http.HandleFunc
配置 HTTP 路由,该函数将给定模式的处理函数注册到DefaultServeMux
中。
它是如何工作的
默认情况下,包http
提供了一个名为DefaultServeMux
的ServeMux
实例。当您调用函数http.ListenAndServe
来运行您的 HTTP 服务器时,您可以提供一个nil
值作为第二个参数的自变量(一个http.Handler
的实现)。
http.ListenAndServe(":8080", nil)
如果您提供一个nil
值,包http
将把DefaultServeMux
作为ServeMux
值。当使用DefaultServeMux
作为ServeMux
值时,可以使用函数http.HandleFunc
为给定的 URL 模式注册一个处理函数。在函数http.HandleFunc
内部,调用DefaultServeMux
的函数HandleFunc
。然后ServeMux
的HandleFunc
通过使用给定的处理函数提供http.HandlerFunc
调用来调用ServeMux
的函数Handle
。
清单 7-4 展示了一个示例 HTTP 服务器,它使用DefaultServeMux
作为ServeMux
值,并使用http.HandleFunc
注册一个处理函数。
package main
import (
"fmt"
"log"
"net/http"
)
func index(w http.ResponseWriter, r *http.Request) {
w.Header().Set(
"Content-Type",
"text/html",
)
html :=
`<doctype html>
<html>
<head>
<title>Hello Gopher</title>
</head>
<body>
<b>Hello Gopher!</b>
<p>
<a href="/welcome">Welcome</a> | <a href="/message">Message</a>
</p>
</body>
</html>`
fmt.Fprintf(w, html)
}
func welcome(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome to Go Web Programming")
}
func message(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "net/http package is used to build web apps")
}
func main() {
http.HandleFunc("/", index)
http.HandleFunc("/welcome", welcome)
http.HandleFunc("/message", message)
log.Println("Listening...")
http.ListenAndServe(":8080", nil)
}
Listing 7-4.HTTP Server That Uses DefaultServeMux and http.HandleFunc
函数http.HandleFunc
用于向DefaultServeMux.
注册一个处理函数
7-5.自定义 http。计算机网络服务器
问题
如何定制用于运行 HTTP 服务器的http.Server
的值?
解决办法
要定制http.Server
并使用它来运行 HTTP 服务器,用所需的值创建一个http.Server
的实例,然后调用它的方法ListenAndServe
。
它是如何工作的
在前面的食谱中,您已经使用功能http.ListenAndServe
运行了一个 HTTP 服务器。当您调用函数http.ListenAndServe
时,它通过提供地址的字符串值和http.Handler
值在内部创建http.Server
的实例,并使用http.Server
值运行服务器。因为http.Server
的实例是从函数http.ListenAndServe
内部创建的,所以您不能自定义http.Server
的值。http.Server
定义了运行 HTTP 服务器的参数。如果你想定制http.Server
值,你可以从你的程序中显式的创建一个http.Server
的实例,然后调用它的方法ListenAndServe
。
清单 7-5 展示了一个定制http.Server
并调用其方法ListenAndServe
来运行 HTTP 服务器的示例 HTTP 服务器。
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func index(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome to Go Web Programming")
}
func main() {
http.HandleFunc("/", index)
server := &http.Server{
Addr: ":8080",
ReadTimeout: 60 * time.Second,
WriteTimeout: 60 * time.Second,
}
log.Println("Listening...")
server.ListenAndServe()
}
Listing 7-5.HTTP Server That Uses the Method ListenAndServe of http.Server
这个例子定制了用于运行 HTTP 服务器的http.Server
的字段ReadTimeout
和WriteTimeout
。
7-6.编写 HTTP 中间件
问题
如何编写一个 HTTP 中间件函数,用一段可插入的代码来包装 HTTP 处理程序,从而为 HTTP 应用程序提供共享行为?
解决办法
要编写 HTTP 中间件函数,请编写带有签名func(http.Handler) http.Handler
的函数,这样 HTTP 中间件函数就可以接受一个处理程序作为参数值,并且可以在中间件函数内部提供一段可插入的代码。因为它返回http.Handler
,中间件函数可以作为Handler
向 HTTP 请求多路复用器注册。
它是如何工作的
HTTP 中间件是包装 web 应用程序的 HTTP 处理程序的可插入和自包含的代码。这些类似于典型的 HTTP 处理程序,但是它们包装了另一个 HTTP 处理程序,通常是普通的应用程序处理程序,为 web 应用程序提供共享行为。它作为 HTTP 请求处理周期中的另一层,注入一些可插入的代码来执行共享行为,如身份验证和授权、日志记录、缓存等。
下面是编写 HTTP 中间件的基本模式:
func middlewareHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Middleware logic goes here before executing application handler
next.ServeHTTP(w, r)
// Middleware logic goes here after executing application handler
})
}
这里,中间件函数接受一个http.Handler
值并返回一个http.Handler
值。因为中间件函数返回http.Handler
,所以它可以通过将应用程序处理程序包装为中间件函数的一个参数,注册为一个带有http.ServeMux
的Handler
。要从中间件调用给定处理程序的逻辑,调用它的方法ServeHTTP
。
next.ServeHTTP(w, r)
中间件逻辑可以在执行应用处理程序之前和之后执行。在执行给定的Handler
(句柄获取为参数值)前写中间件逻辑,在调用ServeHTTP
前写,在执行参数值Handler
后调用ServeHTTP
执行中间件逻辑后写。
清单 7-6 展示了一个示例 HTTP 服务器,它用一个名为loggingHandler.
的中间件函数包装应用程序处理程序
package main
import (
"fmt"
"log"
"net/http"
"time"
)
// loggingHandler is an HTTP Middleware that logs HTTP requests.
func loggingHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Middleware logic before executing given Handler
start := time.Now()
log.Printf("Started %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
// Middleware logic after executing given Handler
log.Printf("Completed %s in %v", r.URL.Path, time.Since(start))
})
}
func index(w http.ResponseWriter, r *http.Request) {
w.Header().Set(
"Content-Type",
"text/html",
)
html :=
`<doctype html>
<html>
<head>
<title>Hello Gopher</title>
</head>
<body>
<b>Hello Gopher!</b>
<p>
<a href="/welcome">Welcome</a> | <a href="/message">Message</a>
</p>
</body>
</html>`
fmt.Fprintf(w, html)
}
func welcome(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome to Go Web Programming")
}
func message(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "net/http package is used to build web apps")
}
func main() {
http.Handle("/", loggingHandler(http.HandlerFunc(index)))
http.Handle("/welcome", loggingHandler(http.HandlerFunc(welcome)))
http.Handle("/message", loggingHandler(http.HandlerFunc(message)))
log.Println("Listening...")
http.ListenAndServe(":8080", nil)
}
Listing 7-6.HTTP Middleware That Wraps Application Handlers
名为loggingHandler
的 HTTP 中间件用于记录所有 HTTP 请求及其响应时间。函数loggingHandler
接受一个http.Handler
值,因此您可以将应用程序处理程序作为参数传递给中间件函数,并且可以将中间件处理程序注册到ServeMux
,因为它返回http.Handler
。
http.Handle("/", loggingHandler(http.HandlerFunc(index)))
http.Handle("/welcome", loggingHandler(http.HandlerFunc(welcome)))
http.Handle("/message", loggingHandler(http.HandlerFunc(message)))
因为中间件函数的参数类型是http.Handler
,通过使用http.HandlerFunc
调用中间件,应用处理函数被转换为http.Handler
。您可以用中间件函数链来包装您的应用程序处理程序,因为您用signature func(http.Hanlder) http.Handler
编写中间件函数。
让我们运行应用程序并导航到所有已配置的 URL 模式。您应该会看到 HTTP 中间件提供的日志消息,如下所示:
2016/08/05 15:34:29 Started GET /
2016/08/05 15:34:29 Completed / in 5.0039ms
2016/08/05 15:34:34 Started GET /welcome
2016/08/05 15:34:34 Completed /welcome in 9.0082ms
2016/08/05 15:34:40 Started GET /message
2016/08/05 15:34:40 Completed /message in 6.0077ms
7-7.用 Go 和 MongoDB 编写 RESTful API
问题
您希望在 Go 中使用 MongoDB 作为持久化存储来编写 RESTful APIs。
解决办法
标准库包http
提供了构建 RESTful APIs 的所有必要组件。包http
是为可扩展性而设计的,因此当您编写 HTTP 应用程序时,您可以使用第三方包和您自己的定制包来扩展包的功能。包mgo
是使用 MongoDB 最流行的包,用于 REST API 示例的数据持久化。
它是如何工作的
让我们构建一个 REST API 示例来演示如何用 Go 和 MongoDB 构建一个 RESTful API。虽然包http
足以构建 web 应用程序,但我们希望使用第三方包Gorilla mux
( github.com/gorilla/mux
)作为 HTTP 请求多路复用器,而不是http.ServeMux
。包mux
为指定 HTTP 路由提供了丰富的功能,这对于指定 RESTful 端点很有用。例如,http.ServeMux
不支持为 URL 模式指定 HTTP 动词,这对于定义 RESTful 端点是必不可少的,但是包mux
为定义应用程序的路由提供了很大的灵活性,包括为 URL 模式指定 HTTP 动词。第三方包mgo
用于在 MongoDB 数据库上执行持久化,这是一个流行的 NoSQL 数据库。
应用程序的目录结构
我们将 REST API 应用程序组织成多个包。图 7-4 显示了用于 REST API 应用程序的高级目录结构。
图 7-4。
Directory structure of the REST API application
图 7-5 显示了 REST API 应用程序完整版本的目录结构和相关文件。
图 7-5。
Directory structure and associated files of the completed application
除了目录keys
,其他目录都代表 Go 包。keys
目录包含用于签署 JSON web 令牌(JWT)及其验证的密钥。这用于通过 JWT 对 API 进行认证。
REST API 应用程序被分成以下几个包:
Common
:包common
提供实用函数,为应用提供初始化逻辑。Controllers
:包controllers
为应用程序提供 HTTP 处理函数。Store
:包store
用 MongoDB 数据库提供持久化逻辑。model
:包model
描述了应用的数据模型。routers
:包routers
为 REST API 实现 HTTP 请求路由器。
书中的示例代码主要关注名为Bookmark
的实体,并讨论构建 REST API 的基本部分。REST API 应用程序的完整版本,包括 JWT 认证、日志等等,可以从本书的代码库中获得,代码库位于 https://github.com/shijuvar/go-recipes
。
数据模型
包model
为 REST API 应用程序提供了数据模型。清单 7-7 展示了 REST API 示例的数据模型。
package model
import (
"time"
"gopkg.in/mgo.v2/bson"
)
// Bookmark type represents the metadata of a bookmark.
type Bookmark struct {
ID bson.ObjectId `bson:"_id,omitempty"`
Name string `json:"name"`
Description string `json:"description"`
Location string `json:"location"`
Priority int `json:"priority"` // Priority (1 -5)
CreatedBy string `json:"createdby"`
CreatedOn time.Time `json:"createdon,omitempty"`
Tags []string `json:"tags,omitempty"`
}
Listing 7-7.Data Model in models.go
类型Bookmark
代表应用程序中书签的元数据。该模型被设计为与 MongoDB 一起工作,因此字段类型ID
被指定为bson.ObjectId
。示例应用程序允许用户添加、编辑、删除和查看书签的元数据,这些元数据可以用优先级和标签来组织。
资源模型
上一步定义了要使用的应用程序的数据模型,即 NoSQL 数据库 MongoDB。既然您已经对数据库进行了数据建模,那么让我们为 REST APIs 定义资源模型。资源建模定义了一个 REST API,它向客户端应用程序提供 API 的端点。这可以利用 URIs、使用各种 HTTP 方法的 API 操作等等。根据 Roy Fielding 关于 REST 的论文,“REST 中信息的关键抽象是资源。任何可以命名的信息都可以是资源:文档或图像、时态服务(例如,“洛杉矶今天的天气”)、其他资源的集合、非虚拟对象(例如人)等等。换句话说,任何可能成为作者超文本参考目标的概念都必须符合资源的定义。资源是到一组实体的概念性映射,而不是在任何特定时间点对应于该映射的实体。
这里您定义了一个名为“/bookmarks
”的资源,它代表了一个书签实体的集合。通过在资源"/bookmarks
"上使用 HTTP Post,您可以创建一个新的资源。URI " /bookmarks/{id}
"可以用来表示单个书签实体。通过在“/bookmarks/{id}
”上使用 HTTP Get,可以检索单个书签的数据。表 7-1 显示了针对书签实体设计的资源模型。
表 7-1。
Resource Model for the Bookmark Entity
| 上呼吸道感染 | HTTP 动词 | 功能 | | --- | --- | --- | | `/bookmarks` | 邮政 | 创建新书签 | | `/bookmarks/{id}` | 放 | 更新给定 ID 的现有书签 | | `/bookmarks` | 得到 | 获取所有书签 | | `/bookmarks/{id}` | 得到 | 获取给定 ID 的单个书签 | | `/bookmarks/users/{id}` | 得到 | 获取与单个用户关联的所有书签 | | `/bookmarks/{id}` | 删除 | 删除给定 ID 的现有书签 |将 REST API 资源配置到 HTTP 复用器中
让我们将 REST API 的资源映射到 HTTP 请求多路复用器中。包mux
被用作这个应用程序的 HTTP 请求多路复用器。以下命令安装软件包mux:
go get github.com/gorilla/mux
要使用包mux
,您必须将github.com/gorilla/mux
添加到导入列表中。
import " github.com/gorilla/mux "
清单 7-8 显示了函数SetBookmarkRoutes
,它将资源端点和Bookmark
实体的相应应用程序处理程序注册到 HTTP 请求多路复用器中。在这里,您希望在单独的函数中组织每个实体的多路复用器配置,以便您可以轻松地维护应用程序的 HTTP 路由。如果您想为User
实体添加一个多路复用器配置,您可以在另一个函数中组织它。这些函数最终从routers.go
的函数InitRoutes
中调用。应用处理程序被组织到包controllers
中。
package routers
import (
"github.com/gorilla/mux"
"github.com/shijuvar/go-recipes/ch07/bookmarkapi/controllers"
)
// SetBookmarkRoutes registers routes for bookmark entity.
func SetBookmarkRoutes(router *mux.Router) *mux.Router {
router.HandleFunc("/bookmarks", controllers.CreateBookmark).Methods("POST")
router.HandleFunc("/bookmarks/{id}", controllers.UpdateBookmark).Methods("PUT")
router.HandleFunc("/bookmarks", controllers.GetBookmarks).Methods("GET")
router.HandleFunc("/bookmarks/{id}", controllers.GetBookmarkByID).Methods("GET")
router.HandleFunc("/bookmarks/users/{id}", controllers.GetBookmarksByUser).Methods("GET")
router.HandleFunc("/bookmarks/{id}", controllers.DeleteBookmark).Methods("DELETE")
return router
}
Listing 7-8.Configuration for the HTTP Request Multiplexer in routers/bookmark.go
类型mux.Router
用于注册 HTTP 路由及其相应的处理函数。它实现了接口http.Handler
,因此它与包http
的类型ServeMux
兼容。函数HandleFunc
向 URL 路径的匹配器注册一个新的路由。该功能的工作方式类似于http.ServeMux
的功能HandleFunc
。从routers.go
的函数 I nitRoutes
中调用函数SetBookmarkRoutes
,如清单 7-9 所示。
package routers
import (
"github.com/gorilla/mux"
)
// InitRoutes registers all routes for the application.
func InitRoutes() *mux.Router {
router := mux.NewRouter().StrictSlash(false)
// Routes for the Bookmark entity
router = SetBookmarkRoutes(router)
// Call other router configurations
return router
}
Listing 7-9.Initializing Routes in routers/routers.go
通过调用函数mux.NewRouter
创建一个新的mux.router
实例。从包main
的main.go
中调用函数InitRoutes
,以配置应用程序的路由,供 HTTP 服务器使用。
管理 mgo。会议
CChapter 6 讨论了如何使用包mgo
处理 MongoDB 数据库。当包mgo
用于 MongoDB 时,首先通过调用mgo.Dial
或mgo.DialWithInfo
获得一个mgo.Session
值。mgo.Session
实例用于对 MongoDB 集合执行 CRUD 操作。但是,不建议在应用程序中对所有 CRUD 操作使用全局mgo.Session
值。使用mgo.Session
值的一个良好实践是使用从全局mgo.Session
值复制的mgo.Session
值用于数据持久化会话。当您编写 web 应用程序时,一个好的实践是为每个 HTTP 请求生命周期使用全局mgo.Session
值的复制值。类型mgo.Session
提供函数Copy
,该函数可用于创建mgo.Session
值的副本。您还可以使用函数Clone
,该函数提供了mgo.Session
值的克隆版本,以制作mgo.Session
的副本,从而为数据持久化会话执行 CRUD 操作。复制和克隆的会话都将重用来自全局mgo.Session
的同一个连接池,该连接池是使用Dial
或DialWithInfo
获得的。函数Clone
就像Copy
一样工作,但是也重用了与原始会话相同的套接字。REST API 示例使用函数Copy
生成一个复制的mgo.Session
值,该值将在单个 HTTP 请求生命周期中使用。
清单 7-10 显示了包common
中mongo_utils.go
的源代码,它提供了使用 MongoDB 的帮助函数,包括一个名为DataStore
的结构类型,该结构类型提供了用于每个 HTTP 请求生命周期的全局mgo.Session
的副本。
package common
import (
"log"
"time"
"gopkg.in/mgo.v2"
)
var session *mgo.Session
// GetSession returns a MongoDB Session
func getSession() *mgo.Session {
if session == nil {
var err error
session, err = mgo.DialWithInfo(&mgo.DialInfo{
Addrs: []string{AppConfig.MongoDBHost},
Username: AppConfig.DBUser,
Password: AppConfig.DBPwd,
Timeout: 60 * time.Second,
})
if err != nil {
log.Fatalf("[GetSession]: %s\n", err)
}
}
return session
}
func createDBSession() {
var err error
session, err = mgo.DialWithInfo(&mgo.DialInfo{
Addrs: []string{AppConfig.MongoDBHost},
Username: AppConfig.DBUser,
Password: AppConfig.DBPwd,
Timeout: 60 * time.Second,
})
if err != nil {
log.Fatalf("[createDbSession]: %s\n", err)
}
}
// DataStore for MongoDB
type DataStore struct {
MongoSession *mgo.Session
}
// Close closes an mgo.Session value.
// Used to add defer statements for closing the copied session.
func (ds *DataStore) Close() {
ds.MongoSession.Close()
}
// Collection returns mgo.collection for the given name
func (ds *DataStore) Collection(name string) *mgo.Collection {
return ds.MongoSession.DB(AppConfig.Database).C(name)
}
// NewDataStore creates a new DataStore object to be used for each HTTP request.
func NewDataStore() *DataStore {
session := getSession().Copy()
dataStore := &DataStore{
MongoSession: session,
}
return dataStore
}
Listing 7-10.Helper Functions for mgo.Session in common/mongo_utils.go
函数createDBSession
创建一个全局mgo.Session
值,在运行 HTTP 服务器之前,这个函数将被立即调用。函数getSession
返回全局mgo.Session
值。通过创建mgo.Session
的副本,从应用程序处理程序创建结构类型DataStore
的实例,以与 MongoDB 数据库一起工作。函数NewDataStore
通过提供全局mgo.Session
值的副本来创建DataStore
的新实例。
func NewDataStore() *DataStore {
session := getSession().Copy()
dataStore := &DataStore{
MongoSession: session,
}
return dataStore
}
JSON 资源的模型
示例 REST API 应用程序是一个基于 JSON 的 REST API,其中 JSON 格式用于在 HTTP 请求和响应中发送和接收数据。为了满足 JSON API 规范( http://jsonapi.org/
),让我们定义用于 HTTP 请求和 HTTP 响应的数据模型。这里您定义了 JSON 表示的模型,其中元素名"data"
被定义为 HTTP 请求和 HTTP 响应主体中所有 JSON 表示的根。清单 7-11 展示了 JSON 表示的数据模型。
package controllers
import (
"github.com/shijuvar/go-recipes/ch07/bookmarkapi/model"
)
//Models for JSON resources
type (
// BookmarkResource for Post and Put - /bookmarks
// For Get - /bookmarks/id
BookmarkResource struct {
Data model.Bookmark `json:"data"`
}
// BookmarksResource for Get - /bookmarks
BookmarksResource struct {
Data []model.Bookmark `json:"data"`
}
)
Listing 7-11.Data Models for JSON Resources in controllers/resources.go
应用程序处理程序使用该类型从http.Request
的主体接收数据,并将数据写入http.ResponseWriter
。
书签资源的 HTTP 处理程序
以下是为Bookmarks
资源配置的路线:
router.HandleFunc("/bookmarks", controllers.CreateBookmark).Methods("POST")
router.HandleFunc("/bookmarks/{id}", controllers.UpdateBookmark).Methods("PUT")
router.HandleFunc("/bookmarks", controllers.GetBookmarks).Methods("GET")
router.HandleFunc("/bookmarks/{id}", controllers.GetBookmarkByID).Methods("GET")
router.HandleFunc("/bookmarks/users/{id}", controllers.GetBookmarksByUser).Methods("GET")
router.HandleFunc("/bookmarks/{id}", controllers.DeleteBookmark).Methods("DELETE")
Bookmarks
资源的 HTTP 处理函数是用bookmark_controller.go
编写的,它被组织到包controllers
中。清单 7-12 显示了为Bookmarks
资源提供 HTTP 请求的处理函数。
package controllers
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
"github.com/shijuvar/go-recipes/ch07/bookmarkapi/common"
"github.com/shijuvar/go-recipes/ch07/bookmarkapi/store"
)
// CreateBookmark insert a new Bookmark.
// Handler for HTTP Post - "/bookmarks
func CreateBookmark(w http.ResponseWriter, r *http.Request) {
var dataResource BookmarkResource
// Decode the incoming Bookmark json
err := json.NewDecoder(r.Body).Decode(&dataResource)
if err != nil {
common.DisplayAppError(
w,
err,
"Invalid Bookmark data",
500,
)
return
}
bookmark := &dataResource.Data
// Creates a new DataStore value to work with MongoDB store.
dataStore := common.NewDataStore()
// Add to the mgo.Session.Close()
defer dataStore.Close()
// Get the mgo.Collection for "bookmarks"
col := dataStore.Collection("bookmarks")
// Creates an instance of BookmarkStore
bookmarkStore := store.BookmarkStore{C: col}
// Insert a bookmark document
err = bookmarkStore.Create(bookmark)
if err != nil {
common.DisplayAppError(
w,
err,
"Invalid Bookmark data",
500,
)
return
}
j, err := json.Marshal(BookmarkResource{Data: *bookmark})
// If error has occurred,
// Send JSON response using helper function common.DisplayAppError
if err != nil {
common.DisplayAppError(
w,
err,
"An unexpected error has occurred",
500,
)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
// Write the JSON data to the ResponseWriter
w.Write(j)
}
// GetBookmarks returns all Bookmark documents
// Handler for HTTP Get - "/Bookmarks"
func GetBookmarks(w http.ResponseWriter, r *http.Request) {
dataStore := common.NewDataStore()
defer dataStore.Close()
col := dataStore.Collection("bookmarks")
bookmarkStore := store.BookmarkStore{C: col}
bookmarks := bookmarkStore.GetAll()
j, err := json.Marshal(BookmarksResource{Data: bookmarks})
if err != nil {
common.DisplayAppError(
w,
err,
"An unexpected error has occurred",
500,
)
return
}
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
w.Write(j)
}
// GetBookmarkByID returns a single bookmark document by id
// Handler for HTTP Get - "/Bookmarks/{id}"
func GetBookmarkByID(w http.ResponseWriter, r *http.Request) {
// Get id from the incoming url
vars := mux.Vars(r)
id := vars["id"]
dataStore := common.NewDataStore()
defer dataStore.Close()
col := dataStore.Collection("bookmarks")
bookmarkStore := store.BookmarkStore{C: col}
bookmark, err := bookmarkStore.GetByID(id)
if err != nil {
if err == mgo.ErrNotFound {
w.WriteHeader(http.StatusNoContent)
} else {
common.DisplayAppError(
w,
err,
"An unexpected error has occurred",
500,
)
}
return
}
j, err := json.Marshal(bookmark)
if err != nil {
common.DisplayAppError(
w,
err,
"An unexpected error has occurred",
500,
)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(j)
}
// GetBookmarksByUser returns all Bookmarks created by a User
// Handler for HTTP Get - "/Bookmarks/users/{id}"
func GetBookmarksByUser(w http.ResponseWriter, r *http.Request) {
// Get id from the incoming url
vars := mux.Vars(r)
user := vars["id"]
dataStore := common.NewDataStore()
defer dataStore.Close()
col := dataStore.Collection("bookmarks")
bookmarkStore := store.BookmarkStore{C: col}
bookmarks := bookmarkStore.GetByUser(user)
j, err := json.Marshal(BookmarksResource{Data: bookmarks})
if err != nil {
common.DisplayAppError(
w,
err,
"An unexpected error has occurred",
500,
)
return
}
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
w.Write(j)
}
// UpdateBookmark update an existing Bookmark document
// Handler for HTTP Put - "/Bookmarks/{id}"
func UpdateBookmark(w http.ResponseWriter, r *http.Request) {
// Get id from the incoming url
vars := mux.Vars(r)
id := bson.ObjectIdHex(vars["id"])
var dataResource BookmarkResource
// Decode the incoming Bookmark json
err := json.NewDecoder(r.Body).Decode(&dataResource)
if err != nil {
common.DisplayAppError(
w,
err,
"Invalid Bookmark data",
500,
)
return
}
bookmark := dataResource.Data
bookmark.ID = id
dataStore := common.NewDataStore()
defer dataStore.Close()
col := dataStore.Collection("bookmarks")
bookmarkStore := store.BookmarkStore{C: col}
// Update an existing Bookmark document
if err := bookmarkStore.Update(bookmark); err != nil {
common.DisplayAppError(
w,
err,
"An unexpected error has occurred",
500,
)
return
}
w.WriteHeader(http.StatusNoContent)
}
// DeleteBookmark deletes an existing Bookmark document
// Handler for HTTP Delete - "/Bookmarks/{id}"
func DeleteBookmark(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
dataStore := common.NewDataStore()
defer dataStore.Close()
col := dataStore.Collection("bookmarks")
bookmarkStore := store.BookmarkStore{C: col}
// Delete an existing Bookmark document
err := bookmarkStore.Delete(id)
if err != nil {
common.DisplayAppError(
w,
err,
"An unexpected error has occurred",
500,
)
return
}
w.WriteHeader(http.StatusNoContent)
}
Listing 7-12.HTTP Handler Functions for Bookmarks Resource in controllers/bookmark_controller.go
HTTP Post 和 HTTP Put 的 HTTP 处理函数对来自请求体的 JSON 数据进行解码,并将其解析到为 JSON 资源创建的模型中。在这里,它被解析成结构类型BookmarkResource
。这里是controllers
包装resources.go
中写的BookmarkResource
声明。
BookmarkResource struct {
Data model.Bookmark `json:"data"`
}
通过访问BookmarkResource
的属性Data
,传入的数据被映射到域模型model.Bookmark
,并使用它的值执行数据持久化逻辑。
var dataResource BookmarkResource
// Decode the incoming Bookmark json
err := json.NewDecoder(r.Body).Decode(&dataResource)
bookmark := &dataResource.Data
结构类型common.DataStore
用于维护全局mgo.Session
值的复制版本,在单个 HTTP 请求生命周期中使用。DataStore
的方法Collection
返回一个mgo.Collection
值。mgo.Collection
值用于创建store.BookmarkStore
的实例。包store
的结构类型BookmarkStore
提供了针对数据模型Bookmark
的持久化逻辑,该数据模型针对名为"bookmarks"
的 MongoDB 集合工作。
dataStore := common.NewDataStore()
// Add to the mgo.Session.Close()
defer dataStore.Close()
// Get the mgo.Collection for "bookmarks"
col := dataStore.Collection("bookmarks")
// Creates an instance of BookmarkStore
bookmarkStore := store.BookmarkStore{C: col}
BookmarkStore
的方法用于对 MongoDB 数据库执行 CRUD 操作。BookmarkStore
的函数Create
用于向 MongoDB 集合中插入一个新文档。
// Insert a bookmark document
err=bookmarkStore.Create(bookmark)
如果在执行了BookmarkStore
的持久化逻辑之后,返回的error
值是nil
,那么一个适当的 HTTP 响应被发送到 HTTP 客户端。下面是从 HTTP Post 的处理函数发送到"/bookmarks"
的 HTTP 响应:
j, err := json.Marshal(BookmarkResource{Data: *bookmark})
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
// Write the JSON data to the ResponseWriter
w.Write(j)
这里,使用model.Bookmark
的值创建一个结构类型BookmarkResource
,并使用json.Marshal
将其编码到 JSON 中。如果从处理函数接收到任何error
值,将使用一个助手函数common.DisplayAppError
以 JSON 格式发送 HTTP 错误消息。
// Insert a bookmark document
err = bookmarkStore.Create(bookmark)
if err != nil {
common.DisplayAppError(
w,
err,
"Invalid Bookmark data",
500,
)
return
}
下面是助手函数DisplayAppError
的实现:
// DisplayAppError provides app specific error in JSON
func DisplayAppError(w http.ResponseWriter, handlerError error, message string, code int) {
errObj := appError{
Error: handlerError.Error(),
Message: message,
HTTPStatus: code,
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
if j, err := json.Marshal(errorResource{Data: errObj}); err == nil {
w.Write(j)
}
}
HTTP Put、Get 和 Delete 的处理函数从 HTTP 请求的 URL 的路由变量中检索值。包mux
提供了一个函数Vars
,它返回当前请求的路由变量,作为类型为map[string]string
的集合的键/值对。下面的代码块检索路由变量"id"
的值。
vars := mux.Vars(r)
id := vars["id"]
MongoDB 的数据持久化
bookmark_controller.go
的 HTTP 处理函数使用数据持久化逻辑的结构类型BookmarkStore
对名为"bookmarks"
的 MongoDB 集合执行 CRUD 操作。清单 7-13 显示了BookmarkStore
提供的数据持久逻辑。
package store
import (
"time"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
"github.com/shijuvar/go-recipes/ch07/bookmarkapi/model"
)
// BookmarkStore provides CRUD operations against the collection "bookmarks".
type BookmarkStore struct {
C *mgo.Collection
}
// Create inserts the value of struct Bookmark into collection.
func (store BookmarkStore) Create(b *model.Bookmark) error {
// Assign a new bson.ObjectId
b.ID = bson.NewObjectId()
b.CreatedOn = time.Now()
err := store.C.Insert(b)
return err
}
// Update modifies an existing document of a collection.
func (store BookmarkStore) Update(b model.Bookmark) error {
// partial update on MogoDB
err := store.C.Update(bson.M{"_id": b.ID},
bson.M{"$set": bson.M{
"name": b.Name,
"description": b.Description,
"location": b.Location,
"priority": b.Priority,
"tags": b.Tags,
}})
return err
}
// Delete removes an existing document from the collection.
func (store BookmarkStore) Delete(id string) error {
err := store.C.Remove(bson.M{"_id": bson.ObjectIdHex(id)})
return err
}
// GetAll returns all documents from the collection.
func (store BookmarkStore) GetAll() []model.Bookmark {
var b []model.Bookmark
iter := store.C.Find(nil).Sort("priority", "-createdon").Iter()
result := model.Bookmark{}
for iter.Next(&result) {
b = append(b, result)
}
return b
}
// GetByUser returns all documents from the collection.
func (store BookmarkStore) GetByUser(user string) []model.Bookmark {
var b []model.Bookmark
iter := store.C.Find(bson.M{"createdby": user}).Sort("priority", "-createdon").Iter()
result := model.Bookmark{}
for iter.Next(&result) {
b = append(b, result)
}
return b
}
// GetByID returns a single document from the collection.
func (store BookmarkStore) GetByID(id string) (model.Bookmark, error) {
var b model.Bookmark
err := store.C.FindId(bson.ObjectIdHex(id)).One(&b)
return b, err
}
Listing 7-13.Data Persistence Logic in store/bookmark_store.go
运行 HTTP 服务器
REST API 的 HTTP 服务器从main.go
开始创建和运行。清单 7-14 显示了main.go.
的来源
package main
import (
"log"
"net/http"
"github.com/shijuvar/go-recipes/ch07/bookmarkapi/common"
"github.com/shijuvar/go-recipes/ch07/bookmarkapi/routers"
)
// Entry point of the program
func main() {
// Calls startup logic
common.StartUp()
// Get the mux router object
router := routers.InitRoutes()
// Create the Server
server := &http.Server{
Addr: common.AppConfig.Server,
Handler: router,
}
log.Println("Listening...")
// Running the HTTP Server
server.ListenAndServe()
}
Listing 7-14.Running HTTP Server in main.go
在函数main
内部,使用函数common.StartUp
执行一些启动逻辑。在运行 HTTP 服务器之前,对common.StartUp
的调用执行几个必需的函数。这包括读取应用程序配置文件并将值加载到 struct 实例中,使用函数mgo.DialWithInfo
连接到 MongoDB 数据库,获得一个mgo.Session
值,等等。包common
提供了运行 HTTP 服务器之前所需的启动逻辑。通过调用返回mux.Router
的routers.InitRoutes
来创建http.Handler
值。mux.Router
有一个接口http.Handler
的实现,因此它被用作 HTTP 服务器的处理程序。
测试 REST API 服务器
让我们运行 HTTP 服务器并测试书签资源的一些 API 端点。Postman ( https://www.getpostman.com/
)用于测试 API 端点。图 7-6 显示了对"/bookmarks"
的 HTTP Post 请求。
图 7-6。
Sending HTTP Post to "/bookmarks"
图 7-7 显示了 API 服务器对 HTTP Post 到"/bookmarks"
的响应。它显示了新创建的资源的 HTTP 状态代码 201 和 JSON。
图 7-7。
HTTP Response for the HTTP Post to "/bookmarks"
让我们再向服务器发送一个 HTTP Post 请求,并测试 HTTP Get 请求。图 7-8 显示了对"/bookmarks"
的 HTTP Get 请求的响应。它显示了所有书签资源的 JSON 数据。
图 7-8。
HTTP Response for the HTTP Get to "/bookmarks"
图 7-9 显示了对"/bookmarks/{id}"
的 HTTP Get 请求的响应。它显示了给定书签 ID 的单个书签资源的 JSON 数据。
图 7-9。
HTTP Response for the HTTP Get request to "/bookmarks/{id}"
图 7-10 显示了对"/bookmarks/users/{id}"
的 HTTP Get 请求的响应。它显示了与给定用户 ID 的用户相关联的所有书签资源的 JSON 数据。
图 7-10。
HTTP Response for the HTTP Get request to "/bookmarks/users/{id}"
REST API 应用程序的完整版本可从本书的代码库中获得,地址为 https://github.com/shijuvar/go-recipes
。
八、测试 Go 应用
软件工程是一个进化过程,在这个过程中,你将应用程序作为一个进化系统来开发,并且不断地修改和重构应用程序。您应该能够随时修改应用程序的功能并重构其代码,而不会破坏应用程序的任何部分。当您将应用程序开发为一个进化产品并修改应用程序代码时,不应该破坏应用程序的任何部分。您可能需要采用一些良好的工程实践来确保您的应用程序的质量。自动化测试是一项重要的工程实践,可以用来确保软件系统的质量。在自动化测试过程中,您针对应用程序中最小的可测试软件(称为单元)编写单元测试,以确定每个单元的功能是否完全符合您的预期。在本章中,你将学习如何在 Go 中编写单元测试。
8-1.编写单元测试
问题
你如何编写单元测试来确保你的 Go 包如你所愿的那样运行?
解决办法
标准库包testing
为编写 Go 包的单元测试提供支持。命令运行用包testing
编写的单元测试。
它是如何工作的
包testing
为编写单元测试提供了所有必要的支持,旨在与运行单元测试的go test
命令一起使用。go test
命令通过查看函数中的以下模式来识别单元测试函数:
func TestXxx(*testing.T)
您编写的单元测试函数带有前缀Test
,后跟一个以大写字母开头的字母数字字符串。要编写单元测试函数,您必须创建一个测试套件文件,其名称以_test.go
结尾,包含带有签名func TestXxx(*testing.T)
的单元测试函数。您通常将测试套件文件放在被测试的同一个包中。当您使用go build
或go install
构建包时,它不包括测试套件文件,当您使用go test
运行单元测试时,它包括测试套件文件。
要获得运行go test
的帮助,请运行以下命令:
go help test
要获得关于go test
命令使用的各种标志的帮助,请运行以下命令:
go help testflag
清单 8-1 展示了一个示例包,稍后您将为其编写一个单元测试。
package calc
import "math"
// Sum returns sum of integer values
func Sum(nums ...int) int {
result := 0
for _, v := range nums {
result += v
}
return result
}
// Average returns average of integer values
// The output provides a float64 value in two decimal points
func Average(nums ...int) float64 {
sum := 0
for _, v := range nums {
sum += v
}
result := float64(sum) / float64(len(nums))
pow := math.Pow(10, float64(2))
digit := pow * result
round := math.Floor(digit)
return round / pow
}
Listing 8-1.Package calc with Two Utility Functions in calc.go
列表 8-2 显示了测试包calc
中函数Sum
和Average
行为的单元测试。
package calc
import "testing"
// Test case for the Sum function
func TestSum(t *testing.T) {
input, expected := []int{7, 8, 10}, 25
result := Sum(input...)
if result != expected {
t.Errorf("Result: %d, Expected: %d", result, expected)
}
}
// Test case for the Sum function
func TestAverage(t *testing.T) {
input, expected := []int{7, 8, 10}, 8.33
result := Average(input...)
if result != expected {
t.Errorf("Result: %f, Expected: %f", result, expected)
}
}
Listing 8-2.Unit Tests for Package calc in calc_test.go
编写了两个测试用例来验证包calc
中函数的行为。单元测试函数的名称以前缀Test
开始,后面是一个以大写字母开始的字符串。在单元测试函数中,函数的输出值验证期望值,并调用方法Errorf
来通知失败。为了发出测试用例失败的信号,你可以调用Error
、Fail
或者testing.T
类型的相关方法。Error
和Fail
方法发出测试用例失败的信号,但是它将继续执行剩余的单元测试。如果您想在测试用例失败时停止执行,您可以调用类型testing.T
的方法FailNow
或Fatal
。方法FailNow
调用方法Fail
并停止执行。Fatal
相当于Log
后跟FailNow
。在这些单元测试功能中,方法Errorf
被用于发出测试用例失败的信号。
if result != expected {
t.Errorf("Result: %d, Expected: %d", result, expected)
}
运行单元测试
要运行单元测试,从您的package
目录运行go test
命令:
go test
您应该会看到类似如下的输出:
PASS
ok github.com/shijuvar/go-recipes/ch08/calc 0.233s
这个测试的输出不是很有描述性。当您执行单元测试时,verbose (-v)
标志提供了描述性的信息。
go test –v
这将产生类似于以下内容的输出:
=== RUN TestSum
--- PASS: TestSum (0.00s)
=== RUN TestAverage
--- PASS: TestAverage (0.00s)
PASS
ok github.com/shijuvar/go-recipes/ch08/calc 0.121s
请注意,单元测试是按顺序执行的。在这些测试中,它首先执行测试函数TestSum
,在完成执行后,它接着执行测试函数TestAverage
。
测试覆盖率
当您运行单元测试时,您可以测量由测试用例执行的测试量。go test
命令提供了一个覆盖率(-cover)
标志,帮助你获得针对你的代码编写的测试用例的覆盖率。让我们运行带有覆盖率标志的单元测试来确定包calc
的测试覆盖率:
go test –v –cover
您应该会看到类似如下的输出:
=== RUN TestSum
--- PASS: TestSum (0.00s)
=== RUN TestAverage
--- PASS: TestAverage (0.00s)
PASS
coverage: 100.0% of statements
ok github.com/shijuvar/go-recipes/ch08/calc 0.139s
这个测试输出显示包calc
有 100%的测试覆盖率。
8-2.跳过长期运行的测试
问题
在运行测试时,您希望能够灵活地跳过一些单元测试。在运行单元测试时,如何跳过一些单元测试的执行?
解决办法
go test
命令允许您传递一个简短的(-short)
标志,让您在执行过程中跳过一些单元测试。在单元测试函数中,您可以通过调用包testing
中的函数Short
来检查是否提供了短标志,如果您想跳过这些测试,可以通过调用testing.T
类型的函数Skip
来跳过测试的执行。
它是如何工作的
当您执行单元测试时,您可能需要跳过其中的一些。有时,您可能想要阻止某些单元测试在某些用例中执行。例如,您可能想跳过一些耗时的单元测试。另一个示例场景是,一些单元测试可能依赖于在这些测试执行期间不可用的配置文件或环境变量,因此您可以跳过这些测试的执行,而不是让它们失败。
类型testing.T
提供了一个可以用来跳过单元测试的方法Skip
。为了跳过那些单元测试,您可以通过向go test
命令提供一个短的(-short)
标志来给出一个信号。清单 8-3 显示了三个单元测试函数,其中如果你给go test
命令提供一个短的(-short)
标志,在测试执行过程中会跳过一个测试。
package calc
import (
"testing"
"time"
)
// Test case for the Sum function
func TestSum(t *testing.T) {
input, expected := []int{7, 8, 10}, 25
result := Sum(input...)
if result != expected {
t.Errorf("Result: %d, Expected: %d", result, expected)
}
}
// Test case for the Sum function
func TestAverage(t *testing.T) {
input, expected := []int{7, 8, 10}, 8.33
result := Average(input...)
if result != expected {
t.Errorf("Result: %f, Expected: %f", result, expected)
}
}
// TestLongRun is a time-consuming test
func TestLongRun(t *testing.T) {
// Checks whether the short flag is provided
if testing.Short() {
t.Skip("Skipping test in short mode")
}
// Long running implementation goes here
time.Sleep(5 * time.Second)
}
Listing 8-3.Unit Tests in Which One Test is Skipped in Execution
在这些单元测试中,如果您可以为go test
命令提供一个短标志,那么您可以跳过函数TestLongRun
的测试执行。函数testing.Short
用于识别是否提供了短标志。如果是,通过调用函数Skip
跳过单元测试的执行。当您调用函数Skip
时,您可以提供一个字符串值。
// Checks whether the short flag is provided
if testing.Short() {
t.Skip("Skipping test in short mode")
}
如果不提供短标志,函数TestLongRun
将作为正常的单元测试运行。让我们通过提供短标志来运行测试:
go test -v -short
您应该会看到类似如下的输出:
=== RUN TestSum
--- PASS: TestSum (0.00s)
=== RUN TestAverage
--- PASS: TestAverage (0.00s)
=== RUN TestLongRun
--- SKIP: TestLongRun (0.00s)
calc_test.go:36: Skipping test in short mode
PASS
ok github.com/shijuvar/go-recipes/ch08/calc 0.241s
测试输出显示单元测试函数TestLongRun
在执行过程中被跳过。现在让我们在不提供短标志的情况下运行测试:
go test -v
这将产生类似于以下内容的输出:
=== RUN TestSum
--- PASS: TestSum (0.00s)
=== RUN TestAverage
--- PASS: TestAverage (0.00s)
=== RUN TestLongRun
--- PASS: TestLongRun (5.00s)
PASS
ok github.com/shijuvar/go-recipes/ch08/calc 5.212s
测试输出显示函数TestLongRun
正常运行。
8-3.编写基准测试
问题
如何通过编写测试来对 Go 代码进行基准测试?
解决办法
包testing
允许你为基准 Go 函数编写测试。为了编写基准,编写模式为func BenchmarkXxx(*testing.B)
的函数,当go test
命令的-bench
标志被提供时,这些函数被执行。
它是如何工作的
当您使用go test
命令运行测试时,您可以传递-bench
标志来执行 bechmark 测试,其中具有模式func BenchmarkXxx(*testing.B)
的函数被视为基准。您在_test.go
文件中编写基准函数。清单 8-4 显示了对软件包calc
功能的基准测试(参见清单 8-1 )。
package calc
import "testing"
// Test case for function Sum
func TestSum(t *testing.T) {
input, expected := []int{7, 8, 10}, 25
result := Sum(input...)
if result != expected {
t.Errorf("Result: %d, Expected: %d", result, expected)
}
}
// Test case for function Average
func TestAverage(t *testing.T) {
input, expected := []int{7, 8, 10}, 8.33
result := Average(input...)
if result != expected {
t.Errorf("Result: %f, Expected: %f", result, expected)
}
}
// Benchmark for function Sum
func BenchmarkSum(b *testing.B) {
for i := 0; i < b.N; i++ {
Sum(7, 8, 10)
}
}
// Benchmark for function Average
func BenchmarkAverage(b *testing.B) {
for i := 0; i < b.N; i++ {
Average(7, 8, 10)
}
}
Listing 8-4.Unit Tests with Benchmarks in Package calc
编写了两个基准测试来测试包calc
中函数的性能。您必须使用循环结构运行目标代码b.N
次,以便以可靠的方式执行要进行基准测试的函数。在执行基准测试期间,b.N
的值将被调整。基准测试为您提供了每个循环的可靠响应时间。当您向go test
命令提供-bench
标志时,您需要提供一个正则表达式来指示要执行哪些基准测试。要运行所有基准,使用-bench .
或-bench=.
让我们通过提供-bench .
来运行测试
go test -v -bench .
您应该会看到类似如下的输出:
=== RUN TestSum
--- PASS: TestSum (0.00s)
=== RUN TestAverage
--- PASS: TestAverage (0.00s)
BenchmarkSum-4 100000000 23.1 ns/op
BenchmarkAverage-4 10000000 224 ns/op
PASS
ok github.com/shijuvar/go-recipes/ch08/calc 4.985s
8-4.并行运行单元测试
问题
如何并行执行单元测试?
解决办法
您可以通过调用类型为testing.T
的方法Parallel
来并行运行单元测试。在单元测试函数内部,对方法Parallel
的调用表明这个测试将与其他并行测试并行运行。
它是如何工作的
默认情况下,单元测试按顺序执行。如果您想并行运行一个单元测试来加速执行,那么在编写测试逻辑之前调用测试函数内部的方法Parallel
。方法Parallel
表明这个单元测试将与其他并行测试并行运行。您可以为任何想要并行运行的单元测试函数调用方法Parallel
。
清单 8-5 提供了几个并行运行的单元测试。
package calc
import (
"testing"
"time"
)
// Test case for the function Sum to be executed in parallel
func TestSumInParallel(t *testing.T) {
t.Parallel()
// Delaying 1 second for the sake of demonstration
time.Sleep(1 * time.Second)
input, expected := []int{7, 8, 10}, 25
result := Sum(input...)
if result != expected {
t.Errorf("Result: %d, Expected: %d", result, expected)
}
}
// Test case for the function Sum to be executed in parallel
func TestAverageInParallel(t *testing.T) {
t.Parallel()
// Delaying 1 second for the sake of demonstration
time.Sleep(2 * time.Second)
input, expected := []int{7, 8, 10}, 8.33
result := Average(input...)
if result != expected {
t.Errorf("Result: %f, Expected: %f", result, expected)
}
}
Listing 8-5.Unit Tests to Be Run in Parallel
在测试函数内部,方法Parallel
作为第一个代码语句被调用,以表示该测试将并行运行,这样并行测试的执行将不会等待测试函数的完成,而是与其他并行测试并行运行。
t.Parallel()
如果您编写混合了并行测试和普通测试的单元测试函数,它将顺序执行普通测试,并与其他并行测试并行执行并行测试。用go test
命令运行测试:
go test -v
您应该会看到类似如下的输出:
=== RUN TestSumInParallel
=== RUN TestAverageInParallel
--- PASS: TestSumInParallel (1.00s)
--- PASS: TestAverageInParallel (2.00s)
PASS
ok github.com/shijuvar/go-recipes/ch08/calc 2.296s
输出显示TestSumInParallel
和TestAverageInParallel
正在并行运行,并且没有等待一个测试完成就运行另一个测试。
8-5.编写用于验证示例代码的测试
问题
如何编写测试来验证示例代码?
解决办法
包testing
提供了对编写测试来验证示例代码的支持。要编写示例函数,请使用以前缀Example
开头的名称来声明函数。
它是如何工作的
示例函数验证为包、类型和函数编写的示例代码。示例函数也可以在由godoc
工具生成的 Go 文档中找到。当您使用godoc
工具生成 Go 文档时,示例函数将作为 Go 包和各种类型的示例代码。示例函数以前缀Example
开头的名称声明。下面是用于声明包示例的命名约定,一个函数F
,一个类型T
,以及类型T
上的方法M
:
func Example() // Example test for package
func ExampleF() // Example test for function F
func ExampleT() // Example test for type T
func ExampleT_M() // Example test for M on type T
在示例函数中,通常包含一个以Output:
开头的结束行注释。当使用go test
命令执行测试功能时,它将给定输出与功能输出进行比较。清单 8-6 展示了包calc
中的示例函数。
package calc
import "fmt"
// Example code for function Sum
func ExampleSum() {
fmt.Println(Sum(7, 8, 10))
// Output: 25
}
// Example code for function Average
func ExampleAverage() {
fmt.Println(Average(7, 8, 10))
// Output: 8.33
}
Listing 8-6.Example Functions for Package calc
编写函数Sum
示例代码的约定是ExampleSum
,函数Average
的约定是ExampleAverage
。在示例测试函数中,提供了一个以Output:
开头的总结行注释。行注释的输出与函数的标准输出进行比较。在示例函数ExampleSum
中,行注释的输出与对Sum
的函数调用的输出进行比较。
让我们用go test
命令运行示例函数:
go test -v
您应该会看到类似如下的输出:
=== RUN ExampleSum
--- PASS: ExampleSum (0.00s)
=== RUN ExampleAverage
--- PASS: ExampleAverage (0.00s)
PASS
ok github.com/shijuvar/go-recipes/ch08/calc 0.165s
示例测试功能将作为示例代码出现在由godoc
工具生成的文档中。图 8-1 显示了函数Sum
的文档,其中包括取自测试函数ExampleSum
的示例代码。
图 8-1。
Documentation for function Sum
generated by godoc
tool
图 8-2 显示了函数Average
的文档,其中包括取自测试函数ExampleAverage
的示例代码。
图 8-2。
Documentation for function Average
generated by godoc
tool
8-6.测试 HTTP 应用程序
问题
如何为 HTTP 应用程序编写测试?
解决办法
标准库包net/http/httptest
提供了测试 HTTP 应用程序的工具。
它是如何工作的
包httptest
为测试 HTTP 应用程序提供支持。为了测试 HTTP 应用程序,包httptest
提供了ResponseRecorder
和Server
结构类型。
ResponseRecorder
是http.ResponseWriter
的实现,它记录 HTTP 响应以检查单元测试中的响应。您可以通过使用记录处理函数中http.ResponseWriter
突变的ResponseRecorder
来验证http.ResponseWriter
在测试中的行为。当您使用ResponseRecorder
测试您的 HTTP 应用程序时,您不需要使用 HTTP 服务器。通过调用包httptest
的函数NewRecorder
创建一个ResponseRecorder
实例。
w := httptest.NewRecorder()
Server
是一个测试 HTTP 服务器,它监听本地环回接口(127.0.0.1)上系统选择的端口,用于端到端 HTTP 测试。这允许您通过从 HTTP 客户端向测试服务器发送 HTTP 请求,使用 HTTP 服务器测试您的 HTTP 应用程序。通过提供一个http.Handler
的实例,调用包httptest
的函数NewServer
来创建测试 HTTP 服务器。
server := httptest.NewServer(r) // r is an instance of http.Handler
HTTP API 服务器
清单 8-7 展示了一个例子 HTTP API 服务器,它是为稍后用包httptest
编写单元测试而创建的。
package main
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
)
// User model
type User struct {
FirstName string `json:"firstname"`
LastName string `json:"lastname"`
Email string `json:"email"`
}
// getUsers serves requests for Http Get to "/users"
func getUsers(w http.ResponseWriter, r *http.Request) {
data := []User{
User{
FirstName: "Shiju",
LastName: "Varghese",
Email: "shiju@xyz.com",
},
User{
FirstName: "Irene",
LastName: "Rose",
Email: "irene@xyz.com",
},
}
users, err := json.Marshal(data)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(users)
}
func main() {
r := mux.NewRouter()
r.HandleFunc("/users", getUsers).Methods("GET")
http.ListenAndServe(":8080", r)
}
Listing 8-7.Example HTTP Server in main.go
清单 8-7 创建了一个简单的带有单个端点的 HTTP 服务器:HTTP Get to "/users"
返回一个集合User
实体。
使用 ResponseRecorder 测试 HTTP 应用程序
清单 8-8 显示了用ResponseRecorder
测试在清单 8-7 中创建的 HTTP 服务器的测试。
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/mux"
)
// TestGetUsers test HTTP Get to "/users" using ResponseRecorder
func TestGetUsers(t *testing.T) {
r := mux.NewRouter()
r.HandleFunc("/users", getUsers).Methods("GET")
req, err := http.NewRequest("GET", "/users", nil)
if err != nil {
t.Error(err)
}
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != 200 {
t.Errorf("HTTP Status expected: 200, got: %d", w.Code)
}
}
Listing 8-8.Testing HTTP API Server Using ResponseRecorder in main_test.go
在TestGetUsers
中,HTTP 多路复用器被配置用于测试"/users"
上的 HTTP Get 请求。
r := mux.NewRouter()
r.HandleFunc("/users", getUsers).Methods("GET")
使用http.NewRequest
to 来调用"/users"
上的 HTTP Get 的处理程序的方法ServeHTTP
来创建 HTTP 请求对象。一个nil
值作为 HTTP 请求主体的参数被提供给函数NewRequest
,因为它是一个 HTTP Get 请求。您可以为 HTTP 请求正文提供一个值,以便在 HTTP Posts 上创建 HTTP 请求对象。
req, err := http.NewRequest("GET", "/users", nil)
if err != nil {
t.Error(err)
}
使用httptest.NewRecorder
创建一个ResponseRecorder
对象,以记录返回的 HTTP 响应,供以后在测试中检查。
w := httptest.NewRecorder()
HTTP 处理程序的方法ServeHTTP
通过提供ResponseRecorder
和Request
对象来调用"/users"
上的 HTTP Get 请求。这将调用处理函数getUsers
。
r.ServeHTTP(w, req)
ResponseRecorder
对象记录返回的 HTTP 响应(处理函数中http.ResponseWriter
的变化),以便于检查。您可以看到 HTTP 响应返回了 HTTP 状态代码 200。
if w.Code != 200 {
t.Errorf("HTTP Status expected: 200, got: %d", w.Code)
}
让我们用go test
命令运行测试:
go test -v
您应该会看到类似如下的输出:
=== RUN TestGetUsers
--- PASS: TestGetUsers (0.00s)
PASS
ok github.com/shijuvar/go-recipes/ch08/httptest 0.353s
使用服务器测试 HTTP 应用程序
在清单 8-8 中,您使用ResponseRecorder
编写了测试来检查 HTTP 响应的值。这种类型足以检查 HTTP 响应的行为。包httptest
还提供了一个类型Server
,允许您创建一个用于测试的 HTTP 服务器,这样您就可以通过 HTTP 管道运行您的测试,方法是使用 HTTP 客户端向测试 HTTP 服务器发送 HTTP 请求。清单 8-9 显示了一个带有测试Server
的测试,用于测试清单 8-7 中创建的 HTTP API 服务器。
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/mux"
)
// TestGetUsersWithServer test HTTP Get to "/users" using Server
func TestGetUsersWithServer(t *testing.T) {
r := mux.NewRouter()
r.HandleFunc("/users", getUsers).Methods("GET")
server := httptest.NewServer(r)
defer server.Close()
usersURL := fmt.Sprintf("%s/users", server.URL)
request, err := http.NewRequest("GET", usersURL, nil)
res, err := http.DefaultClient.Do(request)
if err != nil {
t.Error(err)
}
if res.StatusCode != 200 {
t.Errorf("HTTP Status expected: 200, got: %d", res.StatusCode)
}
}
Listing 8-9.Testing HTTP API Server Using Server in main_test.go
在测试函数TestGetUsersWithServer
中,HTTP 多路复用器被配置用于测试"/users"
上的 HTTP Get 请求。
r := mux.NewRouter()
r.HandleFunc("/users", getUsers).Methods("GET")
通过调用函数httptest.NewServer
创建测试 HTTP 服务器。函数NewServer
启动并返回一个新的 HTTP 服务器。Server
的方法Close
被添加到延迟函数列表中,以在测试完成时关闭测试Server
。
server := httptest.NewServer(r)
defer server.Close()
使用函数http.NewRequest
创建 HTTP 请求,并使用 HTTP 客户端对象的方法Do
发送 HTTP 请求。一个nil
值作为 HTTP 请求主体的参数被提供给函数NewRequest
,因为它是一个 HTTP Get 请求。使用http.DefaultClient
创建 HTTP 客户端,然后调用方法Do
向测试服务器发送 HTTP 请求,测试服务器返回 HTTP 响应。
usersURL:= fmt.Sprintf("%s/users", server.URL)
request, err := http.NewRequest("GET", usersURL, nil)
res, err := http.DefaultClient.Do(request)
您会看到 HTTP 响应返回 HTTP 状态代码 200。
if res.StatusCode != 200 {
t.Errorf("HTTP Status expected: 200, got: %d", res.StatusCode)
}
让我们用go test
命令运行测试:
go test -v
您应该会看到类似如下的输出:
=== RUN TestGetUsersWithServer
--- PASS: TestGetUsersWithServer (0.01s)
PASS
ok github.com/shijuvar/go-recipes/ch08/httptest 0.355s
8-7.编写 BDD 风格的测试
问题
如何在 Go 中编写行为驱动开发(BDD)风格的测试?
解决办法
第三方包Ginkgo
是一个 BDD 风格的 Go 测试框架,允许你编写基于 BDD 的测试。Ginkgo
最好与Gomega
匹配器库配对。
它是如何工作的
BDD 是从测试驱动开发(TDD)演变而来的软件开发过程。在 BDD 中,应用程序是通过描述其行为来指定和设计的。BDD 强调行为而不是测试。Ginkgo
是一个 BDD 风格的测试框架,建立在标准库包之上testing. Ginkgo
通常与Gomega
一起使用,作为测试断言的匹配器库。
构建可测试的 HTTP API 服务器
让我们构建一个可测试的 HTTP API 服务器,为应用程序编写 BDD 风格的测试。在 BDD 中,您通常在编写产品代码之前先编写规范(BDD 风格的测试),但是为了便于演示,这里您先编写应用程序代码,然后再编写规范。当您编写测试时,最重要的事情是您的应用程序代码应该是可测试的,以便您可以独立地隔离应用程序的各个组件,并编写测试来验证其行为。
图 8-3 显示了 HTTP API 应用程序的目录结构。要运行后面显示的示例代码,您需要创建这个目录结构,并确保文件创建在正确的目录中。这个目录结构必须在GOPATH/src
的子目录中。
图 8-3。
Directory structure of the HTTP API application
包controllers
由处理函数和测试组成。包model
定义了应用程序的数据模型。它还为持久化存储定义了一个接口,以便您可以为应用程序代码和测试使用不同的接口实现。包store
通过实现包model.
中定义的接口来提供持久存储的具体实现
清单 8-10 显示了model
包中user.go
的来源。
package model
import "errors"
// ErrorEmailExists is an error value for duplicate email id
var ErrorEmailExists = errors.New("Email Id is exists")
// User model
type User struct {
FirstName string `json:"firstname"`
LastName string `json:"lastname"`
Email string `json:"email"`
}
// UserStore provides a contract for Data Store for User entity
type UserStore interface {
GetUsers() []User
AddUser(User) error
}
Listing 8-10.Data Model and Interface for Persistent Store in model/user.go
包model
声明了一个名为User
的数据模型,并提供了一个名为UserStore
的接口,该接口为User
实体提供了持久存储的契约。包store
通过将User
值保存到 MongoDB 数据库中,提供了接口UserStore
的实现。
清单 8-11 显示了store
包中user_store.go
的来源。
package store
import (
"log"
"time"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
"github.com/shijuvar/go-recipes/ch08/httpbdd/model"
)
// MongoDB Session
var mgoSession *mgo.Session
// Create a MongoDB Session
func createDBSession() {
var err error
mgoSession, err = mgo.DialWithInfo(&mgo.DialInfo{
Addrs: []string{"127.0.0.1"},
Timeout: 60 * time.Second,
})
if err != nil {
log.Fatalf("[createDbSession]: %s\n", err)
}
}
// Initializes the MongoDB Session
func init() {
createDBSession()
}
// MongoUserStore provides persistence logic for "users" collection.
type MongoUserStore struct{}
// AddUser insert new User
func (store *MongoUserStore) AddUser(user model.User) error {
session := mgoSession.Copy()
defer session.Close()
userCol := session.DB("userdb").C("users")
// Check whether email id exists or not
var existUser model.User
err := userCol.Find(bson.M{"email": user.Email}).One(&existUser)
if err != nil {
if err == mgo.ErrNotFound { // Email is unique
}
}
if (model.User{}) != existUser {
return model.ErrorEmailExists
}
err = userCol.Insert(user)
return err
}
// GetUsers returns all documents from the collection.
func (store *MongoUserStore) GetUsers() []model.User {
session := mgoSession.Copy()
defer session.Close()
userCol := session.DB("userdb").C("users")
var users []model.User
iter := userCol.Find(nil).Iter()
result := model.User{}
for iter.Next(&result) {
users = append(users, result)
}
return users
}
Listing 8-11.Implementation of UserStore to Persist Data into MongoDB in store/user_store.go
Struct type MongoUserStore
是接口UserStore
的具体实现,它将数据保存到 MongoDB 数据库中。在AddUser
功能中,您可以检查新用户的电子邮件 ID 是否唯一。这是我们的应用程序的一个行为,当您为处理函数编写规范时,将会对它进行测试。
清单 8-12 显示了为 HTTP API 应用程序提供处理函数的包controllers
中user_controller.go
的源代码。
package controllers
import (
"encoding/json"
"log"
"net/http"
"github.com/shijuvar/go-recipes/ch08/httpbdd/model"
)
// GetUsers serves requests for Http Get to "/users"
func GetUsers(store model.UserStore) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
data := store.GetUsers()
users, err := json.Marshal(data)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(users)
})
}
// CreateUser serves requests for Http Post to "/users"
func CreateUser(store model.UserStore) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var user model.User
// Decode the incoming User json
err := json.NewDecoder(r.Body).Decode(&user)
if err != nil {
log.Fatalf("[Controllers.CreateUser]: %s\n", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
// Insert User entity into User Store
err = store.AddUser(user)
if err != nil {
if err == model.ErrorEmailExists {
w.WriteHeader(http.StatusBadRequest)
} else {
w.WriteHeader(http.StatusInternalServerError)
}
return
}
w.WriteHeader(http.StatusCreated)
})
}
Listing 8-12.Handler Functions in controllers/user_controller.go
包controllers
为 HTTP API 提供了处理函数。示例 HTTP API 有两个端点:HTTP Get on "/users"
和 HTTP Post on "/users"
。处理函数GetUsers
服务于 HTTP Get on "/users"
的 HTTP 请求,处理函数CreateUser
服务于 HTTP Post on "/users"
的 HTTP 请求。处理函数是为了更好的可测试性而编写的。它们接受接口UserStore
的实现作为持久存储,但是它们不依赖于任何具体的实现。因此,您可以为您的应用程序提供持久存储,以将数据持久存储到真实世界的数据库中,并且当您编写测试时,您可以通过提供接口UserStore
的实现来提供持久存储的模拟实现。因为应用程序处理程序依赖于接口UserStore
,所以在应用程序代码和测试中可以有不同的实现。
清单 8-13 显示了配置 HTTP 多路复用器和创建 HTTP API 服务器的main.go
的源代码。
package main
import (
"net/http"
"github.com/gorilla/mux"
"github.com/shijuvar/go-recipes/ch08/httpbdd/controllers"
"github.com/shijuvar/go-recipes/ch08/httpbdd/store"
)
func setUserRoutes() *mux.Router {
r := mux.NewRouter()
userStore := &store.MongoUserStore{}
r.Handle("/users", controllers.CreateUser(userStore)).Methods("POST")
r.Handle("/users", controllers.GetUsers(userStore)).Methods("GET")
return r
}
func main() {
http.ListenAndServe(":8080", setUserRoutes())
}
Listing 8-13.HTTP API Server in main.go
通过提供一个MongoUserStore
实例作为处理函数的参数,应用程序处理程序被配置成一个 HTTP 多路复用器。
userStore := &store.MongoUserStore{}
r.Handle("/users", controllers.CreateUser(userStore)).Methods("POST")
r.Handle("/users", controllers.GetUsers(userStore)).Methods("GET")
为 HTTP API 服务器编写 BDD 风格的测试
第三方包 Ginkgo
及其首选匹配器库Gomega
用于指定和验证测试用例中的行为。
安装银杏和戈美加
要安装Ginkgo
和Gomega
,运行以下命令:
go get github.com/onsi/ginkgo/ginkgo
go get github.com/onsi/gomega
当你安装软件包ginkgo
时,它也会在你的GOBIN
位置安装一个名为ginkgo
的可执行程序,该程序可以用于引导测试套件文件和运行测试。GOBIN
是go install
命令安装 Go 二进制文件的目录。GOBIN
的默认位置是$GOPATH/bin
。如果您想更改默认位置,可以通过配置一个名为GOBIN
的环境变量来实现。
要使用Ginkgo
和Gomega
,您必须将这些包添加到导入列表中:
import (
"github.com/onsi/ginkgo"
"github.com/onsi/gomega"
)
引导测试套件文件
要用Ginkgo
为一个包编写测试,首先要创建一个测试套件文件。让我们从您编写测试套件文件和规范的controllers
目录运行下面的命令。
ginkgo bootstrap
这将生成一个名为controllers_suite_test.go
的文件,其中包含清单 8-14 中所示的代码。
package controllers_test
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"testing"
)
func TestControllers(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Controllers Suite")
}
Listing 8-14.Test Suite File controllers_suite_test.go in controllers_test Package
生成的名为controllers_suite_test.go
的测试套件文件将被组织到一个名为controllers_test
的包中,就像您从包controllers
中生成测试套件文件一样。在这里,您将测试和应用程序代码组织在同一个目录中,但是在不同的包中。Go 允许你组织controllers
目录下的controllers
和controllers_test
包。这将把您的测试与应用程序代码隔离开来,因为您将应用程序代码和测试组织到不同的包中。如果您想将测试套件文件和测试的包名改为controllers
,您可以这样做,并且Ginkgo
可以使用它。
在测试套件controllers_suite_test.go
中,您执行以下操作:
- 使用点(
.
)导入来导入包ginkgo
和gomega
。这允许您调用导出的ginkgo
和gomega
包的标识符,而无需使用限定符。 - 连接
Ginkgo
和Gomega. Gomega
的RegisterFailHandler(Fail)
语句被用作Ginkgo
的匹配器库。 RunSpecs(t, "Controllers Suite")
语句告诉Ginkgo
启动测试套件。如果你的任何一个规格失败,Ginkgo
将自动使testing.T
失败。
注意,除了ginkgo bootstrap
生成的代码,你不需要写任何额外的代码。这个测试套件文件足以运行您在下一步中编写的同一个包中的所有规范。
向套件添加规格
您刚刚创建了名为controllers_suite_test.go
的测试套件文件。要运行您的测试套件,您需要添加一个测试文件来运行规范。您可以使用ginkgo generate
命令生成一个测试文件。
ginkgo generate user_controller
这将生成一个名为user_controller_test.go
的测试文件。清单 8-15 显示了由ginkgo
命令行工具生成的代码。
package controllers_test
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("UserController", func() {
})
Listing 8-15.Test File user_controller_test.go Generated by ginkgo
规范是使用银杏的Describe
函数写在顶级描述容器中的。Ginkgo
使用var _ =
来评估顶层的Describe
函数,而不需要init
函数。
在测试文件中编写规格
生成的测试文件user_controller_test.go
现在只包含一个顶层Describe
容器。让我们在测试文件中编写规范来测试 HTTP API 服务器。让我们在编写规范之前定义基本的用户故事。
- 允许用户查看用户实体列表。
- 让用户创建新的用户实体。
- 用户实体的电子邮件 Id 应该是唯一的。
现在,让我们根据这些用户故事在测试文件中编写规范。清单 8-16 显示了用user_controller_test.go
编写的针对 HTTP API 服务器的 BDD 风格测试的规范。
package controllers_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"github.com/shijuvar/go-recipes/ch08/httpbdd/controllers"
"github.com/shijuvar/go-recipes/ch08/httpbdd/model"
"github.com/gorilla/mux"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("UserController", func() {
var r *mux.Router
var w *httptest.ResponseRecorder
var store *FakeUserStore
BeforeEach(func() {
r = mux.NewRouter()
store = newFakeUserStore()
})
// Specs for HTTP Get to "/users"
Describe("Get list of Users", func() {
Context("Get all Users from data store", func() {
It("Should get list of Users", func() {
r.Handle("/users", controllers.GetUsers(store)).Methods("GET")
req, err := http.NewRequest("GET", "/users", nil)
Expect(err).NotTo(HaveOccurred())
w = httptest.NewRecorder()
r.ServeHTTP(w, req)
Expect(w.Code).To(Equal(200))
var users []model.User
json.Unmarshal(w.Body.Bytes(), &users)
// Verifying mocked data of 2 users
Expect(len(users)).To(Equal(2))
})
})
})
// Specs for HTTP Post to "/users"
Describe("Post a new User", func() {
Context("Provide a valid User data", func() {
It("Should create a new User and get HTTP Status: 201", func() {
r.Handle("/users", controllers.CreateUser(store)).Methods("POST")
userJson := `{"firstname": "Alex", "lastname": "John", "email": "alex@xyz.com"}`
req, err := http.NewRequest(
"POST",
"/users",
strings.NewReader(userJson),
)
Expect(err).NotTo(HaveOccurred())
w = httptest.NewRecorder()
r.ServeHTTP(w, req)
Expect(w.Code).To(Equal(201))
})
})
Context("Provide a User data that contains duplicate email id", func() {
It("Should get HTTP Status: 400", func() {
r.Handle("/users", controllers.CreateUser(store)).Methods("POST")
userJson := `{"firstname": "Shiju", "lastname": "Varghese", "email": "shiju@xyz.com"}`
req, err := http.NewRequest(
"POST",
"/users",
strings.NewReader(userJson),
)
Expect(err).NotTo(HaveOccurred())
w = httptest.NewRecorder()
r.ServeHTTP(w, req)
Expect(w.Code).To(Equal(400))
})
})
})
})
// FakeUserStore provides a mocked implementation of interface model.UserStore
type FakeUserStore struct {
userStore []model.User
}
// GetUsers returns all users
func (store *FakeUserStore) GetUsers() []model.User {
return store.userStore
}
// AddUser inserts a User
func (store *FakeUserStore) AddUser(user model.User) error {
// Check whether email exists
for _, u := range store.userStore {
if u.Email == user.Email {
return model.ErrorEmailExists
}
}
store.userStore = append(store.userStore, user)
return nil
}
// newFakeUserStore provides two dummy data for Users
func newFakeUserStore() *FakeUserStore {
store := &FakeUserStore{}
store.AddUser(model.User{
FirstName: "Shiju",
LastName: "Varghese",
Email: "shiju@xyz.com",
})
store.AddUser(model.User{
FirstName: "Irene",
LastName: "Rose",
Email: "irene@xyz.com",
})
return store
}
Listing 8-16.Specs in user_controller_test.go
清单 8-16 针对 HTTP API 服务器的处理函数(参见清单 8-12 )提供了 BDD 风格的测试。处理函数依赖于接口model.UserStore
,它提供了持久存储的契约。下面是在"/users"
上服务 HTTP Get 请求的处理函数GetUsers
:
func GetUsers(store model.UserStore) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
data := store.GetUsers()
users, err := json.Marshal(data)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(users)
})
}
为了测试处理函数,你必须提供一个接口model.UserStore
的实现。应用程序代码提供了一个store.MongoUserStore
值作为接口model.UserStore
的实现,该接口将数据保存到 MongoDB 数据库中。当您编写测试时,您不需要将数据持久化到现实世界的数据库中;相反,您可以为持久化存储提供一个模拟实现。因为处理函数仅仅依赖于接口model.UserStore
而不是任何具体的实现,所以通过提供接口model.UserStore
的实现,你可以很容易地提供一个模拟实现来使用持久存储。结构类型FakeUserStore
提供了接口model.UserStore
的模拟实现。
type FakeUserStore struct {
userStore []model.User
}
// GetUsers returns all users
func (store *FakeUserStore) GetUsers() []model.User {
return store.userStore
}
// AddUser inserts a User
func (store *FakeUserStore) AddUser(user model.User) error {
// Check whether email exists
for _, u := range store.userStore {
if u.Email == user.Email {
return model.ErrorEmailExists
}
}
store.userStore = append(store.userStore, user)
return nil
}
// newFakeUserStore provides two dummy data for Users
func newFakeUserStore() *FakeUserStore {
store := &FakeUserStore{}
store.AddUser(model.User{
FirstName: "Shiju",
LastName: "Varghese",
Email: "shiju@xyz.com",
})
store.AddUser(model.User{
FirstName: "Irene",
LastName: "Rose",
Email: "irene@xyz.com",
})
return store
}
函数newFakeUserStore
提供了一个带有两个虚拟User
数据的FakeUserStore
实例。
当你写规范时,块Describe
、Context
和It
被用来指定行为。一个Describe
块用于描述代码的单个行为,它被用作Context
和It
块的容器。Context
块用于指定单个行为下的不同上下文。您可以在一个Describe
块中写入多个Context
块。It
块用于在Describe
或Context
容器中写入单独的规格。
BeforeEach
程序块在每个It
程序块之前运行。该块用于在运行每个规范之前编写逻辑。这里它被用来创建mux.Router
和FakeUserStore
的实例。
var r *mux.Router
var w *httptest.ResponseRecorder
var store *FakeUserStore
BeforeEach(func() {
r = mux.NewRouter()
store = newFakeUserStore()
})
mux.Router
和FakeUserStore
的值用于配置It
块中的 HTTP 请求多路复用器。
r.Handle("/users", controllers.GetUsers(store)).Methods("GET")
我们来总结一下user_controller_test.go
的规格:
- 在
Describe
块中指定了两个单独的行为:"users"
上的“获取用户列表”和“发布新用户”。 - 在
Describe
块中,Context
块用于定义个体行为下的上下文。 - 各个规格写在
Describe
和Context
容器内的It
块中。 - 在“获取用户列表”行为中,指定了一个上下文“从数据存储中获取所有用户”。这个上下文映射了 HTTP Get 在
"/users"
端点上的功能。在这个上下文中,指定了一个It
块“应该获取用户列表”。这将检查返回的 HTTP 响应的状态代码是否为 200。由FakeUserStore
提供的持久存储提供了两个用户的虚拟数据,以便您可以检查返回的 HTTP 响应是否有两个用户。 - 在“发布新用户”行为中,在
Context
块中定义了两个上下文:“提供有效的用户数据”和“提供包含重复电子邮件 id 的用户数据”。这些上下文映射了"/users"
端点上 HTTP Post 的功能。如果您提供有效的User
数据,这应该能够创建一个新用户。如果您用重复的电子邮件 ID 提供User
数据,您应该会得到一个错误。您为一个重复的用户提供一个现有的电子邮件 id,以测试上下文“提供一个包含重复电子邮件 ID 的用户数据”的行为。为该规范提供的电子邮件 ID 已经添加到持久存储库FakeUserStore
中,所以当您执行该规范时,应该会得到 HTTP 错误响应。这些规格在It
模块中指定。
您可以使用go test
命令或ginkgo
命令运行规范。让我们使用go test
命令运行规范:
go test -v
您应该会看到类似如下的输出:
=== RUN TestControllers
Running Suite: Controllers Suite
================================
Random Seed: 1473153169
Will run 3 of 3 specs
+++
Ran 3 of 3 Specs in 0.026 seconds
SUCCESS! -- 3 Passed | 0 Failed | 0 Pending | 0 Skipped --- PASS: TestControllers (0.03s)
PASS
ok github.com/shijuvar/go-recipes/ch08/httpbdd/controllers 0.624s
您也可以使用ginkgo
命令来运行规范:
ginkgo test -v
这应该会产生如下所示的输出:
Running Suite: Controllers Suite
================================
Random Seed: 1473153225
Will run 3 of 3 specs
UserController Get list of Users Get all Users from data store
Should get list of Users
D:/go/src/github.com/shijuvar/go-recipes/ch08/httpbdd/controllers/user_controller_test.go:40
+
------------------------------
UserController Post a new User Provide a valid User data
Should create a new User and get HTTP Status: 201
D:/go/src/github.com/shijuvar/go-recipes/ch08/httpbdd/controllers/user_controller_test.go:60
+
------------------------------
UserController Post a new User Provide a User data that contains duplicate email id
Should get HTTP Status: 400
D:/go/src/github.com/shijuvar/go-recipes/ch08/httpbdd/controllers/user_controller_test.go:76
+
Ran 3 of 3 Specs in 0.070 seconds
SUCCESS! -- 3 Passed | 0 Failed | 0 Pending | 0 Skipped PASS
Ginkgo ran 1 suite in 4.6781235s
Test Suite Passed
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库