原文: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
值,以确定是否有错误。