Go 秘籍(一)

原文:Go Recipes

协议:CC BY-NC-SA 4.0

一、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:该目录包含可执行程序(命令)。

创建一个包含三个子目录srcpkgbin的 Go 工作区目录。将所有 Go 源文件放入 Go 工作区下的src子目录中。一个 Go 程序员将 Go 程序打包写入src目录。Go 源文件被组织到称为包的目录中,其中单个目录将用于单个包。你用.go扩展名编写 Go 源文件。Go 中有两种类型的包:

  • 编译成可执行程序的包。
  • 编译成共享库的包。

Go 工具编译 Go 源代码,并通过运行go命令使用 Go 工具将结果二进制文件安装到Workspace下的适当子目录中。go install命令编译 Go 包,如果是共享库,将生成的二进制文件移入pkg目录,如果是可执行程序,将二进制文件移入bin目录。因此,pkgbin目录用于基于包类型的包的二进制输出。

配置 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/srcsrc子目录下。

$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

您现在可以通过从GOPATHbin目录中键入命令来运行可执行命令。如果您已经将$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 中,没有任何类似于publicprivate的关键字。在 Go 中,如果名称的第一个字母是大写字母,那么所有包标识符都会被导出到其他包中。如果包标识符的名称以小写字母开头,它将不会导出到其他包,并且可访问性仅限于包内。在我们的示例程序中,我们使用了两个标准库包,stringsunicode,其中所有可重用函数的标识符都以大写字母开头。当你对 Go 了解更多的时候,它的简单和解决问题的方式会让你大吃一惊。

在我们的包中,我们提供了三个字符串实用函数:ToUpperCaseToLowerCaseToFirstUpperToUpperCase函数返回一个字符串参数的副本,其中所有的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函数中,我们首先将所有字母转换成小写,然后将字符串的第一个字母转换成大写。在这个函数中,我们使用了一个类型为runeSlice(一个用于存储特定类型集合的数据结构)。在本书的后面,我们将更多地讨论用于保存值集合的各种数据结构。表达式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命令从远程存储库中获取源代码,并按照以下步骤安装软件包。

  1. 从远程存储库中获取源代码,并将源代码放入$GOPATH/src目录下的github.com/shijuvar/go-recipes/ch01/strutils目录中。
  2. 安装软件包,将软件包对象strutils放入$GOPATH/pkg目录下平台特定目录下的github.com/shijuvar/go-recipes/ch01目录(macOS 中为darwin_amd64目录)。
编译包

让我们构建strutils包,这样我们就可以使它成为一个共享库,与 Go 工作区中的其他包一起使用。导航到包目录,然后运行go install命令:

go install

install命令编译(类似于go build命令的动作)包源代码,然后将生成的二进制文件安装到GOPATHpkg目录中。当我们从其他包中重用这个包时,我们可以从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 安装将安装位于GOROOTpkg目录中的标准库包。当您编写定制包时,这些包的结果二进制文件会放在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/pkggithub.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 代码的命令fmtgo 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 网站一模一样。通过点击包链接,您可以从GOROOTGOPATH获得包的文档。当您在本地运行godoc服务器时,它只是查看GOROOTGOPATH并为驻留在这些位置的包生成文档。在 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中引用一个程序包时,在执行程序包mainmain功能之前,会调用被引用程序包的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.golib包提供核心功能。它允许您使用Add函数向收藏中添加喜爱的项目,并使用GetAll函数返回所有喜爱的项目。AddGetAll函数将被导出到其他包中,因此标识符名称以大写字母开头。为了存储喜爱项目的数据,我们使用了一个名为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 提供了构建可靠而高效的软件系统所必需的语言。这一章中的方法涉及到编写函数、处理各种集合类型、错误处理以及用关键字deferpanicrecover实现的 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

两个整数变量xy被初始化,为调用Add函数提供参数。局部变量resultAdd函数返回的返回值初始化。

清单 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

在这个程序中,声明了两个函数:AddSubtract。这两个函数是从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.

变量abmain函数中声明,但是匿名函数(内部函数)可以访问这些变量。当您通过将匿名函数的值作为参数传递来调用SplitValues函数时,匿名函数也可以访问变量ab。匿名函数关闭ab的值,使其成为闭包。不管从哪里调用匿名函数的值,它都可以访问外部函数中声明的变量ab

运行上述程序时,您应该会看到以下输出:

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函数声明切片时,lengthcapacity作为参数提供。

下面是使用指定了lengthcapacitymake函数创建切片的代码块:

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,因此该切片的capacitylength将是 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。这次lengthcapacity都是 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语句写成单行上的多个语句时,语句之间用分号(;)并且最后一个表达式应该有一个布尔值。

要从地图中移除项目,请通过提供键来使用内置函数deletedelete函数从 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语句通常用于closedisconnectunlock语句,与openconnectlock语句相对。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块更强大。例如,结合deferrecover语句,您可以从一个混乱的函数中重新获得控制。我们将在本章的下一节介绍panicrecover

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 中,您调用了标准库包osOpen函数来打开一个文件对象。

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函数或切片文字初始化切片。切片可以使用两个内置函数进行修改:appendcopy。映射是哈希表的一种实现,它提供了一个无序的键/值对集合。使用内置的make函数或使用地图文字来初始化地图。

Go 提供了defer,,可以用来在函数中写清理逻辑。一个defer语句将一个函数调用推送到一个保存的列表上,该列表在周围的函数返回后执行。Panic是一个内置函数,可以让你停止正常执行,并开始一个函数的死机。Recover是一个内置函数,可恢复对恐慌功能的控制。Recover仅用于延迟函数内部。

Go 使用一种不同且独特的方法在 Go 代码中实现异常处理。因为 Go 函数可以返回多个值,所以return语句提供了一个error值,以及其他返回值。这样,调用函数可以检查返回的error值,以确定是否有错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值