Skip to content

Latest commit

 

History

History
1079 lines (592 loc) · 70.3 KB

Go语言入门经典.md

File metadata and controls

1079 lines (592 loc) · 70.3 KB

Go语言入门经典

乔治·奥尔波

前言

Go(Golang)语言是编程语言设计的又一次尝试,是对类C语言的重大改进。它让您能够访问底层操作系统,还提供了强大的网络编程和并发编程支持。

Go语言用途众多,其中包括如下几种。 ➢ 网络编程。 ➢ 系统编程。 ➢ 并发编程。 ➢ 分布式编程。

很多重要的开源项目都是使用Go语言开发的,其中包括Go-Ethereum、Terraform、Kubernetes和Docker。Go语言给开源界带来了重大影响,有望提高开源项目的成功率。

1.6 问与答

答:设计Go语言是因为Java和C++ 等传统语言繁琐、缓慢而难以理解。Go语言的设计者借鉴了Python动态类型语言的优点,旨在打造一款易于使用并可用于开发高流量生产系统的语言。

答:在编译器生成的二进制文件中,必须包含执行程序所需的一切。这带来的缺点是二进制文件比源代码文件大,但优点是无须安装依赖就能运行程序。

答:在开发阶段,推荐使用命令go run;程序开发完毕,可以分享时,建议使用go build。

2.2 区分静态类型和动态类型

所谓强类型语言,指的是错误地使用了类型时,编译器将引发错误;所谓动态类型(也叫松散类型或弱类型)语言,指的是为了执行程序,运行时会将一种类型转换为另一种类型,或者编译器没有实现类型系统。哪种语言更好呢?这存在很大的争议,计算机科学家看重强类型语言的正确性和安全性,而其他人则看重动态语言的简单性和开发速度。

➢ 使用动态类型语言编写软件的速度通常更快。➢ 无须为执行代码而等待编译器完成编译。➢ 动态类型语言通常不那么死板,因此有些人认为变更代码更容易。➢ 有些人认为动态类型语言学习门槛更低。

JavaScript是一种使用广泛的动态类型语言

而在使用Go语言编写的函数中,对参数和返回值的类型都做了声明。

2.4 理解数值类型

Go语言中的浮点数可以是32位的,也可以是64位的。在大多数现代计算机中,推荐使用float64。

2.5 检查变量的类型

有此情况下,需要检查变量的类型,为此可使用标准库中的reflect包,它让您能够访问变量的底层类型。

2.6 类型转换

strconv包提供了一整套类型转换方法,可用于转换为字符串或将字符串转换为其他类型。

3.4 编写简短变量声明

2.简短变量赋值语句:=表明使用的是简短变量声明,这意味着不需要使用关键字var,也不用指定变量的类型。同时这还意味着应将:=右边的值赋给变量。

使用简短变量声明时,编译器会推断变量的类型,因此您无须显式地指定变量的类型。请注意,只能在函数中使用简短变量声明。

3.5 变量声明方式

在同一行内声明变量并给它赋值时,Go语言设计者在标准库中遵循的约定如下:在函数内使用简短变量声明,在函数外省略类型。程序清单3.9演示了这种被普遍接受的约定。如果您查看Go源代码,将发现简短变量声明是最常用的变量声明方式。

3.6 理解变量作用域

➢ 在内部块中,可访问外部块中声明的变量。➢ 在外部块中,不能访问在内部块中声明的变量。

而言之,每个内部块都可访问其外部块,但外部块不能访问内部块。

➢ 代码的缩进程度反映了块作用域的层级。在每个块中,代码都被缩进。 ➢ 在内部块中,可引用外部块中声明的变量。

这是因为Go语言将文件也视为块,所以在第一级大括号外声明的变量可在所有块中访问。

3.7 使用指针

指针是Go语言中的一种类型,指向变量所在的内存单元。要声明指针,可在变量名前加上星号字符

如果要使用指针指向的变量的值,而不是其内存地址,该怎么办呢?可在指针变量前加上星号。

4.1 函数是什么

➢ 让每个函数只做一件事情并把这件事情做好。软件不可避免地要修改,通过结合使用大量简短的函数,可让软件更容易修改。这还有助于测试各个函数以及整个软件。➢ 维护。在团队合作开发中,您编写的函数易于阅读和理解吗?如果不是这样的,就说明它过于复杂或必须添加注释。别忘了,您可能在一年后的午夜时分回过头来阅读这个函数!

函数签名指出函数不接受任何参数,并返回一个整数和一个字符串。

4.3 使用具名返回值

具名返回值让函数能够在返回前将值赋给具名变量,这有助于提升函数的可读性,使其功能更加明确。要使用具名返回值,可在函数签名的返回值部分指定变量名。

这个函数体中,在终止语句return前给具名变量进行了赋值。使用具名返回值时,无须显式地返回相应的变量。这被称为裸(naked)return语句。

4.4 使用递归函数

递归函数是不断调用自己直到满足特定条件的函数。要在函数中实现递归,可将调用自己的代码作为终止语句中的返回值。

4.5 将函数作为值传递

Go语言提供了一些函数式编程功能,如能够将一个函数作为参数传递给其他函数。

5.2 使用else语句

在Go语言中,else语句紧跟在其他语句的右大括号后面

5.3 使用else if语句

else if语句让您能够判断布尔条件,而else语句在到达其所在分支时就会执行。

5.7 使用switch语句

除少量的if else比较外,其他的都可替换为switch语句,这样不仅可提高代码的可读性,还可提高其性能。

switch语句更简洁,它还支持在其他case条件都不满足时将执行的default case

5.8 使用for语句进行循环

➢ 初始化语句:仅在首次迭代前执行。➢ 条件语句:每次迭代前都将检查的布尔表达式。➢ 后续语句:每次迭代后都将执行。

5.9 使用defer语句

defer是一个很有用的Go语言功能,它能够让您在函数返回前执行另一个函数。函数在遇到return语句或到达函数末尾时返回。defer语句通常用于执行清理操作或确保操作(如网络调用)完成后再执行另一个函数。

5.10 小结

您学习了defer语句,它能够让您在外部函数返回前执行函数。

第6章 数组、切片和映射

您将熟悉Go语言编程中常用的3种数据结构。

6.1 使用数组

在Go语言中,要创建数组,可声明一个数组变量,并指定其长度和数据类型。

要打印数组的所有元素,可使用变量名本身。

6.2 使用切片

在Go语言中,数组是一个重要构件,但使用切片的情况更多。切片是底层数组中的一个连续片段,通过它您可以访问该数组中一系列带编号的元素。因

在Go语言中,使用数组存在一定的局限性。采用前面的数组cheeses表明方试,您无法在数组中添加元素;然而切片比数组更灵活,您可在切片中添加和删除元素,还可复制切片中的元素。可将切片视为轻量级的数组包装器,它既保留了数组的完整性,又比数组使用起来更容易。

使用Go内置函数make创建一个切片,其中第一个参数为数据类型,而第二个参数为长度。在这里,创建的切片包含两个字符串元素。

切片类似于数组,但不同于数组的是,您可在切片中添加和删除元素。

Go语言提供了内置函数append,让您能够增大切片的长度。

要从切片中删除元素,也可使用内置函数append。在下面的示例中,删除了索引2处的元素。[插图]

6.3 使用映射

数组和切片是可通过索引值访问的元素集合,而映射是通过键来访问的无序元素编组。大多数编程语言都支持数组;在其他编程语言中,映射也被称为关联数组、字典或散列。映射在信息查找方面的效率非常高,因为可直接通过键来检索数据。简单地说,映射可视为键-值对集合。

与数组和切片一样,要打印映射中所有的键-值对,可使用变量名本身。

6.4 小结

您知道调整数组的长度很难,而切片是便利的数组替代品;

6.5 问与答

除非确定必须使用数组,否则请使用切片。切片能够让您轻松地添加和删除元素,还无须处理内存分配问题。

不能将delete用于切片。没有专门用于从切片中删除元素的函数,但可使用内置函数append来完成这种任务,您还可创建子切片。

7.1 结构体是什么

结构体是一系列具有指定数据类型的数据字段,它能够让您通过单个变量引用一系列相关的值。通过使用结构体,可在单个变量中存储众多类型不同的数据字段。存储在结构体中的值可轻松地访问和修改,这提供了一种灵活的数据结构创建方式。通过使用结构体,可提高模块化程度,还能够让您创建并传递复杂的数据结构。

您还可将结构体视为用于创建数据记录(如员工记录和机票预订)的模板。

➢ 关键字type指定一种新类型。➢ 将新类型的名称指定为Movie。

7.2 创建结构体

假设您已经声明了一个结构体,那么就可直接声明这种类型的变量。

也可使用关键字new来创建结构体实例,如程序清单7.3所示。关键字new创建结构体Movie的一个实例(为其分配内存);将这个结构体实例赋给变量m后,就可像前面那样使用点表示法给数据字段赋值了。

还可使用简短变量赋值来创建结构体实例,此时可省略关键字new。创建结构体实例时,可同时给字段赋值,方法是使用字段名、冒号和字段值。

也可省略字段名,按字段声明顺序给它们赋值,但出于可维护性考虑,不推荐这样做。

字段很多时,让每个字段独占一行能够提高代码的可维护性和可读性。请注意,如果您选择这样做,则最后一个数据字段所在的行也必须以逗号结尾。

7.3 嵌套结构体

为建立较复杂的数据结构,在一个结构体中嵌套另一个结构体的方式很有用。

7.4 自定义结构体数据字段的默认值

创建结构体时,如果没有给其数据字段指定值,它们将为表7.1所示的零值。Go语言没有提供自定义默认值的内置方法,但可使用构造函数来实现这个目标。构造函数创建结构体,并将没有指定值的数据字段设置为默认值。

7.5 比较结构体

不能对两个类型不同的结构体进行比较,否则将导致编译错误。因此,试图比较两个结构体之前,必须确定它们的类型相同。要检查结构体的类型,可使用Go语言包reflect。在程序清单7.8中,使用了reflect包来检查结构体的类型。

7.6 理解公有和私有值

根据Go语言约定,结构体及其数据字段都可能被导出,也可能不导出。如果一个标识符的首字母是大写的,那么它将被导出;否则不会导出。

7.7 区分指针引用和值引用

数据值存储在计算机内存中。指针包含值的内存地址,这意味着使用指针可读写存储的值。创建结构体实例时,给数据字段分配内存并给它们指定默认值;然后返回指向内存的指针,并将其赋给一个变量。使用简短变量赋值时,将分配内存并指定默认值。

赋值后,a与b相同,但它是b的副本,而不是指向b的引用。修改b不会影响a,

要修改原始结构体实例包含的值,必须使用指针。指针是指向内存地址的引用,因此使用它操作的不是结构体的副本而是其本身。要获得指针,可在变量名前加上和号。

以使用指针引用而不是值引用,如程序清单7.10所示。

➢ 将指向a的指针(而不是a本身)赋给b,这是使用和号字符表示的。➢ 修改b时,将修改分配给a的内存,因为a和b指向相同的内存。

指针和值的差别很微妙,但选择使用指针还是值很容易区分:如果需要修改原始结构体实例,就使用指针;如果要操作一个结构体,但不想修改原始结构体实例,就使用值。

7.8 小结

结构体是一个功能强大的基本构件,提供了一种编程思维。

7.9 问与答

推荐使用简短变量赋值方式(:=)来创建结构体。关键字new和变量声明方式也合法,但不那么常用。

对结构体嵌套层级数没有任何限制,但如果嵌套层级太多,可能昭示着使用其他数据结构是更好的选择。

7.10 作业

3.要创建结构体的副本,但不希望修改影响原始结构体时,应使用值;要操作结构体的副本,并希望所做的修改在原始结构体中反映出来时,应使用指针。

8.1 使用方法

方法类似于函数,但有一点不同:在关键字func后面添加了另一个参数部分,用于接受单个参数。下面的示例给第7章介绍的结构体Movie添加了一个方法。

在方法声明中,关键字func后面多了一个参数——接收者。严格地说,方法接收者是一种类型,这里是指向结构体Movie的指针。

通过声明方法summary,让结构体Movie的任何实例都可使用它。为何要使用方法,而不直接使用函数呢?

函数summary和结构体Movie相互依赖,但它们之间没有直接关系。例如,如果不能访问结构体Movie的定义,就无法声明函数summary。如果使用函数,则在每个使用函数或结构体的地方,都需包含函数和结构体的定义,这会导致代码重复。另外,函数发生任何改变,都必须随之修改多个地方。这样看来在函数与结构体关系密切时,使用方法更合理。

方法summary的实现将float64等级制转换为字符串并设置其格式。使用方法的优点在于,只需编写方法实现一次,就可对结构体的任何实例进行调用。

8.2 创建方法集

方法集是可对特定数据类型进行调用的一组方法。在Go语言中,任何数据类型都可有相关联的方法集,这让您能够在数据类型和方法之间建立关系,

要创建这个方法集,可声明结构体Sphere,再声明两个将结构体Sphere作为接收者的方法。

相比于使用函数,使用方法集的优点在于,只需编写一次方法SurfaceArea和Volume。例如,如果发现这两个方法中有一个存在Bug,则只需修改一个地方即可。

8.3 使用方法和指针

接收者可以是指针,也可以是值,但两者的差别非常微妙。

在这个示例中,注意到指定接收者参数类型时没有在Triangle前面加上星号,这意味着接收者参数是值而不是指针。

是因为方法changeBase接受的是一个值引用。这意味着这个方法操作的是结构体Triangle的副本,而原始结构体不受影响。在方法changeBase中,修改的是原始Triangle结构体的副本的t.base。

将指针作为接收者的方法能够修改原始结构体的数据字段,这是因为它使用的是指向原始结构体内存单元的指针,因此操作的不是原始结构体的副本。

指针和值之间的差别很微妙,但选择使用指针还是值这一点很简单:如果需要修改原始结构体,就使用指针;如果需要操作结构体,但不想修改原始结构体,就使用值。

8.4 使用接口

在Go语言中,接口指定了一个方法集,这是实现模块化的强大方式。您可将接口视为方法集的蓝本,它描述了方法集中的所有方法,但没有实现它们。接口功能强大,因为它充当了方法集规范,这意味着可在符合接口要求的前提下随便更换实现。

接口描述了方法集中的所有方法,并指定了每个方法的函数签名。

接口Robot只包含一个方法——PowerOn。这个接口描述了方法PowerOn的函数签名:不接受任何参数且返回一种错误类型。从高级层面说,接口还有助于理解代码设计。在无须关心实现的情况下,很容易理解设计是什么样的。

那么如何使用接口呢?接口是方法集的蓝本,要使用接口,必须先实现它。如果代码满足了接口的要求,就实现了接口。要实现接口Robot,可声明一个满足其要求的方法集。

要满足接口的要求,只要实现了它指定的方法集,且函数签名正确无误就可以了。

接口也是一种类型,可作为参数传递给函数,因此可编写可重用于多个接口实现的函数。

例如,编写一个可用于启动任何机器人的函数。

这个函数将接口Robot的实现作为参数,并返回调用方法PowerOn的结果。这个函数可用于启动任何机器人,而不管其方法PowerOn是如何实现的。T850和R2D2都可利用这个方法。

虽然Go语言没有提供类和类继承等面向对象功能,但结构体和方法集弥补了这部分不足,提供了一些面向对象元素。

乍一看,接口提供的抽象层可能有点复杂,但它有助于代码重用,还能够让您完全更换实现。假设要编写一个使用MySQL数据库的计算机程序,如果不使用接口,则代码将完全是针对MySQL的。在这种情况下,如果后来要将MySQL数据库换成其他数据库,如PostgreSQL,就可能需要重写大量的代码。

通过定义一个数据库接口,该接口的实现将比使用的数据库更重要。从理论上说,只要实现满足接口的要求,就可使用任何数据库,因此可轻松地更换数据库。数据库接口可包含多个实现,这就引入了多态的概念。

多态意味着多种形式,它让接口能够有多种实现

如果一个方法集实现了一个接口,就可以说它与另一个实现了该接口的方法集互为多态。

8.5 小结

您大致了解了接口,它是一种创建多态实现的强大方式,这些实现可共享方法或完全更换。

8.6 问与答

方法和函数的唯一差别在于,方法多了一个指定接收者的参数,这让您能够对数据类型调用方法,从而提高代码重用性和模块化程度。

果需要修改原始结构体中的数据,就使用指针;如果要操作原始数据的副本,就使用值引用。

8.7 作业

2.可以。在Go语言中,可将方法集与任何类型相关联。

3.多态指的是多种不同的形式。在Go语言中,接口支持多种实现,它能够让您共享或更换代码。

9.3 拼接字符串

对于简单而少量的拼接,使用运算符+和+=的效果虽然很好,但随着拼接操作次数的增加,这种做法的效率并不高。如果需要在循环中拼接字符串,则使用空的字节缓冲区来拼接的效率更高。

很多字符编码方案都实现了Unicode标准,其中最著名的是UTF-8。更巧的是,Go语言的两位设计者Rob Pike和Ken Thompson也是UTF-8的联合设计者。可见,Go语言支持UTF-8和国际字符集,而Go源代码也是UTF-8的。

很多国际字符都不止1字节,在下面的示例中,每个字符的长度都是3字节。

给字符串变量赋值后,就可使用标准库中的strings包提供的任何方法。这个包提供了一套完备的字符串处理函数,其文档非常详尽。

处理来自用户或数据源的输入时,一种常见的任务是确保开头和末尾没有空格。方法TrimSpace提供了这样的功能

9.6 作业

1.解释型字符串字面量使用双引号("),可包含rune字面量,这意味着可使用特殊字符来设置格式;原始字符串字面量使用反引号(`),位于反引号中的格式保持不变,这包括空格、制表符和换行符。

4.推出UTF-8旨在提供一种能够表示地球上几乎所有字符的标准方式。

第10章 处理错误

很多语言选择在发生必须捕获的错误时引发异常,而Go语言处理错误的方式很有趣——将错误作为一种类型,这意味着可将错误传递给函数和方法。

10.1 错误处理及Go语言的独特之处

在Go语言中,有一种约定是,如果没有发生错误,返回的错误值将为nil。这让程序员调用方法或函数时,能够检查它是否像预期那样执行完毕。

在Go程序中,这种做法很常见。有些开发人员认为,这种做法很繁琐,因为它要求调用每个方法或函数时都检查错误,导致代码重复。这说得也许没错,但Go语言处理错误的方式比其他语言更灵活,因为可像其他类型一样在函数之间传递错误。这通常意味着代码要简短得多。

10.2 理解错误类型

在Go语言中,错误是一个值。标准库声明了接口error,如下所示。[插图]这个接口只有一个方法——Error,它返回一个字符串。

10.3 创建错误

➢ 使用errors包中的方法New创建一个错误。➢ 使用if语句检查这个错误值是否为nil。➢ 如果不为nil,就将它打印出来。

10.4 设置错误的格式

除errors包外,标准库中的fmt包还提供了方法Errorf,可用于设置返回的错误字符串的格式,如程序清单10.3所示。这能够让您将多个值合并成更有意义的错误字符串,从而动态地创建错误字符串。

10.5 从函数返回错误

Go语言错误处理方式的一个优点:错误处理不是在函数中,而是在调用函数的地方进行的。这在错误处理方面提供了极大的灵活性,而不是简单地一刀切。

10.6 错误和可用性

➢ 具体地指出了问题所在。➢ 提供了问题解决方案。➢ 对用户更有礼貌。如果库用户相信错误会以一致的方式返回,且包含有用的错误消息,则用户能够从错误中恢复的可能性将高得多。他们很可能也会认为您编写的库不仅很有用,而且值得信任。

10.7 慎用panic

panic是Go语言中的一个内置函数,它终止正常的控制流程并引发恐慌(panicking),导致程序停止执行。出现普通错误时,并不提倡这种做法,因为程序将停止执行,并且没有任何回旋余地。

下面的情形下,使用panic可能是正确的选择。➢ 程序处于无法恢复的状态。这可能意味着无路可走了,或者再往下执行程序将带来更多的问题。在这种情况下,最佳的选择是让程序崩溃。➢ 发生了无法处理的错误。

10.9 问与答

然这看起来重复太多,但能够检查错误实际上是Go语言的特色,这给程序员提供了极大的控制权。虽然可采用办法和第三方包来减少这样的重复,但大多数Go程序员都喜欢能够随心所欲地处理错误。

答:绝对应该!要编写富有弹性而稳定的代码,处理错误至关重要。对程序可能出现的错误以及如何从中恢复这些问题考虑得越多,代码的质量就越好。

:不同于Java等其他语言,Go语言不支持传统的try-catch-finally控制结构,而向函数或方法的调用者报告错误。Go语言将错误作为返回值,这只是它做出的一种设计决策,但对这种处理错误的方式仍存在争议。

第11章 使用Goroutine

本章介绍并发和Goroutine。您将明白顺序执行和并发执行的差别,知道Goroutine是应对网络延迟的方式之一;您将了解并发和并行的差别,知道Goroutine是如何提高程序的速度的。

11.1 理解并发

程序发出请求后,很多因素都可能影响响应返回的速度,如以下几种。➢ 查找天气服务地址的DNS的速度。➢ 程序和天气服务器之间网络连接的速度。➢ 建立与天气服务器连接的速度。➢ 天气服务器的响应速度。鉴于所有这些因素都不是发出请求的程序能够控制的,因此完全有理由认为响应速度是无法预测的。另外,每次请求得到响应的时间都可能不同。面对这样的情形,程序员可选择等待响应——阻塞程序直到响应返回为止,也可继续执行其他有用的任务。大多数现代编程语言都提供了选择空间,让程序员可等待响应,也可继续做其他事情。

相反,服务员可并发地执行任务。这意味着在厨师做菜时服务员可以给其他顾客点菜,并在其他顾客点菜期间去取菜。

11.2 并发和并行

同时烤多个蛋挞被称为并发;而将烤蛋挞的任务分为两部分,由两家分别烤,烤好后再放在一起,这被称为并行。以并行的方式执行任务时,可利用并发性,也可不利用;它相当于将工作分成多个部分,各部分的工作完成后再将结果合并。

并发就是同时处理很多事情,而并行就是同时做很多事情。

Google日常工作的并发需求是催生出Go的动力之一,因为使用传统的系统语言难以编写高效的并发代码。

11.5 使用Goroutine处理并发操作

Goroutine使用起来非常简单,只需在要让Goroutine执行的函数或方法前加上关键字go即可

11.6 定义Goroutine

并发是编程语言提供的一种常见功能。例如,Node.js使用事件循环来管理并发,而Java使用线程。Apache和Nginx等Web服务器也使用不同的并发方法,Apache喜欢使用线程和进程,而Nginx使用事件循环。如果您不明白这些术语,也不用担心。这里的重点是,实现并发的方式有很多,它们以不同的方式使用计算机资源,这使得编写可靠的软件或难或易。

Go在幕后使用线程来管理并发,但Goroutine让程序员无须直接管理线程,它消除了这样做的痛苦。创建一个Goroutine只需占用几KB的内存,因此即便创建数千个Goroutine也不会耗尽内存。另外,创建和销毁Goroutine的效率也非常高。Goroutine是一个并发抽象,因此开发人员通常无须准确地知道操作系统中发生的情况。

11.8 问与答

答:Goroutine之所以立即返回,是因为这样才符合非阻塞执行的理念。

问:可在哪些情况下使用Goroutine?答:在事件发生顺序未知的情况下,使用Goroutine是不错的选择。比如网络调用、读取磁盘文件以及创建事件驱动的程序(如聊天应用和游戏)。

11.9 作业

1.并发地执行代码意味着程序可能能够更快地执行完毕,并在数据就绪后就返回它,而无须等待程序其他部分结束。

3.使用并发编程的其他情形包括从磁盘文件读写数据、从网络读写数据以及从数据库读写数据。

12.1 使用通道

“不要通过共享内存来通信,而通过通信来共享内存。”

虽然使用共享内存有其用武之地,但Go语言使用通道在Goroutine之间收发消息,避免了使用共享内存。严格地说,Goroutine并不是线程,但您可将其视为线程,因为它们能够以非阻塞方式执行代码。

事件发生时,可将触发的消息推送给接收者。使用共享内存时,程序必须检查共享内存。在变化频繁的并发编程环境中,很多人都认为使用消息是一种更佳的通信方式。

➢ 使用内置函数make创建一个通道,这是使用关键字chan指定的。➢ 关键字chan后面的string指出这个通道将用于存储字符串数据,这意味着这个通道只能用于收发字符串值。

<-,这表示将右边的字符串发送给左边的通道。如果通道被指定为收发字符串,则只能向它发送字符串消息,如果向它发送其他类型的消息将导致错误。

要从通道那里接收消息,需要在<-后面加上通道名。

声明变量msg,用于接收来自通道c的消息。这将阻塞进程直到收到消息为止,从而避免进程过早退出。

12.2 使用缓冲通道

在这种情况下,可使用缓冲通道。缓冲意味着可将数据存储在通道中,等接收者准备就绪再交给它。要创建缓冲通道,可向内置函数make传递另一个表示缓冲区长度的参数。

close在以前没介绍过。它用来关闭通道,禁止再向通道发送消息。

函数receiver使用range迭代通道,并将通道中缓冲的消息打印到控制台。

在知道需要启动多少个Goroutine或需要限制调度的工作量时,缓冲通道很有效。

12.3 阻塞和流程控制

Goroutine会立即返回(非阻塞),因此要让进程处于阻塞状态,必须采用一些流程控制技巧。例如,从通道接收并打印消息的程序需要阻塞,以免终止。给通道指定消息接收者是一个阻塞操作,因为它将阻止函数返回,直到收到一条消息为止。

如果要在接收指定数量的消息后结束,则可使用包含迭代器的for语句。指定的迭代次数完成后,进程将退出。

Goroutine是非阻塞的,因此如果程序要阻塞,以接收大量的消息或不断地重复某个过程,必须使用其他流程控制技术。

12.4 将通道用作函数参数

可将通道作为参数传递给函数,并在函数中向通道发送消息。要进一步指定在函数中如何使用传入的通道,可在传递通道时将其指定为只读、只写或读写的。

<-位于关键字chan左边时,表示通道在函数内是只读的;<-位于关键字chan右边时,表示通道在函数内是只写的;没有指定<-时,表示通道是可读写的。通过指定通道访问权限,有助于确保通道中数据的完整性,还可指定程序的哪部分可向通道发送数据或接收来自通道的数据。

12.5 使用select语句

假设有多个Goroutine,而程序将根据最先返回的Goroutine执行相应的操作,此时可使用select语句

select语句类似于第5章介绍的switch语句,它为通道创建一系列接收者,并执行最先收到消息的接收者。

如果从通道channel1那里收到了消息,将执行第一条case语句;如果从通道channel2那里收到了消息,将执行第二条case语句。具体执行哪条case语句,取决于消息到达的时间,哪条消息最先到达决定了将执行哪条case语句。通常,接下来收到的其他消息将被丢弃。收到一条消息后,select语句将不再阻塞。

➢ 创建两个向这些通道发送消息的函数。为模拟函数的执行速度,第一个函数休眠了1s,而第二个休眠了2s。➢ 启动两个Goroutine,分别用于执行这些函数。➢ select语句创建了两个接收者,分别用于接收来自通道channel1和channel2的消息。

但如果没有收到消息呢?为此可使用超时时间。这让select语句在指定时间后不再阻塞,以便接着往下执行。在程序清单12.6中,可添加一个超时case语句,指定在0.5s内没有收到消息时将采取的措施。

12.6 退出通道

:程序需要使用select语句实现无限制地阻塞,但同时要求能够随时返回。通过在select语句中添加一个退出通道,可向退出通道发送消息来结束该语句,从而停止阻塞。可将退出通道视为阻塞式select语句的开关。对于退出通道,可随便命名,但通常将其命名为stop或quit。

通过在for循环中使用select语句,可在收到消息后立即打印它。由于这是一个阻塞操作,因此将不断打印消息,直到您手动终止这个过程。

12.7 小结

用select语句来等待来自多个通道的消息,并根据来自哪个通道的消息最先到达来采取相应的措施。并发编程是一个庞大而复杂的主题,但只要掌握了如何使用通道在Goroutine之间通信,就能够实现很多并发功能!

12.8 问与答

:不能。通道只能有一种数据类型。您可创建任何类型的通道,因此可使用结构体来存储复杂的数据结构。

问:在select语句中,如果同时从两个通道那里收到消息,结果将如何?答:将随机地选择并执行一条case语句,且只执行被选中的case语句。

答:关闭缓冲通道意味着不能再向它发送消息。缓冲的消息会被保留,可供接收者读取。

12.9 作业

2.通过使用超时时间,可在指定时间过后从select语句返回,从而结束阻塞操作。select语句根据最先到达的消息执行相应的case语句;通过指定超时时间,可在给定时间内没有收到任何消息时从select语句返回。

3.要从一个通道那里接收10条消息后退出,可在迭代10次的for循环中使用select语句。

13.1 导入包

Go程序以package语句打头。main包是一种特殊的包,其特殊之处在于不能导入。对main包的唯一要求是,必须声明一个main函数,这个函数不接受任何参数且不返回任何值。简而言之,main包是程序的入口。

13.2 理解包的用途

bytes包包含用于处理字节的函数

13.3 使用第三方包

考虑使用第三方库时,您应自问如下几个问题。 ➢ 我明白了这些代码是做什么的吗? ➢ 这些代码值得信任吗? ➢ 这些代码的维护情况如何? ➢ 我真的需要这个库吗?

➢ 确定第三方包值得信任很重要。别忘了,将包导入程序后,它就能访问底层的操作系统。要确定第三方包的可信任程度,可了解还有多少人在使用它、是否有同事推荐,还可阅读其源代码。 ➢ 考虑到软件的特征,第三方包不可避免地存在Bug。不要选择几年都没有更新的包,而应选择开发方积极维护的第三方包,因为这意味着随着时间的推移,这样的包会越来越稳定。 ➢ 导入第三方包会增加程序的复杂性。很多时候导入一个包只为了使用其中的一个函数,在这种情况下,可复制这个函数,而不导入整个包。

13.5 管理第三方依赖

在大多数情况下,更新包都很简单,但有时可能很复杂,例如,在多个项目中使用了同一个第三方库时:项目A依赖于某个第三方库的1.2版,而项目B依赖于1.3版。 为应对这种情况,Go 1.5版引入了文件夹vendor,这能够让您将第三方模块添加到项目目录下的文件夹vendor中,并将所有包文件都移到这个文件夹中。这样可以不全局地安装包,而仅在项目中安装它。对于前面安装的stringutil包,可将其移到文件夹vendor中,这样项目结构将如下。

请注意,现在将只能在这个项目中使用stringutil包,因为它不是全局的。这种做法有一些优点。 ➢ 可锁定包的版本,为此只需将特定版本复制到项目目录中。 ➢ 构建服务器无须下载依赖,因为它们包含在项目中。

13.8 问与答

问:如何将第三方包的依赖复制到文件夹vendor中? 答:编写本书期间,Go没有提供将第三方包的依赖移到文件夹vendor的官方工具。您需要手工完成这种任务,或使用第三方依赖管理工具。

14.1 Go代码格式设置

在代码格式设置方面,Go语言采取了实用而严格的态度。Go语言指定了格式设置约定,这种约定虽然并非强制性的,但命令gofmt可以实现它。虽然编译器不要求按命令gofmt指定的那样设置代码格式,但几乎整个Go社区都使用gofmt,并希望按这个命令指定的方式设置代码格式。

14.2 使用gofmt

让工具gofmt修改文件,可使用标志 -w。这将使设置格式后的结果覆盖当前文件。

14.4 命名约定

在Go语言中,对于包含多个单词的变量名,约定是使用骆驼拼写法或帕斯卡拼写法,具体使用哪种拼写法,取决于变量是否需要导出。

在Go程序中,经常使用指出了数据类型的简短变量名,这让程序员能够专注于逻辑而不是变量。在这种情况下,i表示整型(Integer)数据类型,s表示字符串数据类型,等等。一开始,您可能觉得这样做不好,但时间久了就会习惯这种被普遍接受的约定。

在Go源代码中,接口名通常是这样得到的:在动词后面加上后缀er,形成一个名词。后缀er通常表示操作,因此这种命名方式表示操作,如Reader、Writer和ByteWriter。在有些情况下,这样生成的接口名可能不是现成的英语单词,如果您在Go源代码中搜索,将找到诸如Twoer这样的接口名。

在标准库中,math包就遵守了这样的约定:将计算平方根的函数命名为Sqrt而不是MathSqrt。这合乎情理,因为使用这个函数时,代码为math.Sqrt而不是math.MathSqrt,另

➢ 不熟悉代码的人是否只需看一眼就能大致知道它是做什么的?

14.6 使用godoc

godoc是一款官方工具,可通过分析Go语言源代码及其中的注释来生成文档。由于文档是根据源代码生成的,这很大程度上避免了文档不同步(软件项目中常见的问题)的问题。 虽然godoc是一款官方工具,但它必须单独安装。如果Go环境已搭建好,要安装这款工具只需安装相应的包即可。  go get golang.org/x/tools/cmd/godoc 要核实是否正确地安装了这款工具,可在终端中执行命令godoc--help。

请注意,每行都以它注释的类型的名称打头。注释是以大写字母打头的完整句子,以点结束。

例如,要查看strings包的文档,可执行如下命令。  godoc strings 这将把strings包的文档打印到标准输出中。如

您还可通过启动一个Web服务器来查看标准库文档。在您没有连接到网络或连接速度有限时,这种做法是一个不错的选择。  godoc -http=":6060" 执行这个命令后,您就可在浏

godoc $GOPATH/src/github.com/BurntSushi/toml 虽然开发人员可使用自己的约定,但godoc是事实标准,它提供了轻松编写和读取文档的标准方式。

14.9 问与答

答:虽然您可能不喜欢某些约定,但约定的优点在于大家都遵循。随着时间的推移,您会将这种不喜欢忘在脑后的。

问:我习惯了将常量名全大写,如CONSTANT,在Go语言中,也应这样做吗? 答:在Go语言中,常量与其他数据元素没什么不同,因此不应将其全大写。

14.10 作业

3.在接口命名方面,惯用的做法是使用一个动词,并在末尾加上er,如Parser和Authorizer,这样的名称描述了接口是做什么的。

15.1 测试:软件开发最重要的方面

单元测试针对一小部分代码,并独立地对它们进行测试。通常,这一小部分代码可能是单个函数,而要测试的是其输入和输出。

集成测试通常测试的是应用程序各部分协同工作的情况。如果说单元测试检查的是程序的最小组成部分,那么集成测试检查的就是应用程序各个组件协同工作的情况。集成测试还检查诸如网络调用和数据库连接等方面,以确保整个系统按期望的那样工作。通常,集成测试比单元测试更难编写,因为这些测试需要评估应用程序依赖的各个部分。

功能测试通常被称为端到端测试或由外向内的测试。这些测试从最终用户的角度核实软件按期望的那样工作,它们评估从外部看到的程序的运行情况,而不关心软件内部的工作原理。对用户来说,功能测试可能是最重要的测试。下面是一些功能测试的例子。

很多开发人员都提倡采用测试驱动开发(TDD)。这种做法从测试的角度考虑新功能,先编写测试来描述代码片段的功能,再着手编写代码。这很有多优点。 ➢ 有助于描述代码设计,因为考虑清楚代码片段的工作原理后,可改善代码设计。 ➢ 有助于提供有关功能工作原理的定义。 ➢ 未来可使用现成的测试来确定没有发生衰退。 ➢ 可使用现成的测试来核实正确地实现了代码。 通过采用TDD,工程师可改善设计,并根据确保测试得以通过来确认代码是有效的。

15.2 testing包

第一个约定是,Go测试与其测试的代码在一起。测试不是放在独立的测试目录中,而是与它们要测试的代码放在同一个目录中。

➢ 向这个函数传递了类型T,它包含很多用于测试代码的函数。

另一个约定是,在测试包中创建两个变量:got和want,它们分别表示要测试的值以及期望的值。程序清单15.2是一个简单的包,它返回一条问候语。

got want模式很有用,因为使用它可快速发现测试失败的原因。另外,通过在测试失败时显示有帮助的错误消息,可提高避免测试失败的速度。

15.3 运行表格驱动测试

Go语言提供了表格驱动测试模式,让您能够同时测试很多条件。程

➢ 创建一个结构体,用于存储编写测试所需的数据,包括输入和期望的输出。 ➢ 创建一个由结构体组成的切片,用于存储要测试的所有情形,包括期望输出。 ➢ 在测试中,遍历切片中的所有结构体,并测试实际输出是否与期望输出相同。

15.4 基准测试

Go提供了功能强大的基准测试框架,能够让您使用基准测试程序来确定完成特定任务时性能最佳的方式是哪一种。

testing包包含一个功能强大的基准测试框架,它能够让您反复地运行函数,从而建立基准。您无须指定运行函数的次数,因为基准测试框架将通过调整它来获得可靠的数据集。基准测试结束后,将生成一个报告,指出每次操作耗用了多少ns。

基准测试名以关键字Benchmark打头,它们接受一个类型为B的参数,并对函数进行基准测试。

这些基准测试使用循环反复地调用函数,以便建立基准。要运行这些测试,必须在命令go test中指定标志-bench。

15.5 提供测试覆盖率

测试覆盖率是度量代码测试详尽程度的指标,它指出了被测试执行了的代码所在的百分比值。

go test命令提供了标志 -cover,可指出测试覆盖率。

15.7 问与答

答:从长远看,编写测试物有所值,即便项目很小。编写测试看似是负担,但实际上有很多好处。因为这样做有助于您理解代码、确保代码实现正确无误以及发现修改过程中引入的衰退。

问:该以什么样的频率运行测试? 答:理想情况下,每次提交代码前都应运行测试。有些开发人员在每次保存文件后都运行测试,但这可能比较麻烦,而且会分散注意力。提交代码前是运行测试的最佳时机。

15.8 作业

.测试文件与被测试文件放在相同的文件夹中,但在被测试文件名后加上了后缀_test。

16.1 日志

Go语言提供了log包,让应用程序能够将日志写入终端或文件。程序清单16.1是一个简单的程序,它输出一条日志消息。

注意到日志消息中包含日期和时间,这在您以后查看日志时很有用。log包还可用来记录发生的致命错误。程序清单16.2生成了一个致命错误,并将其记录到了日志中。

要将日志写入文件,可使用Go语言本身提供的功能,也可使用操作系统提供的功能。要将日志写入文件,只需命令log包这样做即可。

log.SetOutput(f)

以正常方式运行这个程序时,将向终端输出5条消息。然而,通过使用重定向功能,可将输出重定向到文件。

通常,最好使用操作系统将日志重定向到文件,而不要使用Go语言代码。因为这种方法更灵活,能够让其他工具在必要时使用日志。

16.3 使用fmt包

因为函数Printf默认不会添加回车。动词%!v(MISSING)是类型的默认格式。

结合使用动词v和 + 来打印结构体中字段的名称。这很有用,因为这样您无须查找字段名,也无须记住结构体中字段的排列顺序。

16.4 使用Delve

Delve让开发人员能够与正在执行的程序交互、进入正在运行的进程以及查看核心转储和栈跟踪。

16.5 使用gdb

这将创建一个名为example10的二进制文件。要使用GNU调试器进行调试,首先需要像下面这样启动它。  gdb example10 您将看到大量的输出,最后进入显示程序已暂停执行的控制台。使用GNU调试器时,可使用命令list来查看代码的组成部分。

GNU调试器提供了丰富的调试环境,但它并非专用于Go语言。它提供了极细的调试粒度,让您能够逐行地执行代码。

16.7 问与答

发现Bug后,最好编写不能通过的相应测试。这样在调试代码的过程中,可再次运行这个测试,看看它能否通过。这样做还有另一个好处,那就是以后修改代码时如果引入了Bug,您将有相应的测试。

17.2 访问命令行参数

在Go语言中,要读取传递给命令行程序的参数,可使用标准库中的os包

在本章中,不使用命令go run来执行程序,而先使用go build来构建程序,再将其作为可执行文件来运行。这旨在让您熟悉命令行程序是可执行文件的概念,同时突出不同平台之间的一些细微差别。

17.3 分析命令行标志

虽然可使用os包来获取命令行参数,但Go语言还在标准库中提供了flag包。除os.Args的功能外,这个包还提供了众多其他的功能,其中包括以下几点。 ➢ 指定作为参数传递的值的类型。 ➢ 设置标志的默认值。 ➢ 自动生成帮助文本。

➢ 声明变量s并将其设置为flag.String返回的值。 ➢ flag.String能够让您声明命令行标志,并指定其名称、默认值和帮助文本。

➢ 调用flag.Parse,让程序能够传递声明的参数。

➢ 最后,打印变量s的值。请注意,flag.String返回的是一个指针,因此使用运算符*对其解除引用,以便显示底层的值。

17.5 自定义帮助文本

通过使用原始字符串字面量(放在之间的内容),将保留字符串的格式设置。

17.6 创建子命令

flag包通过FlagSets提供了子命令支持,让您能够创建子命令,并指定独立的标志集。

其中第一个参数为子命令名,而第二个参数则指定了错误处理行为。 ➢ flag.ContinueOnError:如果没有分析错误,就继续执行。 ➢ flag.ExitOnError:如果有分析错误,就退出并将状态码设置为2。 ➢ flag.PanicOnError:如果发生分析错误,就引发panic。

➢ 如果这个参数为uppercase,就在FlagSet uppercase中初始化一个字符串标志,再将其他参数传递给FlagSet uppercase,并对它们进行分析。

27: if len(os.Args) == 1 { 28: flag.Usage() 29: return 30: }

17.8 安装和分享命令行程序

您的Go项目位于这个文件夹中。请在这个文件夹中创建一个名为helloworld的文件夹。

go install github.com/[your github username]/helloworld

遵循Go语言的约定在于,您现在可以将代码提交到Github,让别人能够使用下面的命令轻松地安装它。  go get github.com/[your github username]/helloworld

17.10 问与答

问:Go语言为何将 -option和 --option视为同一个选项? 答:虽然使用一个连字符和两个连字符的选项通常是不同的选项,但Go设计者将它们视为相同的选项。

问:使用go install安装他人提供的命令行程序安全吗? 答:安装并运行包之前,务必检查其内容。在访问操作系统时,Go程序的权限很大,因此务必谨慎地运行它们。想象一下,您安装了一些恶意代码,并试图做些研究来确定它是安全的。您明白了它是做什么的吗?这个程序被广泛使用吗?有您认识的人在使用它吗?

第18章 创建HTTP服务器

Go语言为创建Web服务器提供了强大的支持

18.1 通过Hello World Web服务器宣告您的存在

➢ 在main函数中,使用方法HandleFunc创建了路由/。这个方法接受一个模式和一个函数,其中前者描述了路径,而后者指定如何对发送到该路径的请求做出响应。

➢ 函数helloWorld接受一个http.ResponseWriter和一个指向请求的指针。这意味着在这个函数中,可查看或操作请求,再将响应返回给客户端。

[ ]byte声明一个字节切片并将字符串值转换为字节。这意味着方法Write可以使用[ ]byte,因为这个方法将一个字节切片作为参数。

18.2 查看请求和响应

HandleFunc用于注册对URL地址映射进行响应的函数。简单地说,HandleFunc创建一个路由表,让HTTP服务器能够正确地做出响应。

➢ 路由必须完全匹配。例如,对于向 /users发出的请求,将定向到 /,因为这里末尾少了斜杆。

18.3 使用处理程序函数

路由器负责将路由映射到函数,但如何处理请求以及如何向客户端返回响应,是由处理程序函数定义的。

处理程序函数负责完成如下常见任务。 ➢ 读写报头。 ➢ 查看请求的类型。 ➢ 从数据库中取回数据。 ➢ 分析请求数据。 ➢ 验证身份。 处理程序函数能够访问请求和响应,因此一种常见的模式是,先完成对请求的所有处理,再将响应返回给客户端。

鉴于响应已经发送,这行代码不会有任何作用,但能够通过编译。这里要说的是,发送响应应是最后一步。

18.5 设置报头

只要这行代码是在响应被发送给客户端之前执行的,这个报头就会被添加到响应中。

18.6 响应以不同类型的内容

18.6 响应以不同类型的内容 响应客户端时,HTTP服务器通常提供多种类型的内容。一些常用的内容类型包括text/plain、text/html、application/json和application/xml。如果服务器支持多种类型的内容,客户端可使用Accept报头请求特定类型的内容。这意味着同一个URL可能向浏览器提供HTML,而向API客户端提供JSON。只需对本章的示例稍作修改,就可让它查看客户端发送的Accept报头,并据此提供不同类型的内容,如程序清单18.4所示。

switch r.Header.Get("Accept") { 13: case "application/json": 14: w.Header().Set("Content-Type", "application/json; charset=utf-8") 15: w.Write([]byte({"message": "Hello World"})) 16: case "application/xml": 17: w.Header().Set("Content-Type", "application/xml; charset=utf-8") 18: w.Write([]byte(<?xml version="1.0" encoding="utf-8"?><Message>Hello World</Message>) 19: default: 20: w.Header().Set("Content-Type", "text/plain; charset=utf-8") 21: w.Write([]byte("Hello World\n")) 22: } 23:

检查客户端发送的Accept报头。

18.7 响应不同类型的请求

在路由的处理程序函数中,可使用switch语句检查请求类型,并决定如何处理请求。

➢ 如果请求方法不是GET或POST,就执行default子句,发送501(Not Implemented HTTP)响应。代码501意味着服务器不明白或不支持客户端使用的HTTP请求方法。

18.8 获取GET和POST请求中的数据

reqBody, err := ioutil.ReadAll(r.Body)

这里简单地说一说安全问题。在服务器上,应将收到的数据视为不可信任的。攻击者可能发送请求,企图窃取信息、获取对服务器的访问权或删除数据库。所有进入服务器的数据都应视为不安全的,应过滤后再使用。

18.10 问与答

答:http包默认使用ServeMux来处理路由,因此不支持变量,也不支持正则表达式。社区创建的一些流行的路由器支持变量以及请求和内容类型等特性。一般而言,它们能够与http包集成起来

问:如何创建HTTPS服务器? 答:http包提供了方法ListenAndServeTLS,可用来创建HTTPS(TLS)服务器。这个方法的工作原理与ListenAndServe相同,但必须向它传递证书和密钥文件。

18.11 作业

1.GET请求向指定资源请求数据,而POST请求向指定资源提交数据。GET请求可能通过查询字符串参数发送数据,而POST请求通过消息体向服务器发送数据。

2.对ResponseWriter调用Write导致响应被发送给客户端,这包括报头和响应体的内容。响应一旦发送,就无法对其进行修改。

19.1 理解HTTP

这个在verbose模式下的curl描述了服务器和客户端之间传输的多个请求响应。其中的日志详细说明了发出的请求和收到的响应,对其解读如下。 ➢ 以字符 > 打头的行描述了客户端发出的请求。 ➢ 以字符 < 打头的行描述收到的响应。 ➢ 请求的详细信息描述了随请求发送的一些报头,这些报头向服务器提供了一些有关客户端的信息。 ➢ 响应详细描述了一些报头,这些报头指出了响应的内容类型、长度和发送时间。

19.2 发出GET请求

defer response.Body.Close() 16: body, err := ioutil.ReadAll(response.Body)

if err != nil { 18: log.Fatal(err) 19: }

➢ 如果请求出错(如没有网络连接),就将错误写入日志再退出。 ➢ 客户端读取所有数据后,将连接关闭。 ➢ 将响应体读取到变量中以便打印。

19.3 发出POST请求

程序清单19.2所示的客户端向https://httpbin.org/post发出POST请求。httpbin是一个用于测试HTTP客户端的工具,而端点 /post返回客户端发送给它的数据,以及有关客户端的信息。

➢ 声明变量postData并将一个JSON字符串赋给它。这里使用了标准库strings将其转换为io.Reader,为传输做好准备。

19.4 进一步控制HTTP请求

对为使用自定义HTTP客户端所做的修改解读如下。 ➢ 不使用net/http包的快捷方法Get,而创建一个HTTP客户端。

使用自定义HTTP客户端意味着可对请求设置报头、基本身份验证和cookies。鉴于使用快捷方法和自定义HTTP客户端时,发出请求所需代码的差别很小,建议除非要完成的任务非常简单,否则都使用自定义HTTP客户端。

19.5 调试HTTP请求

net/http/httputil也提供了能够让您轻松调试HTTP客户端和服务器的方法。这个包中的方法DumpRequestOut和DumpResponse能够让您查看请求和响应。

debug := os.Getenv("DEBUG")

if debug == "1" { 21: debugRequest, err := httputil.DumpRequestOut(request, true)

19.6 处理超时

HTTP事务会为接收响应等待一定的时间。客户端向服务器发送请求后,完全无法知道响应会在多长时间内返回。在底层,有大量影响响应速度的变数。 ➢ DNS查找速度。 ➢ 打开到服务器IP地址的TCP套接字的速度。 ➢ 建立TCP连接的速度。 ➢ TLS握手的速度(如果连接是TLS的)。 ➢ 向服务器发送数据的速度。 ➢ 重定向的速度。 ➢ Web服务器返回响应的速度。 ➢ 将数据传输到客户端的速度。

使用默认的HTTP客户端时,没有对请求设置超时时间。这意味着如果服务器没有响应,则请求将无限期地等待或挂起。对于任何请求,都建议设置超时时间。这样如果请求在指定的时间内没有完成,将返回错误。

通过创建一个传输(transport)并将其传递给客户端,可更细致地控制超时:控制HTTP连接的各个阶段。在大多数情况下,使用Timeout就足以控制整个HTTP事务,但在Go语言中,还可通过创建传输来控制HTTP事务的各个部分。

19.8 问与答

问:能够同时发出多个HTTP请求吗? 答:可以。通过使用goroutine,客户端可同时发出多个HTTP请求。

问:能够根据返回HTTP状态码调整程序采取的措施吗? 答:可以。可通过Response.StatusCode来访问响应的状态码,因此可编写基于服务器响应的逻辑。

19.9 作业

1.报头Accept告诉服务器,客户端能够接收哪些类型的内容。报头Content-Type是服务器发送的,它指出当前发送给客户端的是哪种类型的数据。如果客户端使用报头Accept向服务器请求application/json,而服务器也支持这种类型,就应在返回数据时将报头Content-Type设置为application/json。

client := &http.Client{ Timeout: 1 * time.Second, } resp, err := client.Get("http://XXX.com")

如果需要控制报头和请求的其他方面,应使用方法NewRequest。

第20章 处理JSON

您将明白如何使用结构体标签(struct tags)细致地控制JSON,还有如何使用HTTP从远程API取回JSON。

20.1 JSON简介

JavaScript对象表示法(JavaScript Object Notation,JSON)是一种用于存储和交换数据的格式,这是一种人类能够理解的纯文本格式。JSON可以键值对的方式表示数据,也可以数组的方式表示数据。JSON最初是一个JavaScript子集,但它现在已独立于语言,实际上,大多数语言都支持JSON数据编码和解码。JSON已成为互联网上存储和交换数据的事实标准,

虽然Go语言也支持数组,但更常见的是使用切片来表示一组元素。在其他编程语言中,数组也被称为列表,尽管这有点令人迷惑,但数组和列表的定义相同,都是一组元素。

JSON通常更为轻量级。通过诸如互联网等网络发送数据时,可能意味着应用程序的运行速度稍快。另外,JavaScript是占统治地位的Web浏览器编程环境,而JSON是JavaScript的一个子集,因此编码和解码JSON数据就是小菜一碟。

20.2 使用JSON API

近年来,互联网上出现了很多卓越的JSON API。现在,通过作为数据交换平台的互联网,可获得大量有关任何主题的数据。API让程序员无须直接连接到数据库,就可以请求各种格式的数据并使用它们。这

应用程序开发人员已使用很多这样的API来开发新颖而有趣的产品和服务。例如,有很多用于Android系统的Dark Sky客户端。还有一种新的商业模式:数据提供商提供数据,客户可随心所欲地使用这些数据。

20.3 在Go语言中使用JSON

标准库提供了encoding/json包,可用于编码和解码JSON数据。

encoding/json包提供了函数Marshal,可用于将Go数据编码为JSON。

要将这些数据编码为JSON格式,可使用函数Marshal。这个函数接受一个接口,并返回一个字节串。由于结构体可能实现了接口,因此可将其作为参数直接传递给函数Marshal。 

名都以大写字母打头。

乍一看,对所有的键进行分析,并让它们以小写字母开头好像是一项艰巨的任务,所幸Go语言的设计者已经解决了这种问题。 对于结构体,您可给其数据字段指定标签,对于带JSON标签的数据,将使用标签中的数据替换它。要正确地转换为JSON要求的骆驼拼写法,只需给结构体的字段加上标签即可。

结构体标签还可用于指定在编码为JSON时忽略结构体中的空字段。默认情况下,如果结构体的字段被设置为空值,则编码为JSON格式后,将包含Go语言零值规则指定的值。

要指定在编码为JSON格式时忽略零值,可使用结构体标签指出字段可能为空,如果确实为空就忽略它。为此,可在JSON键名后面加上omitempty。

20.4 解码JSON

函数Unmarshal接受一个字节切片以及一个指定要将数据解码为何种格式的接口。根据数据是如何收到的,它可能是字节切片,也可能不是。如果不是字节切片,就必须先进行转换,再将其传递给函数Unmarshal。

与将数据编码为JSON格式一样,必须定义一个接口,以指定要将数据解码为何种格式。与将数据编码为JSON格式一样,可使用结构体标签来告诉解码器如何将键映射到字段。

20.6 处理通过HTTP收到的JSON

在Go语言中,通过HTTP请求获取JSON时,收到的数据为流而不是字符串或字节切片。

由于获取的数据为流,因此可使用encoding/json包中的函数NewDecoder。这个函数接受一个io.Reader(这正是http.Get返回的类型),并返回一个Decoder。通过对返回的Decoder调用方法Decode,可将数据解码为结构体。与以前一样,Decode也接受一个结构体,因此必须创建一个结构体实例,并将其作为参数传递给Decode。

20.8 问与答

答:是的,要编码或解码JSON,必须创建结构体。虽然这好像很繁琐,在JSON对象很大时尤其如此,但正如您在前面的Github示例中看到的,这样做可让代码更健壮、容错能力更强。如果您能将JSON类型映射到Go类型,就能将JSON用作数据交换格式,还可获得类型安全这样的好处。网上有多个服务可根据JSON数据自动创建Go结构。

20.9 作业

1.JavaScript中的数字对应于Go数据类型float64。

2.不是这样的,可定义只包含您感兴趣的字段的结构体。您可使用结构体标签来将JSON字段映射到Go结构体字段。

3.如果一个字段可能为空,应给它添加结构体标签omitempty。这样解码时,如果该字段确实为空,将忽略它。

21.1 文件的重要性

在这方面,UNIX走得更远,它通过虚拟文件系统来暴露系统信息。这意味着可像读取文件一样读取系统数据。

鉴于UNIX系统以文件的方式暴露系统数据,因此命令cat也可用来提取有关底层系统的信息。/proc/loadavg就是这样的一个虚拟文件,它包含有关系统当前负载的信息。 

21.2 使用ioutil包读写文件

读取文件是最常见的操作之一。ioutil包提供了函数Readfile,您可使用它来完成这项任务,这个函数将一个文件名作为参数,并以字节切片的方式返回文件的内容。这意味着如果要将文件内容作为字符串使用,则必须将返回的字节切片转换为字符串。

最左边的字符指出了文件是普通文件、目录还是其他东西,如果这个字符为-,就表示文件为普通文件;接下来的3个字符指定了文件所有者的权限;再接下来的3个字符表示所有者所在用户组的权限;而最后3个字符表示其他人的权限。

在UNIX型系统中,文件的默认权限为0644,即所有者能够读取和写入,而其他人只能读取。在文件系统中创建文件时,应考虑如何给它指定权限。如果不确定该如何指定权限,使用默认的UNIX权限就可以了。

➢ 函数WriteFile接受一个字节切片,因此创建一个空字节切片,并将其赋给变量b。

21.3 写入文件

要写入文件,只需传入一些值,而不是传入空字节切片。要将字符串写入文件,必须先将其转换为字节切片。

21.9 问与答

答:Go语言致力于确保核心库为小巧而轻量级的。另外,不同的操作性系统差别很大,这导致创建通用的文件复制方法是很难的。鉴于此,没有用于复制文件的便利方法,要复制文件,应使用os包。

问:操作文件前,Go语言是否会核实文件确实存在? 答:不会,程序员必须在使用文件前核实它确实存在。如果文件不存在,将引发错误

22.1 定义正则表达式

正则表达式虽然难学,但其功能非常强大。使用正则表达式可完成验证数据、查找数据以及操作大量文本等任务。相比于其他方法,表达式查找和模式匹配的效率要高得多

在Go语言中,正则表达式功能是由regex包提供的,这个包实现了正则表达式的查找和模式匹配功能。它使用的是RE2语法,这大致与Perl和Python使用的语法相同。它操作的目标可以是字符串,也可以是字节。

22.3 使用正则表达式验证数据

➢ Compile:在正则表达式未能通过编译时返回错误。 ➢ MustCompile:在正则表达式无法编译时引发panic。 该使用哪一个取决于具体情况,但MustCompile通常是更佳的选择。

22.4 使用正则表达式来变换数据

一个常见的编程任务是对数据进行清洗,以便能够安全地使用它们。

22.6 问与答

对普通大众来说,创建复杂正则表达式时都需参阅文档并对模式进行测试。

23.1 时间元素编程

这个时间是怎么来的呢?难道Go语言有神奇的时钟可供参考吗?实际上,它来自底层操作系统。取决于底层操作系统中的时间准确度,这种时间可能很有用,也可能没有用。在大多数操作系统中,用户都可设置时间。 例如,在Linux系统中,您可进行时间旅行,将时间设置为未来的。下面的命令将时间设置为2050年元旦。  sudo date +%!Y(MISSING)%!m(MISSING)%!d(MISSING) -s "20500101"

如您所见,时间受众多变数的影响,其中包括在操作系统中设置的时间不正确。鉴于此,很多系统管理员会安装将时间与网络时钟同步的服务。网络时间协议(Network Time Protocol,NTP)是一种在整个网络中同步时间的网络协议,使用NTP的不同计算机更有可能就时间达成一致,但在本地它们依然可以设置不同的时区。

在计算中,要消除时区的影响,可参照世界标准时间(Coordinated Universal Time,UTC)。UTC是时间标准而非时区,它让不同地区的计算机有相同的参照物,而不用考虑相对时区。

23.3 设置超时时间

要在特定的时间过后执行某项操作,可使用函数After。

23.4 使用ticker

使用ticker可让代码每隔特定的时间就重复执行一次。需要在很长的时间内定期执行任务时,这么做很有用。

23.8 比较两个不同的Time结构体

time包提供了方法Before、After和Equal。这些方法都比较两个Time结构体,并返回一个布尔值。

23.10 问与答

UTC使用的是ISO 8601格式,这种格式在网上得到了很好的支持。RFC3339是一个ISO 8601扩展。这两种标准都是不错的选择,如果有疑问,可使用得到广泛支持的ISO 8601标准

23.11 作业

1.壁挂钟显示的时间受时区和时间调整的影响,并不适合用来度量时间。单调时钟是稳定的,适合用来度量时间。

第24章 部署Go语言代码

您将学习如何将二进制文件放在Docker容器中,以及提供可下载的二进制文件时需考虑的安全因素。

24.1 理解目标

使用Go语言编程时,编写的代码只需做少量的修改甚至无须修改就可在最常见的平台中运行。

Go编程语言的优点之一是,支持大量的操作系统和体系结构组合,因此程序员可为这些不同的目标编译Go代码。

24.2 压缩二进制文件的大小

发布Go语言代码很简单:通过编译生成一个二进制文件,其中包含运行程序所需的一切,因此您无须考虑依赖的问题。在诸如Node.js和Ruby等语言中,要将应用程序部署到生产环境,必须组合所有的依赖,并发布一系列文件。使用诸如Go等编译型语言时,这个过程要简单得多

其生成的二进制文件为1.5MB。怎么会这样呢?因为它包含执行这个程序所需的一切,其中包括Go语言run-time。使用诸如Ruby和Node.js等语言时,由于run-time安装在服务器上,因此只需发布需要执行的文件。虽然这个二进制文件看起来较大,但优点是它不需要任何依赖,就能在编译时指定的系统中运行。

通过指定一些编译标志,可压缩编译得到的二进制文件的大小。这些标志指定省略符号表、调试信息和DWARF符号表。

这些设备的存储空间通常有限,在这种情况下,可使用工具upx。在Linux系统中,通常使用包管理器就能安装它。它对二进制文件进行压缩,这意味着运行时必须解压缩。虽然解压缩的速度很快,但这意味着启动时间会稍长些。

24.3 使用Docker

您可能听说过术语“开发和运维”,这指的是帮助软件小组快速、安全、平稳地发布代码。通过与其他工具结合起来使用,Docker可帮助您完成这个过程。 通过使用Docker,可建立部署管道,这意味着开发人员只需将代码提交到仓库,几分钟后它们就将奇迹般地自动发布到生产环境。

Go官网维护着一些Docker映像,让您能够编译代码并在Docker容器中运行它们。这些映像主要用于在自动化环境中测试代码,但也可用于部署代码。

docker run -p 8000:8000 hello-go:latest 这个命令将容器的端口8000绑定到主机,这意味着可以访问这个容器了。如果您在浏览器中输入http://localhost:8000/,这个应用程序将响应以文本Hello World。

24.4 下载二进制文件

为证明可下载的文件名副其实,有两种常用的方法:一是将文件托管到有证书能够证明其身份的https网站,这让证书签发机构能够在用户连接到网站时验证其所有者:第二种常用的方法是提供文件的校验和,这个校验和相当于文件的指纹。 对于任何文件,都可使用相关的校验和查看器来查看其校验和。下面来编译代码,并查看生成的文件的校验和。

校验和相当于独一无二的指纹,通过在下载网站随文件一起发布校验和,可在一定程度上保证文件就是您上传的文件

再为得到的文件生成校验和时,生成的散列值将完全不同。

另外,从互联网上下载Go二进制文件或其他软件时,务必检查提供的校验和,确认它与您为下载的文件而生成的校验和相同。

一个采用了这种做法的项目是Terraform。这是一个使用Go语言编写的开源项目,它可以让用户配置云服务,以提供可重复的基础设施环境,用于部署代码。在Terraform下载页面,有用于各种平台和体系结构的二进制文件,还有列出这些可下载文件的校验和的链接。用户可下载二进制文件,并检查其校验和是否与发布的校验和相同。

24.5 使用go get

第21章介绍了如何安装第三方包,而命令go get也可用来安装命令行工具,官方采用的也是这种方法。

24.8 问与答

如果您要部署Web应用,使用Docker是不错的选择;如果您要分享命令行工具,将其上传到网上供人下载或使用go get是不错的选择;

问:Go二进制文件真的不需要依赖吗?答:不需要。这是使用静态链接的二进制文件的优点之一。只要二进制文件是针对正确的目标编译而成的,运行它时就不需要其他任何东西。

如果您是软件小组的一员,并要部署Web产品,就很可能要用到Docker。包括Google在内的很多大型软件公司都使用容器来管理基础设施。

24.9 作业

2.可以。这被称为跨平台编译。然而,在与目标平台相同的平台上编译时,速度通常更快!