Go-编程秘籍第二版(全)
Go 编程秘籍第二版(全)
原文:
zh.annas-archive.org/md5/6A3DCC49D461FA27A010AAE9FBA229E0
译者:飞龙
前言
感谢您选择这本书!我希望它能成为开发人员快速查阅 Go 开发模式的便利参考。它旨在成为其他资源的伴侣和一个参考,希望在阅读一次后长久有用。本书中的每个配方都包括可用作参考或应用程序基础的工作、简单和经过测试的代码。本书涵盖了从基础到高级主题的各种内容。
这本书是为谁准备的
这本书面向 Web 开发人员、程序员和企业开发人员。假定具有 Go 语言的基本知识。对后端应用程序开发的经验并非必需,但可能有助于理解一些配方背后的动机。
这本书是为已经熟练的 Go 开发人员提供快速提醒、示例或参考的好资源。通过开源存储库,也可以快速与团队分享这些示例。如果你正在寻找 Go 编程中常见和不太常见问题的快速解决方案,这本书适合你。
本书涵盖了什么
第一章,“I/O 和文件系统”,涵盖了常见的 Go I/O 接口,并探讨了与文件系统的工作。这包括临时文件、模板和 CSV 文件。
第二章,“命令行工具”,介绍了通过命令行接受用户输入,并探讨了处理常见数据类型如 TOML、YAML 和 JSON。
第三章,“数据转换和组合”,演示了在 Go 接口和数据类型之间进行转换和转换的方法。它还展示了 Go 的编码策略和一些功能设计模式。
第四章,“Go 中的错误处理”,展示了处理 Go 中错误的策略。它探讨了如何传递错误、处理错误和记录错误。
第五章,“网络编程”,演示了各种网络原语的使用,如 UDP 和 TCP/IP。它还探讨了域名系统(DNS)、处理原始电子邮件消息以及基本的远程过程调用(RPC)。
第六章,“关于数据库和存储的一切”,涉及了用于访问数据存储系统(如 MySQL)的各种存储库。它还演示了使用接口来将库与应用程序逻辑解耦。
第七章,“Web 客户端和 API”,实现了 Go HTTP 客户端接口、REST 客户端、OAuth2 客户端、装饰和扩展客户端以及 gRPC。
第八章,“Go 语言应用的微服务”,探讨了 web 处理程序、向处理程序传递状态、用户输入验证和中间件。
第九章,“测试 Go 代码”,着重于模拟、测试覆盖率、模糊测试、行为测试和有用的测试工具。
第十章,“并行和并发”,提供了通道和异步操作、原子值、Go 上下文对象和通道状态管理的参考。
第十一章,“分布式系统”,实现了服务发现、Docker 容器化、度量和监控以及编排。它主要涉及 Go 应用程序的部署和生产。
第十二章,“响应式编程和数据流”,探讨了响应式和数据流应用程序、Kafka 和分布式消息队列以及 GraphQL 服务器。
第十三章,无服务器编程,涉及在不维护服务器的情况下部署 Go 应用程序。这包括使用 Google App Engine,Firebase,Lambda 以及在无服务器环境中登录。
第十四章,性能改进,技巧和窍门,涉及基准测试,识别瓶颈,优化和改进 Go 应用程序的 HTTP 性能。
为了充分利用本书
要使用本书,您需要以下内容:
-
Unix 编程环境。
-
Go 1.x 系列的最新版本。
-
互联网连接。
-
根据每章描述安装附加软件包的权限。
-
每个配方的先决条件和其他安装要求都在各章的技术要求部分中提到。
下载示例代码文件
您可以从您在www.packtpub.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便文件直接通过电子邮件发送给您。
您可以按照以下步骤下载代码文件:
-
在www.packtpub.com登录或注册。
-
选择“支持”选项卡。
-
单击“代码下载和勘误”。
-
在搜索框中输入书名并按照屏幕上的说明操作。
下载文件后,请确保使用最新版本的解压缩或提取文件夹:
-
Windows 的 WinRAR/7-Zip
-
Mac 的 Zipeg/iZip/UnRarX
-
Linux 的 7-Zip/PeaZip
该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition
。我们还有其他代码包,可以从我们丰富的书籍和视频目录中获得,网址为github.com/PacktPublishing/
。请查看!
代码实例
访问以下链接以查看代码运行的视频:bit.ly/2J2uqQ3
使用的约定
本书中使用了许多文本约定。
CodeInText
:指示文本中的代码字,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄。这是一个例子:“bytes
库在处理数据时提供了许多方便的功能。”
代码块设置如下:
b, err := ioutil.ReadAll(r)
if err != nil {
return "", err
}
return string(b), nil
}
当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体设置:
package bytestrings
import (
"bytes"
"io"
"io/ioutil"
)
任何命令行输入或输出都以以下方式编写:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/Chapter01/interfaces
粗体:表示新术语,重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会以这种方式出现在文本中。这是一个例子:“从管理面板中选择系统信息。”
警告或重要说明会以这种方式出现。
技巧和窍门会以这种方式出现。
各节
在本书中,您会经常看到几个标题(准备工作,如何做…,工作原理…,还有更多…和另请参阅)。
为了清晰地说明如何完成一个配方,使用以下各节:
准备工作
本节告诉您在配方中可以期待什么,并描述了为配方设置任何所需软件或任何预备设置的方法。
如何做…
本节包含遵循配方所需的步骤。
工作原理…
本节通常包括对上一节中发生的事情的详细解释。
还有更多…
本节包含有关配方的其他信息,以使您对配方更加了解。
另请参阅
本节为配方提供了其他有用信息的链接。
第一章:I/O 和文件系统
Go 提供了对基本和复杂 I/O 的出色支持。本章中的配方将探讨用于处理 I/O 的常见 Go 接口,并向您展示如何使用它们。Go 标准库经常使用这些接口,并且它们将被本书中的配方使用。
您将学习如何处理内存中的数据和流式数据。您将看到有关处理文件、目录和 CSV 格式的示例。临时文件配方介绍了一种处理文件的机制,而无需处理名称冲突等开销。最后,我们将探讨 Go 标准模板,包括纯文本和 HTML。
这些配方应该为使用接口来表示和修改数据奠定基础,并应该帮助您以抽象和灵活的方式思考数据。
在本章中,将介绍以下配方:
-
使用常见的 I/O 接口
-
使用 bytes 和 strings 包
-
处理目录和文件
-
处理 CSV 格式
-
使用临时文件
-
使用 text/template 和 html/template
技术要求
为了继续本章中的所有配方,请根据以下步骤配置您的环境:
-
在您的操作系统上下载并安装 Go 1.12.6 或更高版本,网址为
golang.org/doc/install
。 -
打开终端或控制台应用程序,并创建并转到一个项目目录,例如
~/projects/go-programming-cookbook
。所有代码将从该目录运行和修改。 -
将最新的代码克隆到
~/projects/go-programming-cookbook-original
中,如下所示。建议您从该目录中工作,而不是手动输入示例:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
使用常见的 I/O 接口
Go 语言提供了许多 I/O 接口,这些接口在整个标准库中使用。最佳实践是尽可能使用这些接口,而不是直接传递结构或其他类型。我们将在本配方中探讨的两个强大接口是io.Reader
和io.Writer
接口。这些接口在整个标准库中使用,了解如何使用它们将使您成为更好的 Go 开发人员。
Reader
和Writer
接口如下所示:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
Go 还可以轻松地组合接口。例如,看一下以下代码:
type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}
type ReadSeeker interface {
Reader
Seeker
}
本配方还将探讨一个名为Pipe()
的io
函数,如下所示:
func Pipe() (*PipeReader, *PipeWriter)
本书的其余部分将使用这些接口。
如何做...
以下步骤涵盖了如何编写和运行您的应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter1/interfaces
的新目录。 -
转到此目录。
-
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter1/interfaces
您应该会看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter1/interfaces
-
从
~/projects/go-programming-cookbook-original/chapter1/interfaces
复制测试,或者使用这个作为练习来编写一些您自己的代码! -
创建一个名为
interfaces.go
的文件,内容如下:
package interfaces
import (
"fmt"
"io"
"os"
)
// Copy copies data from in to out first directly,
// then using a buffer. It also writes to stdout
func Copy(in io.ReadSeeker, out io.Writer) error {
// we write to out, but also Stdout
w := io.MultiWriter(out, os.Stdout)
// a standard copy, this can be dangerous if there's a
// lot of data in in
if _, err := io.Copy(w, in); err != nil {
return err
}
in.Seek(0, 0)
// buffered write using 64 byte chunks
buf := make([]byte, 64)
if _, err := io.CopyBuffer(w, in, buf); err != nil {
return err
}
// lets print a new line
fmt.Println()
return nil
}
- 创建一个名为
pipes.go
的文件,内容如下:
package interfaces
import (
"io"
"os"
)
// PipeExample helps give some more examples of using io
//interfaces
func PipeExample() error {
// the pipe reader and pipe writer implement
// io.Reader and io.Writer
r, w := io.Pipe()
// this needs to be run in a separate go routine
// as it will block waiting for the reader
// close at the end for cleanup
go func() {
// for now we'll write something basic,
// this could also be used to encode json
// base64 encode, etc.
w.Write([]byte("test\n"))
w.Close()
}()
if _, err := io.Copy(os.Stdout, r); err != nil {
return err
}
return nil
}
-
创建一个名为
example
的新目录并进入该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"bytes"
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter1/bytestrings"
)
func main() {
in := bytes.NewReader([]byte("example"))
out := &bytes.Buffer{}
fmt.Print("stdout on Copy = ")
if err := interfaces.Copy(in, out); err != nil {
panic(err)
}
fmt.Println("out bytes buffer =", out.String())
fmt.Print("stdout on PipeExample = ")
if err := interfaces.PipeExample(); err != nil {
panic(err)
}
}
-
运行
go run .
。 -
您也可以运行以下命令:
$ go build $ ./example
您应该会看到以下输出:
$ go run .
stdout on Copy = exampleexample
out bytes buffer = exampleexample
stdout on PipeExample = test
- 如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
,确保所有测试都通过。
它是如何工作的...
Copy()
函数在接口之间复制字节,并将该数据视为流。将数据视为流在许多实际用途中非常有用,特别是在处理网络流量或文件系统时。Copy()
函数还创建了一个MultiWriter
接口,它将两个写入流组合在一起,并使用ReadSeeker
两次写入它们。如果使用了Reader
接口,而不是看到exampleexample
,您只会看到example
,尽管将数据复制到MultiWriter
接口两次。如果您的流无法适应内存,还可以使用缓冲写入。
PipeReader
和PipeWriter
结构实现了io.Reader
和io.Writer
接口。它们连接在一起,创建一个内存管道。管道的主要目的是从流中读取数据,同时将相同流中的数据写入到不同的源。本质上,它将两个流合并成一个管道。
Go 接口是一种干净的抽象,用于包装执行常见操作的数据。这在进行 I/O 操作时变得明显,因此io
包是学习接口组合的一个很好的资源。pipe
包通常被低估,但在链接输入和输出流时提供了很大的灵活性和线程安全性。
使用bytes
和strings
包
bytes
和strings
包提供了许多有用的辅助函数,用于处理和转换字符串和字节类型的数据。它们允许创建与许多常见 I/O 接口一起使用的缓冲区。
如何做...
以下步骤涵盖了如何编写和运行您的应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter1/bytestrings
的新目录。 -
导航到此目录。
-
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter1/bytestrings
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter1/bytestrings
-
从
~/projects/go-programming-cookbook-original/chapter1/bytestrings
复制测试,或者将其用作练习编写一些自己的代码! -
创建一个名为
buffer.go
的文件,其中包含以下内容:
package bytestrings
import (
"bytes"
"io"
"io/ioutil"
)
// Buffer demonstrates some tricks for initializing bytes
//Buffers
// These buffers implement an io.Reader interface
func Buffer(rawString string) *bytes.Buffer {
// we'll start with a string encoded into raw bytes
rawBytes := []byte(rawString)
// there are a number of ways to create a buffer from
// the raw bytes or from the original string
var b = new(bytes.Buffer)
b.Write(rawBytes)
// alternatively
b = bytes.NewBuffer(rawBytes)
// and avoiding the initial byte array altogether
b = bytes.NewBufferString(rawString)
return b
}
// ToString is an example of taking an io.Reader and consuming
// it all, then returning a string
func toString(r io.Reader) (string, error) {
b, err := ioutil.ReadAll(r)
if err != nil {
return "", err
}
return string(b), nil
}
- 创建一个名为
bytes.go
的文件,其中包含以下内容:
package bytestrings
import (
"bufio"
"bytes"
"fmt"
)
// WorkWithBuffer will make use of the buffer created by the
// Buffer function
func WorkWithBuffer() error {
rawString := "it's easy to encode unicode into a byte
array"
b := Buffer(rawString)
// we can quickly convert a buffer back into byes with
// b.Bytes() or a string with b.String()
fmt.Println(b.String())
// because this is an io Reader we can make use of
// generic io reader functions such as
s, err := toString(b)
if err != nil {
return err
}
fmt.Println(s)
// we can also take our bytes and create a bytes reader
// these readers implement io.Reader, io.ReaderAt,
// io.WriterTo, io.Seeker, io.ByteScanner, and
// io.RuneScanner interfaces
reader := bytes.NewReader([]byte(rawString))
// we can also plug it into a scanner that allows
// buffered reading and tokenzation
scanner := bufio.NewScanner(reader)
scanner.Split(bufio.ScanWords)
// iterate over all of the scan events
for scanner.Scan() {
fmt.Print(scanner.Text())
}
return nil
}
- 创建一个名为
string.go
的文件,其中包含以下内容:
package bytestrings
import (
"fmt"
"io"
"os"
"strings"
)
// SearchString shows a number of methods
// for searching a string
func SearchString() {
s := "this is a test"
// returns true because s contains
// the word this
fmt.Println(strings.Contains(s, "this"))
// returns true because s contains the letter a
// would also match if it contained b or c
fmt.Println(strings.ContainsAny(s, "abc"))
// returns true because s starts with this
fmt.Println(strings.HasPrefix(s, "this"))
// returns true because s ends with this
fmt.Println(strings.HasSuffix(s, "test"))
}
// ModifyString modifies a string in a number of ways
func ModifyString() {
s := "simple string"
// prints [simple string]
fmt.Println(strings.Split(s, " "))
// prints "Simple String"
fmt.Println(strings.Title(s))
// prints "simple string"; all trailing and
// leading white space is removed
s = " simple string "
fmt.Println(strings.TrimSpace(s))
}
// StringReader demonstrates how to create
// an io.Reader interface quickly with a string
func StringReader() {
s := "simple stringn"
r := strings.NewReader(s)
// prints s on Stdout
io.Copy(os.Stdout, r)
}
-
创建一个名为
example
的新目录,并导航到该目录。 -
创建一个名为
main.go
的文件,其中包含以下内容:
package main
import "github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter1/bytestrings"
func main() {
err := bytestrings.WorkWithBuffer()
if err != nil {
panic(err)
}
// each of these print to stdout
bytestrings.SearchString()
bytestrings.ModifyString()
bytestrings.StringReader()
}
-
运行
go run .
。 -
您还可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run .
it's easy to encode unicode into a byte array ??
it's easy to encode unicode into a byte array ??
it'seasytoencodeunicodeintoabytearray??true
true
true
true
[simple string]
Simple String
simple string
simple string
- 如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
,确保所有测试都通过。
它是如何工作的...
bytes
库在处理数据时提供了许多便利函数。例如,与字节数组相比,缓冲区在处理流处理库或方法时更加灵活。创建缓冲区后,它可以用于满足io.Reader
接口,以便您可以利用ioutil
函数来操作数据。对于流应用程序,您可能希望使用缓冲区和扫描器。bufio
包在这些情况下非常有用。有时,对于较小的数据集或者在计算机上有大量内存时,使用数组或切片更为合适。
在使用这些基本类型时,Go 在转换数据之间的接口方面提供了很大的灵活性——在字符串和字节之间进行转换相对简单。在处理字符串时,strings
包提供了许多便利函数,用于处理、搜索和操作字符串。在某些情况下,一个良好的正则表达式可能是合适的,但大多数情况下,strings
和strconv
包就足够了。strings
包允许您将字符串看起来像标题,将其拆分为数组,或修剪空白。它还提供了自己的Reader
接口,可以用于代替bytes
包的读取器类型。
处理目录和文件
在切换平台(例如 Windows 和 Linux)时,处理目录和文件可能会很困难。Go 在os
和ioutils
包中提供了跨平台支持,以处理文件和目录。我们已经看到了ioutils
的示例,现在我们将探讨如何以另一种方式使用它们!
如何做...
以下步骤涵盖了如何编写和运行您的应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter1/filedirs
的新目录。 -
进入此目录。
-
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter1/filedirs
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter1/filedirs
-
从
~/projects/go-programming-cookbook-original/chapter1/filedirs
复制测试,或者利用这个机会编写一些您自己的代码! -
创建一个名为
dirs.go
的文件,其中包含以下内容:
package filedirs
import (
"errors"
"io"
"os"
)
// Operate manipulates files and directories
func Operate() error {
// this 0755 is similar to what you'd see with Chown
// on a command line this will create a director
// /tmp/example, you may also use an absolute path
// instead of a relative one
if err := os.Mkdir("example_dir", os.FileMode(0755));
err != nil {
return err
}
// go to the /tmp directory
if err := os.Chdir("example_dir"); err != nil {
return err
}
// f is a generic file object
// it also implements multiple interfaces
// and can be used as a reader or writer
// if the correct bits are set when opening
f, err := os.Create("test.txt")
if err != nil {
return err
}
// we write a known-length value to the file and
// validate that it wrote correctly
value := []byte("hellon")
count, err := f.Write(value)
if err != nil {
return err
}
if count != len(value) {
return errors.New("incorrect length returned
from write")
}
if err := f.Close(); err != nil {
return err
}
// read the file
f, err = os.Open("test.txt")
if err != nil {
return err
}
io.Copy(os.Stdout, f)
if err := f.Close(); err != nil {
return err
}
// go to the /tmp directory
if err := os.Chdir(".."); err != nil {
return err
}
// cleanup, os.RemoveAll can be dangerous if you
// point at the wrong directory, use user input,
// and especially if you run as root
if err := os.RemoveAll("example_dir"); err != nil {
return err
}
return nil
}
- 创建一个名为
files.go
的文件,其中包含以下内容:
package filedirs
import (
"bytes"
"io"
"os"
"strings"
)
// Capitalizer opens a file, reads the contents,
// then writes those contents to a second file
func Capitalizer(f1 *os.File, f2 *os.File) error {
if _, err := f1.Seek(0, io.SeekStart); err != nil {
return err
}
var tmp = new(bytes.Buffer)
if _, err := io.Copy(tmp, f1); err != nil {
return err
}
s := strings.ToUpper(tmp.String())
if _, err := io.Copy(f2, strings.NewReader(s)); err !=
nil {
return err
}
return nil
}
// CapitalizerExample creates two files, writes to one
//then calls Capitalizer() on both
func CapitalizerExample() error {
f1, err := os.Create("file1.txt")
if err != nil {
return err
}
if _, err := f1.Write([]byte(`this file contains a
number of words and new lines`)); err != nil {
return err
}
f2, err := os.Create("file2.txt")
if err != nil {
return err
}
if err := Capitalizer(f1, f2); err != nil {
return err
}
if err := os.Remove("file1.txt"); err != nil {
return err
}
if err := os.Remove("file2.txt"); err != nil {
return err
}
return nil
}
-
创建一个名为
example
的新目录并进入该目录。 -
创建一个名为
main.go
的文件,其中包含以下内容:
package main
import "github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter1/filedirs"
func main() {
if err := filedirs.Operate(); err != nil {
panic(err)
}
if err := filedirs.CapitalizerExample(); err != nil {
panic(err)
}
}
-
运行
go run .
。 -
您还可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run .
hello
- 如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
,确保所有测试都通过。
工作原理...
如果您熟悉 Unix 中的文件,Go 的os
库应该会让您感到非常熟悉。您可以执行基本上所有常见的操作——Stat
文件以收集属性,使用不同权限收集文件,并创建和修改目录和文件。在本示例中,我们对目录和文件进行了许多操作,然后在完成后进行了清理。
与处理内存流非常相似,处理文件对象也提供了许多便利函数,例如Chown
、Stat
和Truncate
。熟悉文件的最简单方法是利用它们。在所有以前的示例中,我们都必须小心清理我们的程序。
在构建后端应用程序时,与文件的操作是非常常见的。文件可用于配置、秘钥、临时存储等。Go 使用os
包封装了操作系统系统调用,并允许相同的函数在使用 Windows 或 Unix 时运行。
一旦您的文件被打开并存储在File
结构中,它就可以轻松地传递到许多接口中(我们之前讨论过这些接口)。所有之前的示例都可以直接使用os.File
结构,而不是缓冲区和内存数据流,以便在磁盘上存储的数据上进行操作。这对于某些技术可能很有用,例如使用单个写入调用同时将所有日志写入stderr
和文件。
使用 CSV 格式
CSV 是一种常见的格式,用于操作数据。例如,将 CSV 文件导入或导出到 Excel 是常见的。Go CSV
包操作数据接口,因此很容易将数据写入缓冲区、stdout
、文件或套接字。本节中的示例将展示一些常见的将数据转换为 CSV 格式或从 CSV 格式中获取数据的方法。
如何做...
这些步骤涵盖了如何编写和运行您的应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter1/csvformat
的新目录。 -
进入此目录。
-
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter1/csvformat
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter1/csvformat
-
从
~/projects/go-programming-cookbook-original/chapter1/csvformat
复制测试,或者利用这个机会编写一些您自己的代码! -
创建一个名为
read_csv.go
的文件,其中包含以下内容:
package csvformat
import (
"bytes"
"encoding/csv"
"fmt"
"io"
"strconv"
)
// Movie will hold our parsed CSV
type Movie struct {
Title string
Director string
Year int
}
// ReadCSV gives shows some examples of processing CSV
// that is passed in as an io.Reader
func ReadCSV(b io.Reader) ([]Movie, error) {
r := csv.NewReader(b)
// These are some optional configuration options
r.Comma = ';'
r.Comment = '-'
var movies []Movie
// grab and ignore the header for now
// we may also want to use this for a dictionary key or
// some other form of lookup
_, err := r.Read()
if err != nil && err != io.EOF {
return nil, err
}
// loop until it's all processed
for {
record, err := r.Read()
if err == io.EOF {
break
} else if err != nil {
return nil, err
}
year, err := strconv.ParseInt(record[2], 10,
64)
if err != nil {
return nil, err
}
m := Movie{record[0], record[1], int(year)}
movies = append(movies, m)
}
return movies, nil
}
- 将此附加功能添加到
read_csv.go
中,如下所示:
// AddMoviesFromText uses the CSV parser with a string
func AddMoviesFromText() error {
// this is an example of us taking a string, converting
// it into a buffer, and reading it
// with the csv package
in := `
- first our headers
movie title;director;year released
- then some data
Guardians of the Galaxy Vol. 2;James Gunn;2017
Star Wars: Episode VIII;Rian Johnson;2017
`
b := bytes.NewBufferString(in)
m, err := ReadCSV(b)
if err != nil {
return err
}
fmt.Printf("%#vn", m)
return nil
}
- 创建一个名为
write_csv.go
的文件,其中包含以下内容:
package csvformat
import (
"bytes"
"encoding/csv"
"io"
"os"
)
// A Book has an Author and Title
type Book struct {
Author string
Title string
}
// Books is a named type for an array of books
type Books []Book
// ToCSV takes a set of Books and writes to an io.Writer
// it returns any errors
func (books *Books) ToCSV(w io.Writer) error {
n := csv.NewWriter(w)
err := n.Write([]string{"Author", "Title"})
if err != nil {
return err
}
for _, book := range *books {
err := n.Write([]string{book.Author,
book.Title})
if err != nil {
return err
}
}
n.Flush()
return n.Error()
}
- 将以下附加功能添加到
write_csv.go
中,如下所示:
// WriteCSVOutput initializes a set of books
// and writes the to os.Stdout
func WriteCSVOutput() error {
b := Books{
Book{
Author: "F Scott Fitzgerald",
Title: "The Great Gatsby",
},
Book{
Author: "J D Salinger",
Title: "The Catcher in the Rye",
},
}
return b.ToCSV(os.Stdout)
}
// WriteCSVBuffer returns a buffer csv for
// a set of books
func WriteCSVBuffer() (*bytes.Buffer, error) {
b := Books{
Book{
Author: "F Scott Fitzgerald",
Title: "The Great Gatsby",
},
Book{
Author: "J D Salinger",
Title: "The Catcher in the Rye",
},
}
w := &bytes.Buffer{}
err := b.ToCSV(w)
return w, err
}
-
创建一个名为
example
的新目录并进入该目录。 -
创建一个名为
main.go
的文件,其中包含以下内容:
package main
import (
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter1/csvformat"
)
func main() {
if err := csvformat.AddMoviesFromText(); err != nil {
panic(err)
}
if err := csvformat.WriteCSVOutput(); err != nil {
panic(err)
}
buffer, err := csvformat.WriteCSVBuffer()
if err != nil {
panic(err)
}
fmt.Println("Buffer = ", buffer.String())
}
-
运行
go run .
。 -
您还可以运行以下命令:
$ go build
$ ./example
您应该看到以下输出:
$ go run .
[]csvformat.Movie{csvformat.Movie{Title:"Guardians of the
Galaxy Vol. 2", Director:"James Gunn", Year:2017},
csvformat.Movie{Title:"Star Wars: Episode VIII", Director:"Rian
Johnson", Year:2017}}
Author,Title
F Scott Fitzgerald,The Great Gatsby
J D Salinger,The Catcher in the Rye
Buffer = Author,Title
F Scott Fitzgerald,The Great Gatsby
J D Salinger,The Catcher in the Rye
- 如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
,确保所有测试都通过。
工作原理...
为了学习如何读取 CSV 格式,我们首先将我们的数据表示为一个结构。在 Go 中,将数据格式化为结构非常有用,因为它使诸如编组和编码之类的事情相对简单。我们的读取示例使用电影作为我们的数据类型。该函数接受一个io.Reader
接口,该接口将我们的 CSV 数据作为输入。这可以是文件或缓冲区。然后,我们使用该数据来创建和填充一个Movie
结构,包括将年份转换为整数。我们还添加了选项到 CSV 解析器,以使用;
(分号)作为分隔符和-
(连字符)作为注释行。
接下来,我们以相同的思路进行探索,但是反过来。小说由标题和作者表示。我们初始化了一个小说数组,然后以 CSV 格式将特定的小说写入到io.Writer
接口中。这可以是文件、stdout
或缓冲区。
CSV
包是一个很好的例子,说明为什么您希望将 Go 中的数据流视为实现常见接口。通过小幅调整,我们可以轻松更改数据的来源和目的地,并且可以在不使用过多内存或时间的情况下轻松操作 CSV 数据。例如,可以可能一次从数据流中读取一条记录,并一次以修改后的格式将其写入到另一个流中。这样做不会带来显著的内存或处理器使用。
稍后,当我们探索数据管道和工作池时,您将看到这些想法如何结合以及如何并行处理这些流。
使用临时文件
到目前为止,我们已经为许多示例创建并使用了文件。我们还必须手动处理清理、名称冲突等问题。临时文件和目录是处理这些情况的一种更快、更简单的方法。
如何做...
以下步骤涵盖了如何编写和运行您的应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter1/tempfiles
的新目录。 -
导航到此目录。
-
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter1/tempfiles
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter1/tempfiles
-
从
~/projects/go-programming-cookbook-original/chapter1/tempfiles
复制测试,或者利用这个机会编写一些您自己的代码! -
创建一个名为
temp_files.go
的文件,内容如下:
package tempfiles
import (
"fmt"
"io/ioutil"
"os"
)
// WorkWithTemp will give some basic patterns for working
// with temporary files and directories
func WorkWithTemp() error {
// If you need a temporary place to store files with
// the same name ie. template1-10.html a temp directory
// is a good way to approach it, the first argument
// being blank means it will use create the directory
// in the location returned by
// os.TempDir()
t, err := ioutil.TempDir("", "tmp")
if err != nil {
return err
}
// This will delete everything inside the temp file
// when this function exits if you want to do this
// later, be sure to return the directory name to the
// calling function
defer os.RemoveAll(t)
// the directory must exist to create the tempfile
// created. t is an *os.File object.
tf, err := ioutil.TempFile(t, "tmp")
if err != nil {
return err
}
fmt.Println(tf.Name())
// normally we'd delete the temporary file here, but
// because we're placing it in a temp directory, it
// gets cleaned up by the earlier defer
return nil
}
-
创建一个名为
example
的新目录并导航到该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import "github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter1/tempfiles"
func main() {
if err := tempfiles.WorkWithTemp(); err != nil {
panic(err)
}
}
-
运行
go run .
。 -
您也可以运行以下命令:
$ go build $ ./example
您应该看到以下输出(路径不同):
$ go run .
/var/folders/kd/ygq5l_0d1xq1lzk_c7htft900000gn/T
/tmp764135258/tmp588787953
- 如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
,确保所有测试都通过。
工作原理...
可以使用ioutil
包来创建临时文件和目录。虽然您仍然必须自己删除文件,但使用RemoveAll
是惯例,它将为您执行此操作,只需额外一行代码。
在编写测试时,强烈建议使用临时文件。它还对构建产物等非常有用。Go 的ioutil
包将尝试默认遵守操作系统的偏好,但如果需要,它允许您回退到其他目录。
使用 text/template 和 html/template
Go 提供了丰富的模板支持。嵌套模板、导入函数、表示变量、迭代数据等都很简单。如果您需要比 CSV 写入器更复杂的东西,模板可能是一个很好的解决方案。
模板的另一个应用是用于网站。当我们想要将服务器端数据呈现给客户端时,模板非常合适。起初,Go 模板可能看起来令人困惑。本节将探讨使用模板、收集目录中的模板以及使用 HTML 模板。
如何做...
这些步骤涵盖了如何编写和运行您的应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter1/templates
的新目录。 -
导航到此目录。
-
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter1/templates
您应该会看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter1/templates
-
从
~/projects/go-programming-cookbook-original/chapter1/templates
复制测试,或者利用这个机会编写一些自己的代码! -
创建一个名为
templates.go
的文件,其中包含以下内容:
package templates
import (
"os"
"strings"
"text/template"
)
const sampleTemplate = `
This template demonstrates printing a {{ .Variable |
printf "%#v" }}.
{{if .Condition}}
If condition is set, we'll print this
{{else}}
Otherwise, we'll print this instead
{{end}}
Next we'll iterate over an array of strings:
{{range $index, $item := .Items}}
{{$index}}: {{$item}}
{{end}}
We can also easily import other functions like
strings.Split
then immediately used the array created as a result:
{{ range $index, $item := split .Words ","}}
{{$index}}: {{$item}}
{{end}}
Blocks are a way to embed templates into one another
{{ block "block_example" .}}
No Block defined!
{{end}}
{{/*
This is a way
to insert a multi-line comment
*/}}
`
const secondTemplate = `
{{ define "block_example" }}
{{.OtherVariable}}
{{end}}
`
- 在
templates.go
的末尾添加一个函数,如下所示:
// RunTemplate initializes a template and demonstrates a
// variety of template helper functions
func RunTemplate() error {
data := struct {
Condition bool
Variable string
Items []string
Words string
OtherVariable string
}{
Condition: true,
Variable: "variable",
Items: []string{"item1", "item2", "item3"},
Words:
"another_item1,another_item2,another_item3",
OtherVariable: "I'm defined in a second
template!",
}
funcmap := template.FuncMap{
"split": strings.Split,
}
// these can also be chained
t := template.New("example")
t = t.Funcs(funcmap)
// We could use Must instead to panic on error
// template.Must(t.Parse(sampleTemplate))
t, err := t.Parse(sampleTemplate)
if err != nil {
return err
}
// to demonstrate blocks we'll create another template
// by cloning the first template, then parsing a second
t2, err := t.Clone()
if err != nil {
return err
}
t2, err = t2.Parse(secondTemplate)
if err != nil {
return err
}
// write the template to stdout and populate it
// with data
err = t2.Execute(os.Stdout, &data)
if err != nil {
return err
}
return nil
}
- 创建一个名为
template_files.go
的文件,其中包含以下内容:
package templates
import (
"io/ioutil"
"os"
"path/filepath"
"text/template"
)
//CreateTemplate will create a template file that contains data
func CreateTemplate(path string, data string) error {
return ioutil.WriteFile(path, []byte(data),
os.FileMode(0755))
}
// InitTemplates sets up templates from a directory
func InitTemplates() error {
tempdir, err := ioutil.TempDir("", "temp")
if err != nil {
return err
}
defer os.RemoveAll(tempdir)
err = CreateTemplate(filepath.Join(tempdir, "t1.tmpl"),
`Template 1! {{ .Var1 }}
{{ block "template2" .}} {{end}}
{{ block "template3" .}} {{end}}
`)
if err != nil {
return err
}
err = CreateTemplate(filepath.Join(tempdir, "t2.tmpl"),
`{{ define "template2"}}Template 2! {{ .Var2 }}{{end}}
`)
if err != nil {
return err
}
err = CreateTemplate(filepath.Join(tempdir, "t3.tmpl"),
`{{ define "template3"}}Template 3! {{ .Var3 }}{{end}}
`)
if err != nil {
return err
}
pattern := filepath.Join(tempdir, "*.tmpl")
// Parse glob will combine all the files that match
// glob and combine them into a single template
tmpl, err := template.ParseGlob(pattern)
if err != nil {
return err
}
// Execute can also work with a map instead
// of a struct
tmpl.Execute(os.Stdout, map[string]string{
"Var1": "Var1!!",
"Var2": "Var2!!",
"Var3": "Var3!!",
})
return nil
}
- 创建一个名为
html_templates.go
的文件,其中包含以下内容:
package templates
import (
"fmt"
"html/template"
"os"
)
// HTMLDifferences highlights some of the differences
// between html/template and text/template
func HTMLDifferences() error {
t := template.New("html")
t, err := t.Parse("<h1>Hello! {{.Name}}</h1>n")
if err != nil {
return err
}
// html/template auto-escapes unsafe operations like
// javascript injection this is contextually aware and
// will behave differently
// depending on where a variable is rendered
err = t.Execute(os.Stdout, map[string]string{"Name": "
<script>alert('Can you see me?')</script>"})
if err != nil {
return err
}
// you can also manually call the escapers
fmt.Println(template.JSEscaper(`example
<example@example.com>`))
fmt.Println(template.HTMLEscaper(`example
<example@example.com>`))
fmt.Println(template.URLQueryEscaper(`example
<example@example.com>`))
return nil
}
-
创建一个名为
example
的新目录并进入其中。 -
创建一个名为
main.go
的文件,其中包含以下内容:
package main
import "github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter1/templates"
func main() {
if err := templates.RunTemplate(); err != nil {
panic(err)
}
if err := templates.InitTemplates(); err != nil {
panic(err)
}
if err := templates.HTMLDifferences(); err != nil {
panic(err)
}
}
-
运行
go run .
。 -
您也可以运行以下命令:
$ go build $ ./example
您应该会看到以下输出(路径不同):
- 如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
,确保所有测试都通过。
工作原理...
Go 有两个模板包:text/template
和html/template
。它们共享功能和各种函数。一般来说,您应该使用html/template
来渲染网站,而text/template
用于其他所有内容。模板是纯文本,但变量和函数可以在花括号块内使用。
模板包还提供了方便的方法来处理文件。我们在这里使用的示例在临时目录中创建了许多模板,然后用一行代码读取了它们。
html/template
包是text/template
包的包装器。所有的模板示例都直接使用html/template
包,不做任何修改,只改变导入语句。HTML 模板提供了上下文感知的安全性,这可以防止诸如 JavaScript 注入之类的安全漏洞。
模板包提供了现代模板库应有的功能。很容易组合模板,添加应用程序逻辑,并确保在将结果输出到 HTML 和 JavaScript 时的安全性。
第二章:命令行工具
命令行应用程序是处理用户输入和输出的最简单方式之一。本章将重点介绍基于命令行的交互,如命令行参数、配置和环境变量。最后,我们将介绍一个用于在 Unix 和 Bash for Windows 中着色文本输出的库。
通过本章的配方,您应该能够处理预期和意外的用户输入。捕获和处理信号配方是一个例子,说明用户可能向您的应用程序发送意外信号的情况,而管道配方是相对于标志或命令行参数来说获取用户输入的一个很好的替代方法。
ANSI 颜色配方有望提供一些清理输出给用户的示例。例如,在日志记录中,能够根据其用途着色文本有时可以使大块文本变得更清晰。
在本章中,我们将介绍以下配方:
-
使用命令行标志
-
使用命令行参数
-
读取和设置环境变量
-
使用 TOML、YAML 和 JSON 进行配置
-
使用 Unix 管道
-
捕获和处理信号
-
一个 ANSI 着色应用程序
技术要求
为了继续本章中的所有配方,请根据以下步骤配置您的环境:
-
在您的操作系统上下载并安装 Go 1.12.6 或更高版本,网址为
golang.org/doc/install
。 -
打开终端或控制台应用程序,并创建并导航到项目目录,例如
~/projects/go-programming-cookbook
。我们所有的代码都将在这个目录中运行和修改。 -
将最新的代码克隆到
~/projects/go-programming-cookbook-original
,并从该目录中工作,而不是手动输入示例:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
使用命令行标志
flag
包使得向 Go 应用程序添加命令行标志参数变得简单。它有一些缺点——您往往需要重复大量的代码来添加标志的简写版本,并且它们按照帮助提示的字母顺序排列。有许多第三方库试图解决这些缺点,但本章将重点介绍标准库版本,而不是这些库。
如何做...
这些步骤涵盖了编写和运行应用程序的过程:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter2/flags
的新目录。 -
导航到这个目录。
-
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter2/flags
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter2/flags
-
从
~/projects/go-programming-cookbook-original/chapter2/flags
复制测试,或者利用这个机会编写一些您自己的代码! -
创建一个名为
flags.go
的文件,内容如下:
package main
import (
"flag"
"fmt"
)
// Config will be the holder for our flags
type Config struct {
subject string
isAwesome bool
howAwesome int
countTheWays CountTheWays
}
// Setup initializes a config from flags that
// are passed in
func (c *Config) Setup() {
// you can set a flag directly like so:
// var someVar = flag.String("flag_name", "default_val",
// "description")
// but in practice putting it in a struct is generally
// better longhand
flag.StringVar(&c.subject, "subject", "", "subject is a
string, it defaults to empty")
// shorthand
flag.StringVar(&c.subject, "s", "", "subject is a string,
it defaults to empty (shorthand)")
flag.BoolVar(&c.isAwesome, "isawesome", false, "is it
awesome or what?")
flag.IntVar(&c.howAwesome, "howawesome", 10, "how awesome
out of 10?")
// custom variable type
flag.Var(&c.countTheWays, "c", "comma separated list of
integers")
}
// GetMessage uses all of the internal
// config vars and returns a sentence
func (c *Config) GetMessage() string {
msg := c.subject
if c.isAwesome {
msg += " is awesome"
} else {
msg += " is NOT awesome"
}
msg = fmt.Sprintf("%s with a certainty of %d out of 10\. Let
me count the ways %s", msg, c.howAwesome,
c.countTheWays.String())
return msg
}
- 创建一个名为
custom.go
的文件,内容如下:
package main
import (
"fmt"
"strconv"
"strings"
)
// CountTheWays is a custom type that
// we'll read a flag into
type CountTheWays []int
func (c *CountTheWays) String() string {
result := ""
for _, v := range *c {
if len(result) > 0 {
result += " ... "
}
result += fmt.Sprint(v)
}
return result
}
// Set will be used by the flag package
func (c *CountTheWays) Set(value string) error {
values := strings.Split(value, ",")
for _, v := range values {
i, err := strconv.Atoi(v)
if err != nil {
return err
}
*c = append(*c, i)
}
return nil
}
- 运行以下命令:
$ go mod tidy
- 创建一个名为
main.go
的文件,内容如下:
package main
import (
"flag"
"fmt"
)
func main() {
// initialize our setup
c := Config{}
c.Setup()
// generally call this from main
flag.Parse()
fmt.Println(c.GetMessage())
}
- 在命令行上运行以下命令:
$ go build $ ./flags -h
- 尝试这些和其他一些参数;您应该看到以下输出:
$ go build
$ ./flags -h
Usage of ./flags:
-c value
comma separated list of integers
-howawesome int
how awesome out of 10? (default 10)
-isawesome
is it awesome or what? (default false)
-s string
subject is a string, it defaults to empty (shorthand)
-subject string
subject is a string, it defaults to empty
$ ./flags -s Go -isawesome -howawesome 10 -c 1,2,3
Go is awesome with a certainty of 10 out of 10\. Let me count
the ways 1 ... 2 ... 3
- 如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
,确保所有测试都通过。
它是如何工作的...
该配方试图演示flag
包的大多数常见用法。它显示自定义变量类型、各种内置变量、简写标志,并将所有标志写入一个公共结构。这是第一个需要主函数的配方,因为应该从主函数中调用 flag 的主要用法(flag.Parse()
)。因此,正常的示例目录被省略了。
该应用程序的示例用法显示,您会自动得到-h
以获取包含的标志列表。还有一些需要注意的是,布尔标志是在没有参数的情况下调用的,而标志的顺序并不重要。
flag
包是一种快速构建命令行应用程序输入的方式,并提供了一种灵活的方式来指定用户输入,比如设置日志级别或应用程序的冗长程度。在使用命令行参数示例中,我们将探讨标志集并使用参数在它们之间切换。
使用命令行参数
上一个示例中的标志是一种命令行参数。本章将扩展这些参数的其他用途,通过构建支持嵌套子命令的命令来演示标志集,并使用传递给应用程序的位置参数。
与上一个示例一样,这个示例需要一个主函数来运行。有许多第三方包处理复杂的嵌套参数和标志,但我们将探讨如何仅使用标准库来实现这一点。
如何操作...
这些步骤涵盖了编写和运行应用程序的过程:
-
从你的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter2/cmdargs
的新目录。 -
导航到这个目录。
-
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter2/cmdargs
你应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter2/cmdargs
-
从
~/projects/go-programming-cookbook-original/chapter2/cmdargs
中复制测试,或者利用这个机会编写一些自己的代码! -
创建一个名为
cmdargs.go
的文件,内容如下:
package main
import (
"flag"
"fmt"
"os"
)
const version = "1.0.0"
const usage = `Usage:
%s [command]
Commands:
Greet
Version
`
const greetUsage = `Usage:
%s greet name [flag]
Positional Arguments:
name
the name to greet
Flags:
`
// MenuConf holds all the levels
// for a nested cmd line argument
type MenuConf struct {
Goodbye bool
}
// SetupMenu initializes the base flags
func (m *MenuConf) SetupMenu() *flag.FlagSet {
menu := flag.NewFlagSet("menu", flag.ExitOnError)
menu.Usage = func() {
fmt.Printf(usage, os.Args[0])
menu.PrintDefaults()
}
return menu
}
// GetSubMenu return a flag set for a submenu
func (m *MenuConf) GetSubMenu() *flag.FlagSet {
submenu := flag.NewFlagSet("submenu", flag.ExitOnError)
submenu.BoolVar(&m.Goodbye, "goodbye", false, "Say goodbye
instead of hello")
submenu.Usage = func() {
fmt.Printf(greetUsage, os.Args[0])
submenu.PrintDefaults()
}
return submenu
}
// Greet will be invoked by the greet command
func (m *MenuConf) Greet(name string) {
if m.Goodbye {
fmt.Println("Goodbye " + name + "!")
} else {
fmt.Println("Hello " + name + "!")
}
}
// Version prints the current version that is
// stored as a const
func (m *MenuConf) Version() {
fmt.Println("Version: " + version)
}
- 创建一个名为
main.go
的文件,内容如下:
package main
import (
"fmt"
"os"
"strings"
)
func main() {
c := MenuConf{}
menu := c.SetupMenu()
if err := menu.Parse(os.Args[1:]); err != nil {
fmt.Printf("Error parsing params %s, error: %v", os.Args[1:], err)
return
}
// we use arguments to switch between commands
// flags are also an argument
if len(os.Args) > 1 {
// we don't care about case
switch strings.ToLower(os.Args[1]) {
case "version":
c.Version()
case "greet":
f := c.GetSubMenu()
if len(os.Args) < 3 {
f.Usage()
return
}
if len(os.Args) > 3 {
if err := f.Parse(os.Args[3:]); err != nil {
fmt.Fprintf(os.Stderr, "Error parsing params %s, error: %v", os.Args[3:], err)
return
}
}
c.Greet(os.Args[2])
default:
fmt.Println("Invalid command")
menu.Usage()
return
}
} else {
menu.Usage()
return
}
}
-
运行
go build
。 -
运行以下命令,并尝试一些其他参数的组合:
$ ./cmdargs -h
Usage:
./cmdargs [command]
Commands:
Greet
Version
$./cmdargs version
Version: 1.0.0
$./cmdargs greet
Usage:
./cmdargs greet name [flag]
Positional Arguments:
name
the name to greet
Flags:
-goodbye
Say goodbye instead of hello
$./cmdargs greet reader
Hello reader!
$./cmdargs greet reader -goodbye
Goodbye reader!
- 如果你复制或编写了自己的测试,返回上一级目录并运行
go test
,确保所有测试都通过。
它是如何工作的...
标志集可用于设置独立的预期参数列表、使用字符串等。开发人员需要对许多参数进行验证,将正确的子集参数解析到命令中,并定义使用字符串。这可能容易出错,并需要大量迭代才能完全正确。
flag
包使解析参数变得更加容易,并包括方便的方法来获取标志的数量、参数等。这个示例演示了使用参数构建复杂命令行应用程序的基本方法,包括包级配置、必需的位置参数、多级命令使用,以及如何将这些内容拆分成多个文件或包(如果需要)。
读取和设置环境变量
环境变量是另一种可以将状态传递到应用程序中的方式,除了从文件中读取数据或通过命令行显式传递数据。这个示例将探讨一些非常基本的获取和设置环境变量的方法,然后使用非常有用的第三方库envconfig
(github.com/kelseyhightower/envconfig
)。
我们将构建一个应用程序,可以通过 JSON 或环境变量读取config
文件。下一个示例将探讨替代格式,包括 TOML 和 YAML。
如何操作...
这些步骤涵盖了编写和运行应用程序的过程:
-
从你的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter2/envvar
的新目录。 -
导航到这个目录。
-
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter2/envvar
你应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter2/envvar
-
复制
~/projects/go-programming-cookbook-original/chapter2/envvar
中的测试,或者利用这个机会编写一些自己的代码! -
创建一个名为
config.go
的文件,内容如下:
package envvar
import (
"encoding/json"
"os"
"github.com/kelseyhightower/envconfig"
"github.com/pkg/errors"
)
// LoadConfig will load files optionally from the json file
// stored at path, then will override those values based on the
// envconfig struct tags. The envPrefix is how we prefix our
// environment variables.
func LoadConfig(path, envPrefix string, config interface{})
error {
if path != "" {
err := LoadFile(path, config)
if err != nil {
return errors.Wrap(err, "error loading config from
file")
}
}
err := envconfig.Process(envPrefix, config)
return errors.Wrap(err, "error loading config from env")
}
// LoadFile unmarshalls a json file into a config struct
func LoadFile(path string, config interface{}) error {
configFile, err := os.Open(path)
if err != nil {
return errors.Wrap(err, "failed to read config file")
}
defer configFile.Close()
decoder := json.NewDecoder(configFile)
if err = decoder.Decode(config); err != nil {
return errors.Wrap(err, "failed to decode config file")
}
return nil
}
-
创建一个名为
example
的新目录,并导航到该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter2/envvar"
)
// Config will hold the config we
// capture from a json file and env vars
type Config struct {
Version string `json:"version" required:"true"`
IsSafe bool `json:"is_safe" default:"true"`
Secret string `json:"secret"`
}
func main() {
var err error
// create a temporary file to hold
// an example json file
tf, err := ioutil.TempFile("", "tmp")
if err != nil {
panic(err)
}
defer tf.Close()
defer os.Remove(tf.Name())
// create a json file to hold
// our secrets
secrets := `{
"secret": "so so secret"
}`
if _, err =
tf.Write(bytes.NewBufferString(secrets).Bytes());
err != nil {
panic(err)
}
// We can easily set environment variables
// as needed
if err = os.Setenv("EXAMPLE_VERSION", "1.0.0"); err != nil
{
panic(err)
}
if err = os.Setenv("EXAMPLE_ISSAFE", "false"); err != nil {
panic(err)
}
c := Config{}
if err = envvar.LoadConfig(tf.Name(), "EXAMPLE", &c);
err != nil {
panic(err)
}
fmt.Println("secrets file contains =", secrets)
// We can also read them
fmt.Println("EXAMPLE_VERSION =",
os.Getenv("EXAMPLE_VERSION"))
fmt.Println("EXAMPLE_ISSAFE =",
os.Getenv("EXAMPLE_ISSAFE"))
// The final config is a mix of json and environment
// variables
fmt.Printf("Final Config: %#v\n", c)
}
-
运行
go run main.go
。 -
你也可以运行以下命令:
go build ./example
- 你应该看到以下输出:
$ go run main.go
secrets file contains = {
"secret": "so so secret"
}
EXAMPLE_VERSION = 1.0.0
EXAMPLE_ISSAFE = false
Final Config: main.Config{Version:"1.0.0", IsSafe:false,
Secret:"so so secret"}
-
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶级示例目录中。 -
如果你复制或编写了自己的测试,返回上一级目录并运行
go test
,确保所有测试都通过。
它是如何工作的...
使用os
包读取和写入环境变量非常简单。这个配方使用的envconfig
第三方库是一种聪明的方式,可以捕获环境变量并使用struct
标签指定某些要求。
LoadConfig
函数是一种灵活的方式,可以从各种来源获取配置信息,而不需要太多的开销或太多额外的依赖。将主要的config
转换为除 JSON 以外的其他格式,或者始终使用环境变量也很简单。
还要注意错误的使用。我们在这个配方的代码中包装了错误,这样我们就可以注释错误而不会丢失原始错误的信息。在第四章中会有更多关于这个的细节,Go 中的错误处理。
使用 TOML、YAML 和 JSON 进行配置
Go 有许多配置格式,通过使用第三方库,支持。其中三种最流行的数据格式是 TOML、YAML 和 JSON。Go 可以直接支持 JSON,其他格式有关于如何为这些格式编组/解组或编码/解码数据的线索。这些格式除了配置之外还有许多好处,但本章主要关注将 Go 结构转换为配置结构的过程。这个配方将探讨使用这些格式进行基本输入和输出。
这些格式还提供了一个接口,通过这个接口,Go 和其他语言编写的应用程序可以共享相同的配置。还有许多处理这些格式并简化与它们一起工作的工具。
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从你的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter2/confformat
的新目录。 -
导航到这个目录。
-
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter2/confformat
你应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter2/confformat
-
从
~/projects/go-programming-cookbook-original/chapter2/confformat
复制测试,或者利用这个机会编写一些你自己的代码! -
创建一个名为
toml.go
的文件,内容如下:
package confformat
import (
"bytes"
"github.com/BurntSushi/toml"
)
// TOMLData is our common data struct
// with TOML struct tags
type TOMLData struct {
Name string `toml:"name"`
Age int `toml:"age"`
}
// ToTOML dumps the TOMLData struct to
// a TOML format bytes.Buffer
func (t *TOMLData) ToTOML() (*bytes.Buffer, error) {
b := &bytes.Buffer{}
encoder := toml.NewEncoder(b)
if err := encoder.Encode(t); err != nil {
return nil, err
}
return b, nil
}
// Decode will decode into TOMLData
func (t *TOMLData) Decode(data []byte) (toml.MetaData, error) {
return toml.Decode(string(data), t)
}
- 创建一个名为
yaml.go
的文件,内容如下:
package confformat
import (
"bytes"
"github.com/go-yaml/yaml"
)
// YAMLData is our common data struct
// with YAML struct tags
type YAMLData struct {
Name string `yaml:"name"`
Age int `yaml:"age"`
}
// ToYAML dumps the YAMLData struct to
// a YAML format bytes.Buffer
func (t *YAMLData) ToYAML() (*bytes.Buffer, error) {
d, err := yaml.Marshal(t)
if err != nil {
return nil, err
}
b := bytes.NewBuffer(d)
return b, nil
}
// Decode will decode into TOMLData
func (t *YAMLData) Decode(data []byte) error {
return yaml.Unmarshal(data, t)
}
- 创建一个名为
json.go
的文件,内容如下:
package confformat
import (
"bytes"
"encoding/json"
"fmt"
)
// JSONData is our common data struct
// with JSON struct tags
type JSONData struct {
Name string `json:"name"`
Age int `json:"age"`
}
// ToJSON dumps the JSONData struct to
// a JSON format bytes.Buffer
func (t *JSONData) ToJSON() (*bytes.Buffer, error) {
d, err := json.Marshal(t)
if err != nil {
return nil, err
}
b := bytes.NewBuffer(d)
return b, nil
}
// Decode will decode into JSONData
func (t *JSONData) Decode(data []byte) error {
return json.Unmarshal(data, t)
}
// OtherJSONExamples shows ways to use types
// beyond structs and other useful functions
func OtherJSONExamples() error {
res := make(map[string]string)
err := json.Unmarshal([]byte(`{"key": "value"}`), &res)
if err != nil {
return err
}
fmt.Println("We can unmarshal into a map instead of a
struct:", res)
b := bytes.NewReader([]byte(`{"key2": "value2"}`))
decoder := json.NewDecoder(b)
if err := decoder.Decode(&res); err != nil {
return err
}
fmt.Println("we can also use decoders/encoders to work with
streams:", res)
return nil
}
- 创建一个名为
marshal.go
的文件,内容如下:
package confformat
import "fmt"
// MarshalAll takes some data stored in structs
// and converts them to the various data formats
func MarshalAll() error {
t := TOMLData{
Name: "Name1",
Age: 20,
}
j := JSONData{
Name: "Name2",
Age: 30,
}
y := YAMLData{
Name: "Name3",
Age: 40,
}
tomlRes, err := t.ToTOML()
if err != nil {
return err
}
fmt.Println("TOML Marshal =", tomlRes.String())
jsonRes, err := j.ToJSON()
if err != nil {
return err
}
fmt.Println("JSON Marshal=", jsonRes.String())
yamlRes, err := y.ToYAML()
if err != nil {
return err
}
fmt.Println("YAML Marshal =", yamlRes.String())
return nil
}
- 创建一个名为
unmarshal.go
的文件,内容如下:
package confformat
import "fmt"
const (
exampleTOML = `name="Example1"
age=99
`
exampleJSON = `{"name":"Example2","age":98}`
exampleYAML = `name: Example3
age: 97
`
)
// UnmarshalAll takes data in various formats
// and converts them into structs
func UnmarshalAll() error {
t := TOMLData{}
j := JSONData{}
y := YAMLData{}
if _, err := t.Decode([]byte(exampleTOML)); err != nil {
return err
}
fmt.Println("TOML Unmarshal =", t)
if err := j.Decode([]byte(exampleJSON)); err != nil {
return err
}
fmt.Println("JSON Unmarshal =", j)
if err := y.Decode([]byte(exampleYAML)); err != nil {
return err
}
fmt.Println("Yaml Unmarshal =", y)
return nil
}
-
创建一个名为
example
的新目录并导航到该目录。 -
创建一个
main.go
文件,内容如下:
package main
import "github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter2/confformat"
func main() {
if err := confformat.MarshalAll(); err != nil {
panic(err)
}
if err := confformat.UnmarshalAll(); err != nil {
panic(err)
}
if err := confformat.OtherJSONExamples(); err != nil {
panic(err)
}
}
-
运行
go run main.go
。 -
你也可以运行以下命令:
$ go build $ ./example
- 你应该看到以下输出:
$ go run main.go
TOML Marshal = name = "Name1"
age = 20
JSON Marshal= {"name":"Name2","age":30}
YAML Marshal = name: Name3
age: 40
TOML Unmarshal = {Example1 99}
JSON Unmarshal = {Example2 98}
Yaml Unmarshal = {Example3 97}
We can unmarshal into a map instead of a struct: map[key:value]
we can also use decoders/encoders to work with streams:
map[key:value key2:value2]
-
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶级配方目录中。 -
如果你复制或编写了自己的测试,返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
这个配方为我们提供了如何使用 TOML、YAML 和 JSON 解析器的示例,用于将原始数据写入 go 结构并从中读取数据并转换为相应的格式。就像第一章中的配方,I/O 和文件系统,我们看到了在[]byte
、string
、bytes.Buffer
和其他 I/O 接口之间快速切换是多么常见。
encoding/json
包在提供编码、编组和其他方法以处理 JSON 格式方面是最全面的。我们通过ToFormat
函数将这些抽象出来,非常简单地可以附加多个类似的方法,这样我们就可以使用一个结构快速地转换成任何这些类型,或者从这些类型转换出来。
这个配方还涉及结构标签及其用法。上一章也使用了这些,它们是一种常见的方式,用于向包和库提供关于如何处理结构中包含的数据的提示。
使用 Unix 管道进行工作
当我们将一个程序的输出传递给另一个程序的输入时,Unix 管道非常有用。例如,看一下以下代码:
$ echo "test case" | wc -l
1
在 Go 应用程序中,管道的左侧可以使用os.Stdin
进行读取,它的作用类似于文件描述符。为了演示这一点,本教程将接受管道左侧的输入,并返回一个单词列表及其出现次数。这些单词将在空格上进行标记化。
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter2/pipes
的新目录。 -
导航到此目录。
-
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter2/pipes
您应该会看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter2/pipes
-
从
~/projects/go-programming-cookbook-original/chapter2/pipes
复制测试,或者利用这个机会编写一些自己的代码! -
创建一个名为
pipes.go
的文件,其中包含以下内容:
package main
import (
"bufio"
"fmt"
"io"
"os"
)
// WordCount takes a file and returns a map
// with each word as a key and it's number of
// appearances as a value
func WordCount(f io.Reader) map[string]int {
result := make(map[string]int)
// make a scanner to work on the file
// io.Reader interface
scanner := bufio.NewScanner(f)
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
result[scanner.Text()]++
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "reading input:", err)
}
return result
}
func main() {
fmt.Printf("string: number_of_occurrences\n\n")
for key, value := range WordCount(os.Stdin) {
fmt.Printf("%s: %d\n", key, value)
}
}
-
运行
echo "some string" | go run pipes.go
。 -
您还可以运行以下命令:
$ go build echo "some string" | ./pipes
您应该会看到以下输出:
$ echo "test case" | go run pipes.go
string: number_of_occurrences
test: 1
case: 1
$ echo "test case test" | go run pipes.go
string: number_of_occurrences
test: 2
case: 1
- 如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
在 Go 中使用管道非常简单,特别是如果您熟悉使用文件。例如,您可以使用第一章中的管道教程,I/O 和文件系统,创建一个tee应用程序([en.wikipedia.org/wiki/Tee_(command)
](https://en.wikipedia.org/wiki/Tee_(command))其中所有输入的内容都立即写入到stdout
和文件中。
本教程使用扫描程序来标记os.Stdin
文件对象的io.Reader
接口。您可以看到在完成所有读取后必须检查错误。
捕获和处理信号
信号是用户或操作系统终止正在运行的应用程序的有用方式。有时,以比默认行为更优雅的方式处理这些信号是有意义的。Go 提供了一种机制来捕获和处理信号。在本教程中,我们将通过使用处理 Go 例程的信号来探讨信号的处理。
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter2/signals
的新目录。 -
导航到此目录。
-
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter2/signals
您应该会看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter2/signals
-
从
~/projects/go-programming-cookbook-original/chapter2/signals
复制测试,或者利用这个机会编写一些自己的代码! -
创建一个名为
signals.go
的文件,其中包含以下内容:
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
// CatchSig sets up a listener for
// SIGINT interrupts
func CatchSig(ch chan os.Signal, done chan bool) {
// block on waiting for a signal
sig := <-ch
// print it when it's received
fmt.Println("nsig received:", sig)
// we can set up handlers for all types of
// sigs here
switch sig {
case syscall.SIGINT:
fmt.Println("handling a SIGINT now!")
case syscall.SIGTERM:
fmt.Println("handling a SIGTERM in an entirely
different way!")
default:
fmt.Println("unexpected signal received")
}
// terminate
done <- true
}
func main() {
// initialize our channels
signals := make(chan os.Signal)
done := make(chan bool)
// hook them up to the signals lib
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
// if a signal is caught by this go routine
// it will write to done
go CatchSig(signals, done)
fmt.Println("Press ctrl-c to terminate...")
// the program blocks until someone writes to done
<-done
fmt.Println("Done!")
}
- 运行以下命令:
$ go build $ ./signals
- 尝试运行代码,然后按Ctrl + C。您应该会看到以下内容:
$./signals
Press ctrl-c to terminate...
^C
sig received: interrupt
handling a SIGINT now!
Done!
- 尝试再次运行它。然后,从另一个终端确定 PID 并终止应用程序:
$./signals
Press ctrl-c to terminate...
# in a separate terminal
$ ps -ef | grep signals
501 30777 26360 0 5:00PM ttys000 0:00.00 ./signals
$ kill -SIGTERM 30777
# in the original terminal
sig received: terminated
handling a SIGTERM in an entirely different way!
Done!
- 如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
本教程使用了通道,这在第九章“并行和并发”中有更详细的介绍。signal.Notify
函数需要一个通道来发送信号通知,还需要我们关心的信号类型。然后,我们在 Go 例程中设置一个函数来处理我们传递给该函数的通道上的任何活动。一旦我们收到信号,我们可以以任何我们想要的方式处理它。我们可以终止应用程序,回复消息,并对不同的信号有不同的行为。kill
命令是测试向应用程序传递信号的好方法。
我们还使用一个 done
通道来阻止应用程序在接收到信号之前终止。否则,程序会立即终止。对于长时间运行的应用程序(如 Web 应用程序),这是不必要的。创建适当的信号处理例程来执行清理工作可能非常有用,特别是在具有大量 Go 协程并持有大量状态的应用程序中。一个优雅关闭的实际例子可能是允许当前处理程序完成其 HTTP 请求而不会在中途终止它们。
一个 ANSI 着色应用程序
对 ANSI 终端应用程序进行着色是通过一系列代码来处理的,在你想要着色的文本之前和之后。本教程将探讨一种基本的着色机制,可以将文本着色为红色或普通色。要了解完整的应用程序,请查看 github.com/agtorre/gocolorize
,它支持更多的颜色和文本类型,并且还实现了 fmt.Formatter
接口以便于打印。
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter2/ansicolor
的新目录。 -
导航到这个目录。
-
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter2/ansicolor
您应该会看到一个名为 go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter2/ansicolor
-
从
~/projects/go-programming-cookbook-original/chapter2/ansicolor
复制测试,或者利用这个机会编写一些您自己的代码! -
创建一个名为
color.go
的文件,其中包含以下内容:
package ansicolor
import "fmt"
//Color of text
type Color int
const (
// ColorNone is default
ColorNone = iota
// Red colored text
Red
// Green colored text
Green
// Yellow colored text
Yellow
// Blue colored text
Blue
// Magenta colored text
Magenta
// Cyan colored text
Cyan
// White colored text
White
// Black colored text
Black Color = -1
)
// ColorText holds a string and its color
type ColorText struct {
TextColor Color
Text string
}
func (r *ColorText) String() string {
if r.TextColor == ColorNone {
return r.Text
}
value := 30
if r.TextColor != Black {
value += int(r.TextColor)
}
return fmt.Sprintf("33[0;%dm%s33[0m", value, r.Text)
}
-
创建一个名为
example
的新目录并导航到它。 -
创建一个名为
main.go
的文件,其中包含以下内容:
package main
import (
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter2/ansicolor"
)
func main() {
r := ansicolor.ColorText{
TextColor: ansicolor.Red,
Text: "I'm red!",
}
fmt.Println(r.String())
r.TextColor = ansicolor.Green
r.Text = "Now I'm green!"
fmt.Println(r.String())
r.TextColor = ansicolor.ColorNone
r.Text = "Back to normal..."
fmt.Println(r.String())
}
-
运行
go run main.go
。 -
您也可以运行以下命令:
$ go build $ ./example
- 如果您的终端支持 ANSI 着色格式,您应该会看到以下输出的文本被着色:
$ go run main.go
I'm red!
Now I'm green!
Back to normal...
- 如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
该应用程序利用一个结构来维护着色文本的状态。在这种情况下,它存储文本的颜色和值。当您调用 String()
方法时,最终的字符串将被渲染,根据结构中存储的值,它将返回着色文本或普通文本。默认情况下,文本将是普通的。
第三章:数据转换和组合
理解 Go 的类型系统是掌握 Go 开发各个层面的关键步骤。本章将展示一些转换数据类型、处理非常大的数字、处理货币、使用不同类型的编码和解码(包括 Base64 和gob
),以及使用闭包创建自定义集合的示例。在本章中,将介绍以下配方:
-
转换数据类型和接口转换
-
使用 math 和 math/big 处理数值数据类型
-
货币转换和 float64 考虑
-
使用指针和 SQL NullTypes 进行编码和解码
-
编码和解码 Go 数据
-
在 Go 中使用结构标签和基本反射
-
使用闭包实现集合
技术要求
为了继续本章中的所有配方,请根据以下步骤配置您的环境:
-
在您的操作系统上下载并安装 Go 1.12.6 或更高版本,网址为
golang.org/doc/install
。 -
打开一个终端/控制台应用程序,并创建并导航到一个项目目录,例如
~/projects/go-programming-cookbook
。所有的代码都将从这个目录运行和修改。 -
将最新的代码克隆到
~/projects/go-programming-cookbook-original
。如果愿意,您可以从该目录中工作,而不必手动输入示例:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
转换数据类型和接口转换
通常情况下,Go 在将数据从一种类型转换为另一种类型时非常灵活。一种类型可以继承另一种类型,如下所示:
type A int
我们总是可以将类型强制转换回我们继承的类型,如下所示:
var a A = 1
fmt.Println(int(a))
还有一些方便的函数,可以使用类型转换进行数字之间的转换,使用fmt.Sprint
和strconv
进行字符串和其他类型之间的转换,使用反射进行接口和类型之间的转换。本配方将探讨一些基本的转换,这些转换将贯穿本书使用。
如何做...
以下步骤涵盖了如何编写和运行您的应用程序:
-
从您的终端/控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter3/dataconv
的新目录。 -
导航到这个目录。
-
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter3/dataconv
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter3/dataconv
-
从
~/projects/go-programming-cookbook-original/chapter3/dataconv
复制测试,或者将其用作编写自己代码的练习! -
创建一个名为
dataconv.go
的文件,内容如下:
package dataconv
import "fmt"
// ShowConv demonstrates some type conversion
func ShowConv() {
// int
var a = 24
// float 64
var b = 2.0
// convert the int to a float64 for this calculation
c := float64(a) * b
fmt.Println(c)
// fmt.Sprintf is a good way to convert to strings
precision := fmt.Sprintf("%.2f", b)
// print the value and the type
fmt.Printf("%s - %T\n", precision, precision)
}
- 创建一个名为
strconv.go
的文件,内容如下:
package dataconv
import (
"fmt"
"strconv"
)
// Strconv demonstrates some strconv
// functions
func Strconv() error {
//strconv is a good way to convert to and from strings
s := "1234"
// we can specify the base (10) and precision
// 64 bit
res, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return err
}
fmt.Println(res)
// lets try hex
res, err = strconv.ParseInt("FF", 16, 64)
if err != nil {
return err
}
fmt.Println(res)
// we can do other useful things like:
val, err := strconv.ParseBool("true")
if err != nil {
return err
}
fmt.Println(val)
return nil
}
- 创建一个名为
interfaces.go
的文件,内容如下:
package dataconv
import "fmt"
// CheckType will print based on the
// interface type
func CheckType(s interface{}) {
switch s.(type) {
case string:
fmt.Println("It's a string!")
case int:
fmt.Println("It's an int!")
default:
fmt.Println("not sure what it is...")
}
}
// Interfaces demonstrates casting
// from anonymous interfaces to types
func Interfaces() {
CheckType("test")
CheckType(1)
CheckType(false)
var i interface{}
i = "test"
// manually check an interface
if val, ok := i.(string); ok {
fmt.Println("val is", val)
}
// this one should fail
if _, ok := i.(int); !ok {
fmt.Println("uh oh! glad we handled this")
}
}
-
创建一个名为
example
的新目录,并导航到该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import "github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter3/dataconv"
func main() {
dataconv.ShowConv()
if err := dataconv.Strconv(); err != nil {
panic(err)
}
dataconv.Interfaces()
}
- 运行
go run main.go
。您也可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
48
2.00 - string
1234
255
true
It's a string!
It's an int!
not sure what it is...
val is test
uh oh! glad we handled this
- 如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
本配方演示了如何通过使用strconv
包和接口反射将类型包装在新类型中来进行类型转换。这些方法允许 Go 开发人员快速在各种抽象的 Go 类型之间进行转换。前两种方法在编译期间将返回错误,但接口反射中的错误可能直到运行时才会被发现。如果您错误地反射到一个不受支持的类型,您的程序将会崩溃。在不同类型之间切换是一种泛化的方式,本配方也进行了演示。
转换对于诸如math
这样专门操作float64
的包非常重要。
使用 math 和 math/big 处理数值数据类型
math
和math/big
包专注于向 Go 语言公开更复杂的数学运算,如Pow
、Sqrt
和Cos
。math
包本身主要操作float64
,除非函数另有说明。math/big
包用于无法用 64 位值表示的数字。这个配方将展示math
包的一些基本用法,并演示如何使用math/big
来进行斐波那契数列。
如何做...
以下步骤涵盖了如何编写和运行您的应用程序:
-
从您的终端/控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter3/math
的新目录。 -
导航到这个目录。
-
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter3/math
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter3/math
-
从
~/projects/go-programming-cookbook-original/chapter3/math
复制测试,或者将其用作编写自己代码的练习! -
创建一个名为
fib.go
的文件,内容如下:
package math
import "math/big"
// global to memoize fib
var memoize map[int]*big.Int
func init() {
// initialize the map
memoize = make(map[int]*big.Int)
}
// Fib prints the nth digit of the fibonacci sequence
// it will return 1 for anything < 0 as well...
// it's calculated recursively and use big.Int since
// int64 will quickly overflow
func Fib(n int) *big.Int {
if n < 0 {
return big.NewInt(1)
}
// base case
if n < 2 {
memoize[n] = big.NewInt(1)
}
// check if we stored it before
// if so return with no calculation
if val, ok := memoize[n]; ok {
return val
}
// initialize map then add previous 2 fib values
memoize[n] = big.NewInt(0)
memoize[n].Add(memoize[n], Fib(n-1))
memoize[n].Add(memoize[n], Fib(n-2))
// return result
return memoize[n]
}
- 创建一个名为
math.go
的文件,内容如下:
package math
import (
"fmt"
"math"
)
// Examples demonstrates some of the functions
// in the math package
func Examples() {
//sqrt Examples
i := 25
// i is an int, so convert
result := math.Sqrt(float64(i))
// sqrt of 25 == 5
fmt.Println(result)
// ceil rounds up
result = math.Ceil(9.5)
fmt.Println(result)
// floor rounds down
result = math.Floor(9.5)
fmt.Println(result)
// math also stores some consts:
fmt.Println("Pi:", math.Pi, "E:", math.E)
}
-
创建一个名为
example
的新目录,并导航到该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter3/math"
)
func main() {
math.Examples()
for i := 0; i < 10; i++ {
fmt.Printf("%v ", math.Fib(i))
}
fmt.Println()
}
- 运行
go run main.go
。您也可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
5
10
9
Pi: 3.141592653589793 E: 2.718281828459045
1 1 2 3 5 8 13 21 34 55
- 如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
math
包使得在 Go 语言中执行复杂的数学运算成为可能。这个配方应该与这个包一起使用,用于执行复杂的浮点运算并根据需要在各种类型之间进行转换。值得注意的是,即使使用float64
,对于某些浮点数仍可能存在舍入误差;以下配方演示了一些处理这种情况的技巧。
math/big
部分展示了一个递归的斐波那契数列。如果您修改main.go
以循环超过 10 次,如果使用big.Int
而不是int64
,您将很快溢出。big.Int
包还有一些辅助方法,可以将大类型转换为其他类型。
货币转换和 float64 注意事项
处理货币始终是一个棘手的过程。将货币表示为float64
可能很诱人,但在进行计算时可能会导致一些非常棘手(和错误的)舍入错误。因此,最好将货币以美分的形式存储为int64
实例。
当从表单、命令行或其他来源收集用户输入时,货币通常以美元形式表示。因此,最好将其视为字符串,并直接将该字符串转换为美分,而不进行浮点转换。这个配方将介绍将货币的字符串表示转换为int64
(美分)实例的方法,并再次转换回去。
如何做...
以下步骤涵盖了如何编写和运行您的应用程序:
-
从您的终端/控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter3/currency
的新目录。 -
导航到这个目录。
-
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter3/currency
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter3/currency
-
从
~/projects/go-programming-cookbook-original/chapter3/currency
复制测试,或者将其用作编写自己代码的练习! -
创建一个名为
dollars.go
的文件,内容如下:
package currency
import (
"errors"
"strconv"
"strings"
)
// ConvertStringDollarsToPennies takes a dollar amount
// as a string, i.e. 1.00, 55.12 etc and converts it
// into an int64
func ConvertStringDollarsToPennies(amount string) (int64,
error) {
// check if amount can convert to a valid float
_, err := strconv.ParseFloat(amount, 64)
if err != nil {
return 0, err
}
// split the value on "."
groups := strings.Split(amount, ".")
// if there is no . result will still be
// captured here
result := groups[0]
// base string
r := ""
// handle the data after the "."
if len(groups) == 2 {
if len(groups[1]) != 2 {
return 0, errors.New("invalid cents")
}
r = groups[1]
}
// pad with 0, this will be
// 2 0's if there was no .
for len(r) < 2 {
r += "0"
}
result += r
// convert it to an int
return strconv.ParseInt(result, 10, 64)
}
- 创建一个名为
pennies.go
的文件,内容如下:
package currency
import (
"strconv"
)
// ConvertPenniesToDollarString takes a penny amount as
// an int64 and returns a dollar string representation
func ConvertPenniesToDollarString(amount int64) string {
// parse the pennies as a base 10 int
result := strconv.FormatInt(amount, 10)
// check if negative, will set it back later
negative := false
if result[0] == '-' {
result = result[1:]
negative = true
}
// left pad with 0 if we're passed in value < 100
for len(result) < 3 {
result = "0" + result
}
length := len(result)
// add in the decimal
result = result[0:length-2] + "." + result[length-2:]
// from the negative we stored earlier!
if negative {
result = "-" + result
}
return result
}
-
创建一个名为
example
的新目录,并导航到该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter3/currency"
)
func main() {
// start with our user input
// of fifteen dollars and 93 cents
userInput := "15.93"
pennies, err :=
currency.ConvertStringDollarsToPennies(userInput)
if err != nil {
panic(err)
}
fmt.Printf("User input converted to %d pennies\n", pennies)
// adding 15 cents
pennies += 15
dollars := currency.ConvertPenniesToDollarString(pennies)
fmt.Printf("Added 15 cents, new values is %s dollars\n",
dollars)
}
- 运行
go run main.go
。您也可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
User input converted to 1593 pennies
Added 15 cents, new values is 16.08 dollars
- 如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
这个配方利用strconv
和strings
包将货币在字符串格式的美元和int64
的便士之间进行转换。它可以在不转换为float64
类型的情况下进行,这可能会导致舍入误差,并且仅在验证时才这样做。
strconv.ParseInt
和strconv.FormatInt
函数非常有用,用于将int64
和字符串相互转换。我们还利用了 Go 字符串可以根据需要轻松添加和切片的特点。
使用指针和 SQL NullTypes 进行编码和解码
在 Go 中对对象进行编码或解码时,未明确设置的类型将被设置为它们的默认值。例如,字符串将默认为空字符串(""
),整数将默认为0
。通常情况下,这是可以的,除非0
对于您的 API 或服务来说有特殊含义。
此外,如果您使用struct
标签,比如json omitempty
,即使它们是有效的,0
值也会被忽略。另一个例子是从 SQL 返回的Null
。什么值最能代表Int
的Null
?这个示例将探讨 Go 开发人员处理这个问题的一些方法。
如何做...
以下步骤涵盖了如何编写和运行您的应用程序:
-
从您的终端/控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter3/nulls
的新目录。 -
进入这个目录。
-
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter3/nulls
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter3/nulls
-
从
~/projects/go-programming-cookbook-original/chapter3/nulls
复制测试,或者使用这个练习来编写一些您自己的代码! -
创建一个名为
base.go
的文件,内容如下:
package nulls
import (
"encoding/json"
"fmt"
)
// json that has name but not age
const (
jsonBlob = `{"name": "Aaron"}`
fulljsonBlob = `{"name":"Aaron", "age":0}`
)
// Example is a basic struct with age
// and name fields
type Example struct {
Age int `json:"age,omitempty"`
Name string `json:"name"`
}
// BaseEncoding shows encoding and
// decoding with normal types
func BaseEncoding() error {
e := Example{}
// note that no age = 0 age
if err := json.Unmarshal([]byte(jsonBlob), &e); err != nil
{
return err
}
fmt.Printf("Regular Unmarshal, no age: %+v\n", e)
value, err := json.Marshal(&e)
if err != nil {
return err
}
fmt.Println("Regular Marshal, with no age:", string(value))
if err := json.Unmarshal([]byte(fulljsonBlob), &e);
err != nil {
return err
}
fmt.Printf("Regular Unmarshal, with age = 0: %+v\n", e)
value, err = json.Marshal(&e)
if err != nil {
return err
}
fmt.Println("Regular Marshal, with age = 0:",
string(value))
return nil
}
- 创建一个名为
pointer.go
的文件,内容如下:
package nulls
import (
"encoding/json"
"fmt"
)
// ExamplePointer is the same, but
// uses a *Int
type ExamplePointer struct {
Age *int `json:"age,omitempty"`
Name string `json:"name"`
}
// PointerEncoding shows methods for
// dealing with nil/omitted values
func PointerEncoding() error {
// note that no age = nil age
e := ExamplePointer{}
if err := json.Unmarshal([]byte(jsonBlob), &e); err != nil
{
return err
}
fmt.Printf("Pointer Unmarshal, no age: %+v\n", e)
value, err := json.Marshal(&e)
if err != nil {
return err
}
fmt.Println("Pointer Marshal, with no age:", string(value))
if err := json.Unmarshal([]byte(fulljsonBlob), &e);
err != nil {
return err
}
fmt.Printf("Pointer Unmarshal, with age = 0: %+v\n", e)
value, err = json.Marshal(&e)
if err != nil {
return err
}
fmt.Println("Pointer Marshal, with age = 0:",
string(value))
return nil
}
- 创建一个名为
nullencoding.go
的文件,内容如下:
package nulls
import (
"database/sql"
"encoding/json"
"fmt"
)
type nullInt64 sql.NullInt64
// ExampleNullInt is the same, but
// uses a sql.NullInt64
type ExampleNullInt struct {
Age *nullInt64 `json:"age,omitempty"`
Name string `json:"name"`
}
func (v *nullInt64) MarshalJSON() ([]byte, error) {
if v.Valid {
return json.Marshal(v.Int64)
}
return json.Marshal(nil)
}
func (v *nullInt64) UnmarshalJSON(b []byte) error {
v.Valid = false
if b != nil {
v.Valid = true
return json.Unmarshal(b, &v.Int64)
}
return nil
}
// NullEncoding shows an alternative method
// for dealing with nil/omitted values
func NullEncoding() error {
e := ExampleNullInt{}
// note that no means an invalid value
if err := json.Unmarshal([]byte(jsonBlob), &e); err != nil
{
return err
}
fmt.Printf("nullInt64 Unmarshal, no age: %+v\n", e)
value, err := json.Marshal(&e)
if err != nil {
return err
}
fmt.Println("nullInt64 Marshal, with no age:",
string(value))
if err := json.Unmarshal([]byte(fulljsonBlob), &e);
err != nil {
return err
}
fmt.Printf("nullInt64 Unmarshal, with age = 0: %+v\n", e)
value, err = json.Marshal(&e)
if err != nil {
return err
}
fmt.Println("nullInt64 Marshal, with age = 0:",
string(value))
return nil
}
-
创建一个名为
example
的新目录,并进入该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter3/nulls"
)
func main() {
if err := nulls.BaseEncoding(); err != nil {
panic(err)
}
fmt.Println()
if err := nulls.PointerEncoding(); err != nil {
panic(err)
}
fmt.Println()
if err := nulls.NullEncoding(); err != nil {
panic(err)
}
}
- 运行
go run main.go
。您也可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
Regular Unmarshal, no age: {Age:0 Name:Aaron}
Regular Marshal, with no age: {"name":"Aaron"}
Regular Unmarshal, with age = 0: {Age:0 Name:Aaron}
Regular Marshal, with age = 0: {"name":"Aaron"}
Pointer Unmarshal, no age: {Age:<nil> Name:Aaron}
Pointer Marshal, with no age: {"name":"Aaron"}
Pointer Unmarshal, with age = 0: {Age:0xc42000a610 Name:Aaron}
Pointer Marshal, with age = 0: {"age":0,"name":"Aaron"}
nullInt64 Unmarshal, no age: {Age:<nil> Name:Aaron}
nullInt64 Marshal, with no age: {"name":"Aaron"}
nullInt64 Unmarshal, with age = 0: {Age:0xc42000a750
Name:Aaron}
nullInt64 Marshal, with age = 0: {"age":0,"name":"Aaron"}
- 如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
从值切换到指针是在编组和解组时表达空值的一种快速方式。设置这些值可能有点棘手,因为您不能直接将它们分配给指针,-- *a := 1
,但是,这是一种灵活的处理方法。
这个示例还演示了使用sql.NullInt64
类型的替代方法。这通常用于 SQL,如果返回的不是Null
,则valid
会被设置;否则,它会设置Null
。我们添加了一个MarshalJSON
方法和一个UnmarshallJSON
方法,以允许这种类型与JSON
包进行交互,并且我们选择使用指针,以便omitempty
可以继续按预期工作。
编码和解码 Go 数据
Go 提供了许多除了 JSON、TOML 和 YAML 之外的替代编码类型。这些主要用于在 Go 进程之间传输数据,比如使用线协议和 RPC,或者在某些字符格式受限的情况下。
这个示例将探讨如何编码和解码gob
格式和base64
。后面的章节将探讨诸如 GRPC 之类的协议。
如何做...
以下步骤涵盖了如何编写和运行您的应用程序:
-
从您的终端/控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter3/encoding
的新目录。 -
进入这个目录。
-
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter3/encoding
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter3/encoding
-
从
~/projects/go-programming-cookbook-original/chapter3/encoding
复制测试,或者使用这个练习来编写一些您自己的代码! -
创建一个名为
gob.go
的文件,内容如下:
package encoding
import (
"bytes"
"encoding/gob"
"fmt"
)
// pos stores the x, y position
// for Object
type pos struct {
X int
Y int
Object string
}
// GobExample demonstrates using
// the gob package
func GobExample() error {
buffer := bytes.Buffer{}
p := pos{
X: 10,
Y: 15,
Object: "wrench",
}
// note that if p was an interface
// we'd have to call gob.Register first
e := gob.NewEncoder(&buffer)
if err := e.Encode(&p); err != nil {
return err
}
// note this is a binary format so it wont print well
fmt.Println("Gob Encoded valued length: ",
len(buffer.Bytes()))
p2 := pos{}
d := gob.NewDecoder(&buffer)
if err := d.Decode(&p2); err != nil {
return err
}
fmt.Println("Gob Decode value: ", p2)
return nil
}
- 创建一个名为
base64.go
的文件,内容如下:
package encoding
import (
"bytes"
"encoding/base64"
"fmt"
"io/ioutil"
)
// Base64Example demonstrates using
// the base64 package
func Base64Example() error {
// base64 is useful for cases where
// you can't support binary formats
// it operates on bytes/strings
// using helper functions and URL encoding
value := base64.URLEncoding.EncodeToString([]byte("encoding
some data!"))
fmt.Println("With EncodeToString and URLEncoding: ", value)
// decode the first value
decoded, err := base64.URLEncoding.DecodeString(value)
if err != nil {
return err
}
fmt.Println("With DecodeToString and URLEncoding: ",
string(decoded))
return nil
}
// Base64ExampleEncoder shows similar examples
// with encoders/decoders
func Base64ExampleEncoder() error {
// using encoder/ decoder
buffer := bytes.Buffer{}
// encode into the buffer
encoder := base64.NewEncoder(base64.StdEncoding, &buffer)
if _, err := encoder.Write([]byte("encoding some other
data")); err != nil {
return err
}
// be sure to close
if err := encoder.Close(); err != nil {
return err
}
fmt.Println("Using encoder and StdEncoding: ",
buffer.String())
decoder := base64.NewDecoder(base64.StdEncoding, &buffer)
results, err := ioutil.ReadAll(decoder)
if err != nil {
return err
}
fmt.Println("Using decoder and StdEncoding: ",
string(results))
return nil
}
-
创建一个名为
example
的新目录,并进入该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter3/encoding"
)
func main() {
if err := encoding.Base64Example(); err != nil {
panic(err)
}
if err := encoding.Base64ExampleEncoder(); err != nil {
panic(err)
}
if err := encoding.GobExample(); err != nil {
panic(err)
}
}
- 运行
go run main.go
。您也可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
With EncodeToString and URLEncoding:
ZW5jb2Rpbmcgc29tZSBkYXRhIQ==
With DecodeToString and URLEncoding: encoding some data!
Using encoder and StdEncoding: ZW5jb2Rpbmcgc29tZSBvdGhlciBkYXRh
Using decoder and StdEncoding: encoding some other data
Gob Encoded valued length: 57
Gob Decode value: {10 15 wrench}
- 如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
Gob 编码是一种以 Go 数据类型为基础构建的流格式。当发送和编码许多连续的项目时,它是最有效的。对于单个项目,其他编码格式,如 JSON,可能更有效和更便携。尽管如此,gob
编码使得将大型、复杂的结构编组并在另一个进程中重建它们变得简单。尽管这里没有展示,gob
也可以在自定义类型或具有自定义MarshalBinary
和UnmarshalBinary
方法的非导出类型上操作。
Base64 编码对于通过 URL 在GET
请求中进行通信或生成二进制数据的字符串表示编码非常有用。大多数语言都可以支持这种格式,并在另一端解组数据。因此,在不支持 JSON 格式的情况下,通常会对诸如 JSON 有效负载之类的东西进行编码。
Go 中的结构标签和基本反射
反射是一个复杂的主题,无法在一篇文章中完全涵盖;然而,反射的一个实际应用是处理结构标签。在本质上,struct
标签只是键-值字符串:你查找键,然后处理值。正如你所想象的那样,对于诸如 JSON 编组和解组这样的事情,处理这些值有很多复杂性。
reflect
包旨在审查和理解接口对象。它有助手方法来查看不同种类的结构、值、struct
标签等。如果你需要超出基本接口转换的东西,比如本章开头的内容,这就是你应该查看的包。
如何做...
以下步骤涵盖了如何编写和运行你的应用程序:
-
从你的终端/控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter3/tags
的新目录。 -
进入这个目录。
-
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter3/tags
你应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter3/tags
-
从
~/projects/go-programming-cookbook-original/chapter3/tags
复制测试,或者利用这个机会编写一些你自己的代码! -
创建一个名为
serialize.go
的文件,内容如下:
package tags
import "reflect"
// SerializeStructStrings converts a struct
// to our custom serialization format
// it honors serialize struct tags for string types
func SerializeStructStrings(s interface{}) (string, error) {
result := ""
// reflect the interface into
// a type
r := reflect.TypeOf(s)
value := reflect.ValueOf(s)
// if a pointer to a struct is passed
// in, handle it appropriately
if r.Kind() == reflect.Ptr {
r = r.Elem()
value = value.Elem()
}
// loop over all of the fields
for i := 0; i < r.NumField(); i++ {
field := r.Field(i)
// struct tag found
key := field.Name
if serialize, ok := field.Tag.Lookup("serialize"); ok {
// ignore "-" otherwise that whole value
// becomes the serialize 'key'
if serialize == "-" {
continue
}
key = serialize
}
switch value.Field(i).Kind() {
// this recipe only supports strings!
case reflect.String:
result += key + ":" + value.Field(i).String() + ";"
// by default skip it
default:
continue
}
}
return result, nil
}
- 创建一个名为
deserialize.go
的文件,内容如下:
package tags
import (
"errors"
"reflect"
"strings"
)
// DeSerializeStructStrings converts a serialized
// string using our custom serialization format
// to a struct
func DeSerializeStructStrings(s string, res interface{}) error
{
r := reflect.TypeOf(res)
// we're setting using a pointer so
// it must always be a pointer passed
// in
if r.Kind() != reflect.Ptr {
return errors.New("res must be a pointer")
}
// dereference the pointer
r = r.Elem()
value := reflect.ValueOf(res).Elem()
// split our serialization string into
// a map
vals := strings.Split(s, ";")
valMap := make(map[string]string)
for _, v := range vals {
keyval := strings.Split(v, ":")
if len(keyval) != 2 {
continue
}
valMap[keyval[0]] = keyval[1]
}
// iterate over fields
for i := 0; i < r.NumField(); i++ {
field := r.Field(i)
// check if in the serialize set
if serialize, ok := field.Tag.Lookup("serialize"); ok {
// ignore "-" otherwise that whole value
// becomes the serialize 'key'
if serialize == "-" {
continue
}
// is it in the map
if val, ok := valMap[serialize]; ok {
value.Field(i).SetString(val)
}
} else if val, ok := valMap[field.Name]; ok {
// is our field name in the map instead?
value.Field(i).SetString(val)
}
}
return nil
}
- 创建一个名为
tags.go
的文件,内容如下:
package tags
import "fmt"
// Person is a struct that stores a persons
// name, city, state, and a misc attribute
type Person struct {
Name string `serialize:"name"`
City string `serialize:"city"`
State string
Misc string `serialize:"-"`
Year int `serialize:"year"`
}
// EmptyStruct demonstrates serialize
// and deserialize for an Empty struct
// with tags
func EmptyStruct() error {
p := Person{}
res, err := SerializeStructStrings(&p)
if err != nil {
return err
}
fmt.Printf("Empty struct: %#v\n", p)
fmt.Println("Serialize Results:", res)
newP := Person{}
if err := DeSerializeStructStrings(res, &newP); err != nil
{
return err
}
fmt.Printf("Deserialize results: %#v\n", newP)
return nil
}
// FullStruct demonstrates serialize
// and deserialize for an Full struct
// with tags
func FullStruct() error {
p := Person{
Name: "Aaron",
City: "Seattle",
State: "WA",
Misc: "some fact",
Year: 2017,
}
res, err := SerializeStructStrings(&p)
if err != nil {
return err
}
fmt.Printf("Full struct: %#v\n", p)
fmt.Println("Serialize Results:", res)
newP := Person{}
if err := DeSerializeStructStrings(res, &newP);
err != nil {
return err
}
fmt.Printf("Deserialize results: %#v\n", newP)
return nil
}
-
创建一个名为
example
的新目录并进入。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter3/tags"
)
func main() {
if err := tags.EmptyStruct(); err != nil {
panic(err)
}
fmt.Println()
if err := tags.FullStruct(); err != nil {
panic(err)
}
}
- 运行
go run main.go
。你也可以运行以下命令:
$ go build $ ./example
你应该看到以下输出:
$ go run main.go
Empty struct: tags.Person{Name:"", City:"", State:"", Misc:"",
Year:0}
Serialize Results: name:;city:;State:;
Deserialize results: tags.Person{Name:"", City:"", State:"",
Misc:"", Year:0}
Full struct: tags.Person{Name:"Aaron", City:"Seattle",
State:"WA", Misc:"some fact", Year:2017}
Serialize Results: name:Aaron;city:Seattle;State:WA;
Deserialize results: tags.Person{Name:"Aaron", City:"Seattle",
State:"WA", Misc:"", Year:0}
- 如果你复制或编写了自己的测试,返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
这个示例创建了一个字符串序列化格式,它接受一个struct
值并将所有字符串字段序列化为可解析的格式。这个示例不处理某些边缘情况;特别是,字符串不能包含冒号(:
)或分号(;
)字符。以下是它的行为摘要:
-
如果一个字段是字符串,它将被序列化/反序列化。
-
如果一个字段不是字符串,它将被忽略。
-
如果字段的
struct
标签包含序列化的“键”,那么该键将成为返回的序列化/反序列化环境。 -
不处理重复。
-
如果未指定
struct
标签,则使用字段名。 -
如果
struct
标签的值是连字符(-
),则该字段将被忽略,即使它是一个字符串。
还有一些需要注意的是,反射不能完全处理非导出值。
通过闭包实现集合
如果您一直在使用函数式或动态编程语言,您可能会觉得for
循环和if
语句会产生冗长的代码。对列表进行处理时使用map
和filter
等函数构造可能很有用,并且可以使代码看起来更可读;但是,在 Go 中,这些类型不在标准库中,并且在没有泛型或非常复杂的反射和使用空接口的情况下很难泛化。这个配方将为您提供使用 Go 闭包实现集合的一些基本示例。
如何做...
以下步骤涵盖了如何编写和运行您的应用程序:
-
从您的终端/控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter3/collections
的新目录。 -
转到此目录。
-
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter3/collections
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter3/collections
-
从
~/projects/go-programming-cookbook-original/chapter3/collections
复制测试,或者将其用作编写自己代码的练习! -
创建一个名为
collections.go
的文件,其中包含以下内容:
package collections
// WorkWith is the struct we'll
// be implementing collections for
type WorkWith struct {
Data string
Version int
}
// Filter is a functional filter. It takes a list of
// WorkWith and a WorkWith Function that returns a bool
// for each "true" element we return it to the resultant
// list
func Filter(ws []WorkWith, f func(w WorkWith) bool) []WorkWith
{
// depending on results, smalles size for result
// is len == 0
result := make([]WorkWith, 0)
for _, w := range ws {
if f(w) {
result = append(result, w)
}
}
return result
}
// Map is a functional map. It takes a list of
// WorkWith and a WorkWith Function that takes a WorkWith
// and returns a modified WorkWith. The end result is
// a list of modified WorkWiths
func Map(ws []WorkWith, f func(w WorkWith) WorkWith) []WorkWith
{
// the result should always be the same
// length
result := make([]WorkWith, len(ws))
for pos, w := range ws {
newW := f(w)
result[pos] = newW
}
return result
}
- 创建一个名为
functions.go
的文件,其中包含以下内容:
package collections
import "strings"
// LowerCaseData does a ToLower to the
// Data string of a WorkWith
func LowerCaseData(w WorkWith) WorkWith {
w.Data = strings.ToLower(w.Data)
return w
}
// IncrementVersion increments a WorkWiths
// Version
func IncrementVersion(w WorkWith) WorkWith {
w.Version++
return w
}
// OldVersion returns a closures
// that validates the version is greater than
// the specified amount
func OldVersion(v int) func(w WorkWith) bool {
return func(w WorkWith) bool {
return w.Version >= v
}
}
-
创建一个名为
example
的新目录,并转到该目录。 -
创建一个名为
main.go
的文件,其中包含以下内容:
package main
import (
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter3/collections"
)
func main() {
ws := []collections.WorkWith{
collections.WorkWith{"Example", 1},
collections.WorkWith{"Example 2", 2},
}
fmt.Printf("Initial list: %#v\n", ws)
// first lower case the list
ws = collections.Map(ws, collections.LowerCaseData)
fmt.Printf("After LowerCaseData Map: %#v\n", ws)
// next increment all versions
ws = collections.Map(ws, collections.IncrementVersion)
fmt.Printf("After IncrementVersion Map: %#v\n", ws)
// lastly remove all versions older than 3
ws = collections.Filter(ws, collections.OldVersion(3))
fmt.Printf("After OldVersion Filter: %#v\n", ws)
}
- 运行
go run main.go
。您也可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
Initial list:
[]collections.WorkWith{collections.WorkWith{Data:"Example",
Version:1}, collections.WorkWith{Data:"Example 2", Version:2}}
After LowerCaseData Map:
[]collections.WorkWith{collections.WorkWith{Data:"example",
Version:1}, collections.WorkWith{Data:"example 2", Version:2}}
After IncrementVersion Map:
[]collections.WorkWith{collections.WorkWith{Data:"example",
Version:2}, collections.WorkWith{Data:"example 2", Version:3}}
After OldVersion Filter:
[]collections.WorkWith{collections.WorkWith{Data:"example 2",
Version:3}}
- 如果您复制或编写了自己的测试,请返回上一个目录并运行
go test
。确保所有测试都通过。
工作原理...
Go 中的闭包非常强大。虽然我们的collections
函数不是通用的,但它们相对较小,可以很容易地应用于我们的WorkWith
结构,而只需使用各种函数添加最少的代码。您可能会注意到,我们没有在任何地方返回错误。这些函数的理念是它们是纯粹的:原始列表没有副作用,除了我们选择在每次调用后覆盖它。
如果您需要对列表或列表结构应用修改层,则此模式可以帮助您避免许多混乱,并使测试变得非常简单。还可以将映射和过滤器链接在一起,以实现非常表达的编码风格。
第四章:Go 中的错误处理
即使是最基本的 Go 程序,错误处理也很重要。Go 中的错误实现了Error
接口,并且必须在代码的每一层中处理。Go 的错误不像异常那样工作,未处理的错误可能会导致巨大的问题。您应该努力处理和考虑每当出现错误时。
本章还涵盖了日志记录,因为在实际错误发生时通常会记录日志。我们还将研究包装错误,以便给定的错误在返回到函数堆栈时提供额外的上下文,这样更容易确定某些错误的实际原因。
在本章中,将介绍以下配方:
-
处理错误和 Error 接口
-
使用 pkg/errors 包和包装错误
-
使用日志包并了解何时记录错误
-
使用 apex 和 logrus 包进行结构化日志记录
-
使用上下文包进行日志记录
-
使用包级全局变量
-
捕获长时间运行进程的 panic
技术要求
为了继续本章中的所有配方,请根据以下步骤配置您的环境:
-
在您的操作系统上下载并安装 Go 1.12.6 或更高版本,网址为
golang.org/doc/install
。 -
打开终端/控制台应用程序;创建并导航到项目目录,例如
~/projects/go-programming-cookbook
。所有代码将在此目录中运行和修改。 -
将最新的代码克隆到
~/projects/go-programming-cookbook-original
,或者可以选择从该目录工作,而不是手动输入示例:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
处理错误和 Error 接口
Error
接口是一个非常小且简单的接口:
type Error interface{
Error() string
}
这个接口很简洁,因为很容易制作任何东西来满足它。不幸的是,这也给需要根据接收到的错误采取某些操作的包带来了困惑。
在 Go 中创建错误的方法有很多种;本篇将探讨创建基本错误、具有分配值或类型的错误,以及使用结构创建自定义错误。
操作步骤...
以下步骤涵盖了编写和运行应用程序:
-
从您的终端/控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter4/basicerrors
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/basicerrors
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/basicerrors
-
从
~/projects/go-programming-cookbook-original/chapter4/basicerrors
复制测试,或者利用这个机会编写一些您自己的代码! -
创建一个名为
basicerrors.go
的文件,其中包含以下内容:
package basicerrors
import (
"errors"
"fmt"
)
// ErrorValue is a way to make a package level
// error to check against. I.e. if err == ErrorValue
var ErrorValue = errors.New("this is a typed error")
// TypedError is a way to make an error type
// you can do err.(type) == ErrorValue
type TypedError struct {
error
}
//BasicErrors demonstrates some ways to create errors
func BasicErrors() {
err := errors.New("this is a quick and easy way to create an error")
fmt.Println("errors.New: ", err)
err = fmt.Errorf("an error occurred: %s", "something")
fmt.Println("fmt.Errorf: ", err)
err = ErrorValue
fmt.Println("value error: ", err)
err = TypedError{errors.New("typed error")}
fmt.Println("typed error: ", err)
}
- 创建一个名为
custom.go
的文件,其中包含以下内容:
package basicerrors
import (
"fmt"
)
// CustomError is a struct that will implement
// the Error() interface
type CustomError struct {
Result string
}
func (c CustomError) Error() string {
return fmt.Sprintf("there was an error; %s was the result", c.Result)
}
// SomeFunc returns an error
func SomeFunc() error {
c := CustomError{Result: "this"}
return c
}
-
创建一个名为
example
的新目录并导航到该目录。 -
创建一个名为
main.go
的文件,其中包含以下内容:
package main
import (
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter4/basicerrors"
)
func main() {
basicerrors.BasicErrors()
err := basicerrors.SomeFunc()
fmt.Println("custom error: ", err)
}
-
运行
go run main.go
。 -
您也可以运行以下命令:
$ go build $ ./example
您现在应该看到以下输出:
$ go run main.go
errors.New: this is a quick and easy way to create an error
fmt.Errorf: an error occurred: something
typed error: this is a typed error
custom error: there was an error; this was the result
- 如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
无论您使用errors.New
、fmt.Errorf
还是自定义错误,最重要的是要记住,您不应该在代码中留下未处理的错误。定义错误的这些不同方法提供了很大的灵活性。例如,您可以在结构中添加额外的函数来进一步查询错误,并在调用函数中将接口转换为您的错误类型以获得一些附加功能。
接口本身非常简单,唯一的要求是返回一个有效的字符串。将其连接到结构可能对一些高级应用程序有用,这些应用程序在整个过程中具有一致的错误处理,但希望与其他应用程序良好地配合。
使用 pkg/errors 包和包装错误
位于github.com/pkg/errors
的errors
包是标准 Go errors
包的一个可替换项。此外,它还提供了一些非常有用的功能来包装和处理错误。前面的示例中的类型和声明的错误就是一个很好的例子——它们可以用来向错误添加额外的信息,但以标准方式包装它将改变其类型并破坏类型断言:
// this wont work if you wrapped it
// in a standard way. that is,
// fmt.Errorf("custom error: %s", err.Error())
if err == Package.ErrorNamed{
//handle this error in a specific way
}
本示例将演示如何使用pkg/errors
包在整个代码中添加注释。
如何做...
这些步骤涵盖了编写和运行应用程序的过程:
-
从您的终端/控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter4/errwrap
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/errwrap
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/errwrap
-
从
~/projects/go-programming-cookbook-original/chapter4/errwrap
复制测试,或者将其用作练习编写自己的一些代码! -
创建一个名为
errwrap.go
的文件,内容如下:
package errwrap
import (
"fmt"
"github.com/pkg/errors"
)
// WrappedError demonstrates error wrapping and
// annotating an error
func WrappedError(e error) error {
return errors.Wrap(e, "An error occurred in WrappedError")
}
// ErrorTyped is a error we can check against
type ErrorTyped struct{
error
}
// Wrap shows what happens when we wrap an error
func Wrap() {
e := errors.New("standard error")
fmt.Println("Regular Error - ", WrappedError(e))
fmt.Println("Typed Error - ",
WrappedError(ErrorTyped{errors.New("typed error")}))
fmt.Println("Nil -", WrappedError(nil))
}
- 创建一个名为
unwrap.go
的文件,内容如下:
package errwrap
import (
"fmt"
"github.com/pkg/errors"
)
// Unwrap will unwrap an error and do
// type assertion to it
func Unwrap() {
err := error(ErrorTyped{errors.New("an error occurred")})
err = errors.Wrap(err, "wrapped")
fmt.Println("wrapped error: ", err)
// we can handle many error types
switch errors.Cause(err).(type) {
case ErrorTyped:
fmt.Println("a typed error occurred: ", err)
default:
fmt.Println("an unknown error occurred")
}
}
// StackTrace will print all the stack for
// the error
func StackTrace() {
err := error(ErrorTyped{errors.New("an error occurred")})
err = errors.Wrap(err, "wrapped")
fmt.Printf("%+v\n", err)
}
-
创建一个名为
example
的新目录,并导航到该目录。 -
创建一个
main.go
文件,内容如下:
package main
import (
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter4/errwrap"
)
func main() {
errwrap.Wrap()
fmt.Println()
errwrap.Unwrap()
fmt.Println()
errwrap.StackTrace()
}
-
运行
go run main.go
。 -
您还可以运行以下命令:
$ go build $ ./example
现在您应该看到以下输出:
$ go run main.go
Regular Error - An error occurred in WrappedError: standard
error
Typed Error - An error occurred in WrappedError: typed error
Nil - <nil>
wrapped error: wrapped: an error occurred
a typed error occurred: wrapped: an error occurred
an error occurred
github.com/PacktPublishing/Go-Programming-Cookbook-Second-
Edition/chapter4/errwrap.StackTrace
/Users/lothamer/go/src/github.com/agtorre/go-
cookbook/chapter4/errwrap/unwrap.go:30
main.main
/tmp/go/src/github.com/agtorre/go-
cookbook/chapter4/errwrap/example/main.go:14
-
go.mod
文件应该已更新,顶级示例目录中现在应该存在go.sum
文件。 -
如果您复制或编写了自己的测试,请返回到上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
pkg/errors
包是一个非常有用的工具。使用这个包来包装每个返回的错误以提供额外的上下文记录和错误调试是有意义的。当错误发生时,它足够灵活,可以打印整个堆栈跟踪,也可以在打印错误时只是添加前缀。它还可以清理代码,因为包装的 nil 返回一个nil
值;例如,考虑以下代码:
func RetError() error{
err := ThisReturnsAnError()
return errors.Wrap(err, "This only does something if err != nil")
}
在某些情况下,这可以使您免于在简单返回错误之前首先检查错误是否为nil
。本示例演示了如何使用该包来包装和解包错误,以及基本的堆栈跟踪功能。该包的文档还提供了一些其他有用的示例,例如打印部分堆栈。该库的作者 Dave Cheney 还写了一些有用的博客并就此主题发表了一些演讲;您可以访问dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully
了解更多信息。
使用日志包并了解何时记录错误
通常在错误是最终结果时应记录日志。换句话说,当发生异常或意外情况时记录日志是有用的。如果您使用提供日志级别的日志,可能还适合在代码的关键部分添加调试或信息语句,以便在开发过程中快速调试问题。过多的日志记录会使查找有用信息变得困难,但日志记录不足可能导致系统崩溃而无法了解根本原因。本示例将演示默认的 Go log
包和一些有用的选项的使用,还展示了何时可能应该记录日志。
如何做...
这些步骤涵盖了编写和运行应用程序的过程:
-
从您的终端/控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter4/log
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/log
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/log
-
从
~/projects/go-programming-cookbook-original/chapter4/log
复制测试,或者将其用作练习编写自己的一些代码! -
创建一个名为
log.go
的文件,内容如下:
package log
import (
"bytes"
"fmt"
"log"
)
// Log uses the setup logger
func Log() {
// we'll configure the logger to write
// to a bytes.Buffer
buf := bytes.Buffer{}
// second argument is the prefix last argument is about
// options you combine them with a logical or.
logger := log.New(&buf, "logger: ",
log.Lshortfile|log.Ldate)
logger.Println("test")
logger.SetPrefix("new logger: ")
logger.Printf("you can also add args(%v) and use Fatalln to
log and crash", true)
fmt.Println(buf.String())
}
- 创建一个名为
error.go
的文件,内容如下:
package log
import "github.com/pkg/errors"
import "log"
// OriginalError returns the error original error
func OriginalError() error {
return errors.New("error occurred")
}
// PassThroughError calls OriginalError and
// forwards the error along after wrapping.
func PassThroughError() error {
err := OriginalError()
// no need to check error
// since this works with nil
return errors.Wrap(err, "in passthrougherror")
}
// FinalDestination deals with the error
// and doesn't forward it
func FinalDestination() {
err := PassThroughError()
if err != nil {
// we log because an unexpected error occurred!
log.Printf("an error occurred: %s\n", err.Error())
return
}
}
-
创建一个名为
example
的新目录,并导航到该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter4/log"
)
func main() {
fmt.Println("basic logging and modification of logger:")
log.Log()
fmt.Println("logging 'handled' errors:")
log.FinalDestination()
}
-
运行
go run main.go
。 -
您还可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
basic logging and modification of logger:
logger: 2017/02/05 log.go:19: test
new logger: 2017/02/05 log.go:23: you can also add args(true)
and use Fataln to log and crash
logging 'handled' errors:
2017/02/05 18:36:11 an error occurred: in passthrougherror:
error occurred
-
go.mod
文件将被更新,go.sum
文件现在应该存在于顶级配方目录中。 -
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
您可以初始化一个记录器并传递它使用 log.NewLogger()
,或者使用 log
包级别的记录器来记录消息。这个配方中的 log.go
文件执行前者,error.go
执行后者。它还显示了在错误到达最终目的地后记录可能是有意义的时间;否则,可能会为一个事件记录多次。
这种方法存在一些问题。首先,您可能在其中一个中间函数中有额外的上下文,比如您想要记录的变量。其次,记录一堆变量可能会变得混乱,使其令人困惑和难以阅读。下一个配方将探讨提供灵活性的结构化日志记录,以记录变量,并且在以后的配方中,我们将探讨实现全局包级别记录器。
使用 apex 和 logrus 包进行结构化日志记录
记录信息的主要原因是在事件发生或过去发生时检查系统的状态。当有大量微服务记录日志时,基本的日志消息很难查看。
如果您可以将日志记录到它们理解的数据格式中,那么有各种第三方包可以对日志进行检索。这些包提供索引功能、可搜索性等。sirupsen/logrus
和 apex/log
包提供了一种结构化日志记录的方式,您可以记录许多字段,这些字段可以重新格式化以适应这些第三方日志读取器。例如,可以简单地以 JSON 格式发出日志,以便被各种服务解析。
如何做...
这些步骤涵盖了您的应用程序的编写和运行:
-
从您的终端/控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter4/structured
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/structured
您应该看到一个名为 go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/structured
-
从
~/projects/go-programming-cookbook-original/chapter4/structured
复制测试,或者将其作为练习编写一些自己的代码! -
创建一个名为
logrus.go
的文件,内容如下:
package structured
import "github.com/sirupsen/logrus"
// Hook will implement the logrus
// hook interface
type Hook struct {
id string
}
// Fire will trigger whenever you log
func (hook *Hook) Fire(entry *logrus.Entry) error {
entry.Data["id"] = hook.id
return nil
}
// Levels is what levels this hook will fire on
func (hook *Hook) Levels() []logrus.Level {
return logrus.AllLevels
}
// Logrus demonstrates some basic logrus functionality
func Logrus() {
// we're emitting in json format
logrus.SetFormatter(&logrus.TextFormatter{})
logrus.SetLevel(logrus.InfoLevel)
logrus.AddHook(&Hook{"123"})
fields := logrus.Fields{}
fields["success"] = true
fields["complex_struct"] = struct {
Event string
When string
}{"Something happened", "Just now"}
x := logrus.WithFields(fields)
x.Warn("warning!")
x.Error("error!")
}
- 创建一个名为
apex.go
的文件,内容如下:
package structured
import (
"errors"
"os"
"github.com/apex/log"
"github.com/apex/log/handlers/text"
)
// ThrowError throws an error that we'll trace
func ThrowError() error {
err := errors.New("a crazy failure")
log.WithField("id", "123").Trace("ThrowError").Stop(&err)
return err
}
// CustomHandler splits to two streams
type CustomHandler struct {
id string
handler log.Handler
}
// HandleLog adds a hook and does the emitting
func (h *CustomHandler) HandleLog(e *log.Entry) error {
e.WithField("id", h.id)
return h.handler.HandleLog(e)
}
// Apex has a number of useful tricks
func Apex() {
log.SetHandler(&CustomHandler{"123", text.New(os.Stdout)})
err := ThrowError()
//With error convenience function
log.WithError(err).Error("an error occurred")
}
-
创建一个名为
example
的新目录并导航到该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter4/structured"
)
func main() {
fmt.Println("Logrus:")
structured.Logrus()
fmt.Println()
fmt.Println("Apex:")
structured.Apex()
}
-
运行
go run main.go
。 -
您还可以运行以下命令:
$ go build $ ./example
您现在应该看到以下输出:
$ go run main.go
Logrus:
WARN[0000] warning! complex_struct={Something happened Just now}
id=123 success=true
ERRO[0000] error! complex_struct={Something happened Just now}
id=123 success=true
Apex:
INFO[0000] ThrowError id=123
ERROR[0000] ThrowError duration=133ns error=a crazy failure
id=123
ERROR[0000] an error occurred error=a crazy failure
-
go.mod
文件应该被更新,go.sum
文件现在应该存在于顶级配方目录中。 -
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
sirupsen/logrus
和 apex/log
包都是优秀的结构化记录器。两者都提供了钩子,可以用于发出多个事件或向日志条目添加额外字段。例如,可以相对简单地使用 logrus
钩子或 apex
自定义处理程序来向所有日志添加行号以及服务名称。钩子的另一个用途可能包括 traceID
,以跟踪请求在不同服务之间的传递。
虽然 logrus
将钩子和格式化器分开,但 apex
将它们合并在一起。除此之外,apex
还添加了一些方便的功能,比如 WithError
添加一个 error
字段以及跟踪,这两者都在这个配方中进行了演示。从 logrus
转换到 apex
处理程序的适配也相对简单。对于这两种解决方案,将转换为 JSON 格式,而不是 ANSI 彩色文本,将是一个简单的改变。
使用上下文包进行日志记录
这个配方将演示一种在各种函数之间传递日志字段的方法。Go pkg/context
包是在函数之间传递附加变量和取消的绝佳方式。这个配方将探讨使用这个功能将变量分发到函数之间以进行日志记录。
这种风格可以从前一个配方中适应logrus
或apex
。我们将在这个配方中使用apex
。
如何做...
这些步骤涵盖了编写和运行应用程序的过程:
-
从您的终端/控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter4/context
的新目录,并转到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/context
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/context
-
从
~/projects/go-programming-cookbook-original/chapter4/context
中复制测试,或者将其用作编写自己代码的练习! -
创建一个名为
log.go
的文件,其中包含以下内容:
package context
import (
"context"
"github.com/apex/log"
)
type key int
// logFields is a key we use
// for our context logging
const logFields key = 0
func getFields(ctx context.Context) *log.Fields {
fields, ok := ctx.Value(logFields).(*log.Fields)
if !ok {
f := make(log.Fields)
fields = &f
}
return fields
}
// FromContext takes an entry and a context
// then returns an entry populated from the context object
func FromContext(ctx context.Context, l log.Interface)
(context.Context, *log.Entry) {
fields := getFields(ctx)
e := l.WithFields(fields)
ctx = context.WithValue(ctx, logFields, fields)
return ctx, e
}
// WithField adds a log field to the context
func WithField(ctx context.Context, key string, value
interface{}) context.Context {
return WithFields(ctx, log.Fields{key: value})
}
// WithFields adds many log fields to the context
func WithFields(ctx context.Context, fields log.Fielder)
context.Context {
f := getFields(ctx)
for key, val := range fields.Fields() {
(*f)[key] = val
}
ctx = context.WithValue(ctx, logFields, f)
return ctx
}
- 创建一个名为
collect.go
的文件,其中包含以下内容:
package context
import (
"context"
"os"
"github.com/apex/log"
"github.com/apex/log/handlers/text"
)
// Initialize calls 3 functions to set up, then
// logs before terminating
func Initialize() {
// set basic log up
log.SetHandler(text.New(os.Stdout))
// initialize our context
ctx := context.Background()
// create a logger and link it to
// the context
ctx, e := FromContext(ctx, log.Log)
// set a field
ctx = WithField(ctx, "id", "123")
e.Info("starting")
gatherName(ctx)
e.Info("after gatherName")
gatherLocation(ctx)
e.Info("after gatherLocation")
}
func gatherName(ctx context.Context) {
ctx = WithField(ctx, "name", "Go Cookbook")
}
func gatherLocation(ctx context.Context) {
ctx = WithFields(ctx, log.Fields{"city": "Seattle",
"state": "WA"})
}
-
创建一个名为
example
的新目录,并转到该目录。 -
创建一个
main.go
文件,其中包含以下内容:
package main
import "github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter4/context"
func main() {
context.Initialize()
}
-
运行
go run main.go
。 -
您还可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
INFO[0000] starting id=123
INFO[0000] after gatherName id=123 name=Go Cookbook
INFO[0000] after gatherLocation city=Seattle id=123 name=Go
Cookbook state=WA
-
go.mod
文件已更新,go.sum
文件现在应该存在于顶层配方目录中。 -
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
context
包现在出现在各种包中,包括数据库和 HTTP 包。这个配方将允许您将日志字段附加到上下文中,并将它们用于日志记录目的。其思想是不同的方法可以在上下文中附加更多字段,然后最终的调用站点可以执行日志记录和聚合变量。
这个配方模仿了前一个配方中日志包中找到的WithField
和WithFields
方法。这些方法修改了上下文中存储的单个值,并提供了使用上下文的其他好处:取消、超时和线程安全。
使用包级全局变量
在之前的示例中,apex
和logrus
包都使用了包级全局变量。有时,将您的库结构化以支持具有各种方法和顶级函数的结构是有用的,这样您可以直接使用它们而不必传递它们。
这个配方还演示了使用sync.Once
来确保全局记录器只初始化一次。它也可以被Set
方法绕过。该配方只导出WithField
和Debug
,但您可以想象导出附加到log
对象的每个方法。
如何做...
这些步骤涵盖了编写和运行应用程序的过程:
-
从您的终端/控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter4/global
的新目录,并转到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/global
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/global
-
复制
~/projects/go-programming-cookbook-original/chapter4/global
中的测试,或者将其用作编写自己代码的练习! -
创建一个名为
global.go
的文件,其中包含以下内容:
package global
import (
"errors"
"os"
"sync"
"github.com/sirupsen/logrus"
)
// we make our global package level
// variable lower case
var (
log *logrus.Logger
initLog sync.Once
)
// Init sets up the logger initially
// if run multiple times, it returns
// an error
func Init() error {
err := errors.New("already initialized")
initLog.Do(func() {
err = nil
log = logrus.New()
log.Formatter = &logrus.JSONFormatter{}
log.Out = os.Stdout
log.Level = logrus.DebugLevel
})
return err
}
// SetLog sets the log
func SetLog(l *logrus.Logger) {
log = l
}
// WithField exports the logs withfield connected
// to our global log
func WithField(key string, value interface{}) *logrus.Entry {
return log.WithField(key, value)
}
// Debug exports the logs Debug connected
// to our global log
func Debug(args ...interface{}) {
log.Debug(args...)
}
- 创建一个名为
log.go
的文件,其中包含以下内容:
package global
// UseLog demonstrates using our global
// log
func UseLog() error {
if err := Init(); err != nil {
return err
}
// if we were in another package these would be
// global.WithField and
// global.Debug
WithField("key", "value").Debug("hello")
Debug("test")
return nil
}
-
创建一个名为
example
的新目录,并转到该目录。 -
创建一个
main.go
文件,其中包含以下内容:
package main
import "github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter4/global"
func main() {
if err := global.UseLog(); err != nil {
panic(err)
}
}
-
运行
go run main.go
。 -
您还可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
{"key":"value","level":"debug","msg":"hello","time":"2017-02-
12T19:22:50-08:00"}
{"level":"debug","msg":"test","time":"2017-02-12T19:22:50-
08:00"}
-
go.mod
文件已更新,go.sum
文件现在应该存在于顶层配方目录中。 -
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
这些global
包级别对象的常见模式是保持global
变量未导出,并仅通过方法公开所需的功能。通常,你还可以包括一个方法来返回global
日志记录器的副本,以供需要logger
对象的包使用。
sync.Once
类型是一个新引入的结构。这个结构与Do
方法一起,只会在代码中执行一次。我们在初始化代码中使用这个结构,如果Init
被调用多次,Init
函数将抛出错误。如果我们想要向我们的global
日志传递参数,我们使用自定义的Init
函数而不是内置的init()
函数。
尽管这个例子使用了日志,你也可以想象在数据库连接、数据流和许多其他用例中这可能是有用的情况。
捕获长时间运行进程的 panic
在实现长时间运行的进程时,可能会出现某些代码路径导致 panic 的情况。这通常是常见的情况,比如未初始化的映射和指针,以及在用户输入验证不良的情况下出现的除零问题。
在这些情况下,程序完全崩溃通常比 panic 本身更糟糕,因此捕获和处理 panic 是有帮助的。
如何做...
这些步骤涵盖了编写和运行应用程序的过程:
-
从你的终端/控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter4/panic
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/panic
你应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/panic
-
从
~/projects/go-programming-cookbook-original/chapter4/panic
复制测试,或者利用这个机会编写一些你自己的代码! -
创建一个名为
panic.go
的文件,内容如下:
package panic
import (
"fmt"
"strconv"
)
// Panic panics with a divide by zero
func Panic() {
zero, err := strconv.ParseInt("0", 10, 64)
if err != nil {
panic(err)
}
a := 1 / zero
fmt.Println("we'll never get here", a)
}
// Catcher calls Panic
func Catcher() {
defer func() {
if r := recover(); r != nil {
fmt.Println("panic occurred:", r)
}
}()
Panic()
}
-
创建一个名为
example
的新目录,并导航到该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter4/panic"
)
func main() {
fmt.Println("before panic")
panic.Catcher()
fmt.Println("after panic")
}
-
运行
go run main.go
。 -
你也可以运行以下命令:
$ go build $ ./example
你应该看到以下输出:
$ go run main.go
before panic
panic occurred: runtime error: integer divide by zero
after panic
- 如果你复制或编写了自己的测试,那么返回上一个目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
这个示例是如何捕获 panic 的一个非常基本的例子。你可以想象使用更复杂的中间件,如何可以延迟恢复并在运行许多嵌套函数后捕获它。在恢复中,你可以做任何你想做的事情,尽管发出日志是常见的。
在大多数 Web 应用程序中,捕获 panic 并在发生 panic 时发出http.InternalServerError
消息是很常见的。
第五章:网络编程
Go 标准库为网络操作提供了大量支持。它包括允许您使用 TCP/IP、UDP、DNS、邮件和使用 HTTP 的 RPC 的包。第三方包也可以填补标准库中包含的内容的空白,包括gorilla/websockets
(github.com/gorilla/websocket/
),用于 WebSocket 实现,可以在普通的 HTTP 处理程序中使用。本章探讨了这些库,并演示了一些简单的用法。这些用法将帮助那些无法使用更高级的抽象,如 REST 或 GRPC,但需要网络连接的开发人员。它对需要执行 DNS 查找或处理原始电子邮件的 DevOps 应用程序也很有用。阅读完本章后,您应该已经掌握了基本的网络编程,并准备深入学习。
在本章中,将涵盖以下用法:
-
编写 TCP/IP 回显服务器和客户端
-
编写 UDP 服务器和客户端
-
处理域名解析
-
使用 WebSockets
-
使用 net/rpc 调用远程方法
-
使用 net/mail 解析电子邮件
技术要求
为了继续本章中的所有用法,请按照以下步骤配置您的环境:
-
从
golang.org/doc/install
下载并安装 Go 1.12.6 或更高版本到您的操作系统上。 -
打开终端或控制台应用程序,然后创建并导航到一个项目目录,例如
~/projects/go-programming-cookbook
。所有代码都将从这个目录运行和修改。 -
将最新的代码克隆到
~/projects/go-programming-cookbook-original
,您可以选择从该目录工作,而不是手动输入示例:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
编写 TCP/IP 回显服务器和客户端
TCP/IP 是一种常见的网络协议,HTTP 协议是在其上构建的。TCP 要求客户端连接到服务器以发送和接收数据。这个用法将使用net
包在客户端和服务器之间建立 TCP 连接。客户端将把用户输入发送到服务器,服务器将用strings.ToUpper()
的结果将输入的相同字符串转换为大写形式进行响应。客户端将打印从服务器接收到的任何消息,因此它应该输出我们输入的大写版本。
如何做...
这些步骤涵盖了编写和运行您的应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter5/tcp
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/tcp
您应该会看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/tcp
-
从
~/projects/go-programming-cookbook-original/chapter5/tcp
复制测试,或者使用这个作为练习编写一些您自己的代码! -
创建一个名为
server
的新目录,并导航到该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"bufio"
"fmt"
"net"
"strings"
)
const addr = "localhost:8888"
func echoBackCapitalized(conn net.Conn) {
// set up a reader on conn (an io.Reader)
reader := bufio.NewReader(conn)
// grab the first line of data encountered
data, err := reader.ReadString('\n')
if err != nil {
fmt.Printf("error reading data: %s\n", err.Error())
return
}
// print then send back the data
fmt.Printf("Received: %s", data)
conn.Write([]byte(strings.ToUpper(data)))
// close up the finished connection
conn.Close()
}
func main() {
ln, err := net.Listen("tcp", addr)
if err != nil {
panic(err)
}
defer ln.Close()
fmt.Printf("listening on: %s\n", addr)
for {
conn, err := ln.Accept()
if err != nil {
fmt.Printf("encountered an error accepting connection: %s\n",
err.Error())
// if there's an error try again
continue
}
// handle this asynchronously
// potentially a good use-case
// for a worker pool
go echoBackCapitalized(conn)
}
}
-
导航到上一个目录。
-
创建一个名为
client
的新目录,并导航到该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"bufio"
"fmt"
"net"
"os"
)
const addr = "localhost:8888"
func main() {
reader := bufio.NewReader(os.Stdin)
for {
// grab a string input from the clie
fmt.Printf("Enter some text: ")
data, err := reader.ReadString('\n')
if err != nil {
fmt.Printf("encountered an error reading input: %s\n",
err.Error())
continue
}
// connect to the addr
conn, err := net.Dial("tcp", addr)
if err != nil {
fmt.Printf("encountered an error connecting: %s\n",
err.Error())
}
// write the data to the connection
fmt.Fprintf(conn, data)
// read back the response
status, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
fmt.Printf("encountered an error reading response: %s\n",
err.Error())
}
fmt.Printf("Received back: %s", status)
// close up the finished connection
conn.Close()
}
}
-
导航到上一个目录。
-
运行
go run ./server
,您将看到以下输出:
$ go run ./server
listening on: localhost:8888
- 在另一个终端中,从
tcp
目录运行go run ./client
,您将看到以下输出:
$ go run ./client
Enter some text:
- 输入
this is a test
并按Enter。您将看到以下内容:
$ go run ./client
Enter some text: this is a test
Received back: THIS IS A TEST
Enter some text:
-
按下Ctrl + C退出。
-
如果您复制或编写了自己的测试,请返回上一个目录并运行
go test
。确保所有测试都通过。
工作原理...
服务器正在侦听端口8888
。每当有请求时,服务器必须接收请求并管理客户端连接。在这个程序的情况下,它会派发一个 Goroutine 来从客户端读取请求,将接收到的数据大写,发送回客户端,最后关闭连接。服务器立即再次循环,等待接收新的客户端连接,同时处理先前的连接。
客户端从STDIN
读取输入,通过 TCP 连接到地址,写入从输入中读取的消息,然后打印服务器的响应。之后,它关闭连接并再次从STDIN
循环读取。您也可以重新设计此示例,使客户端保持连接,直到程序退出,而不是在每个请求上。
编写 UDP 服务器和客户端
UDP 协议通常用于游戏和速度比可靠性更重要的地方。UDP 服务器和客户端不需要相互连接。这个示例将创建一个 UDP 服务器,它将监听来自客户端的消息,将它们的 IP 添加到其列表中,并向先前看到的每个客户端广播消息。
每当客户端连接时,服务器将向STDOUT
写入一条消息,并将相同的消息广播给所有客户端。这条消息的文本应该是Sent <count>
,其中<count>
将在服务器向所有客户端广播时递增。因此,count
的值可能会有所不同,这取决于您连接到客户端所需的时间,因为服务器将无论发送消息给多少客户端都会广播。
如何做...
这些步骤涵盖了编写和运行应用程序的过程:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter5/udp
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/udp
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/udp
-
从
~/projects/go-programming-cookbook-original/chapter5/udp
复制测试,或者将其用作编写自己代码的练习! -
创建一个名为
server
的新目录,并导航到该目录。 -
创建一个名为
broadcast.go
的文件,内容如下:
package main
import (
"fmt"
"net"
"sync"
"time"
)
type connections struct {
addrs map[string]*net.UDPAddr
// lock for modifying the map
mu sync.Mutex
}
func broadcast(conn *net.UDPConn, conns *connections) {
count := 0
for {
count++
conns.mu.Lock()
// loop over known addresses
for _, retAddr := range conns.addrs {
// send a message to them all
msg := fmt.Sprintf("Sent %d", count)
if _, err := conn.WriteToUDP([]byte(msg), retAddr); err != nil {
fmt.Printf("error encountered: %s", err.Error())
continue
}
}
conns.mu.Unlock()
time.Sleep(1 * time.Second)
}
}
- 创建一个名为
main.go
的文件,内容如下:
package main
import (
"fmt"
"net"
)
const addr = "localhost:8888"
func main() {
conns := &connections{
addrs: make(map[string]*net.UDPAddr),
}
fmt.Printf("serving on %s\n", addr)
// construct a udp addr
addr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
panic(err)
}
// listen on our specified addr
conn, err := net.ListenUDP("udp", addr)
if err != nil {
panic(err)
}
// cleanup
defer conn.Close()
// async send messages to all known clients
go broadcast(conn, conns)
msg := make([]byte, 1024)
for {
// receive a message to gather the ip address
// and port to send back to
_, retAddr, err := conn.ReadFromUDP(msg)
if err != nil {
continue
}
//store it in a map
conns.mu.Lock()
conns.addrs[retAddr.String()] = retAddr
conns.mu.Unlock()
fmt.Printf("%s connected\n", retAddr)
}
}
-
导航到上一个目录。
-
创建一个名为
client
的新目录,并导航到该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"fmt"
"net"
)
const addr = "localhost:8888"
func main() {
fmt.Printf("client for server url: %s\n", addr)
addr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
panic(err)
}
conn, err := net.DialUDP("udp", nil, addr)
if err != nil {
panic(err)
}
defer conn.Close()
msg := make([]byte, 512)
n, err := conn.Write([]byte("connected"))
if err != nil {
panic(err)
}
for {
n, err = conn.Read(msg)
if err != nil {
continue
}
fmt.Printf("%s\n", string(msg[:n]))
}
}
-
导航到上一个目录。
-
运行
go run ./server
,您将看到以下输出:
$ go run ./server
serving on localhost:8888
- 在另一个终端中,从
udp
目录运行go run ./client
,您将看到以下输出,尽管计数可能有所不同:
$ go run ./client
client for server url: localhost:8888
Sent 3
Sent 4
Sent 5
- 导航到运行服务器的终端,您应该看到类似以下的内容:
$ go run ./server
serving on localhost:8888
127.0.0.1:64242 connected
-
按下Ctrl + C退出服务器和客户端。
-
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
服务器正在监听端口8888
,就像在上一个示例中一样。如果客户端启动,它会向服务器发送一条消息,服务器会将其地址添加到地址列表中。因为客户端可以异步连接,所以服务器在修改或读取列表之前必须使用互斥锁。
一个单独的广播 Goroutine 独立运行,并将相同的消息发送到以前发送消息的所有客户端地址。假设它们仍在监听,它们将在大致相同的时间从服务器接收相同的消息。您还可以连接更多的客户端来查看这种效果。
使用域名解析
net
包提供了许多有用的 DNS 查找功能。这些信息与使用 Unix 的dig
命令获得的信息相似。这些信息对于您实现任何需要动态确定 IP 地址的网络编程非常有用。
本教程将探讨如何收集这些数据。为了演示这一点,我们将实现一个简化的dig
命令。我们将寻求将 URL 映射到其所有 IPv4 和 IPv6 地址。通过修改GODEBUG=netdns=
为go
或cgo
,它将使用纯 Go DNS 解析器或cgo
解析器。默认情况下,使用纯 Go DNS 解析器。
如何做...
以下步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter5/dns
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/dns
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/dns
-
从
~/projects/go-programming-cookbook-original/chapter5/dns
复制测试,或者使用此作为练习来编写一些您自己的代码! -
创建一个名为
dns.go
的文件,内容如下:
package dns
import (
"fmt"
"net"
"github.com/pkg/errors"
)
// Lookup holds the DNS information we care about
type Lookup struct {
cname string
hosts []string
}
// We can use this to print the lookup object
func (d *Lookup) String() string {
result := ""
for _, host := range d.hosts {
result += fmt.Sprintf("%s IN A %s\n", d.cname, host)
}
return result
}
// LookupAddress returns a DNSLookup consisting of a cname and host
// for a given address
func LookupAddress(address string) (*Lookup, error) {
cname, err := net.LookupCNAME(address)
if err != nil {
return nil, errors.Wrap(err, "error looking up CNAME")
}
hosts, err := net.LookupHost(address)
if err != nil {
return nil, errors.Wrap(err, "error looking up HOST")
}
return &Lookup{cname: cname, hosts: hosts}, nil
}
-
创建一个名为
example
的新目录,并导航到该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"fmt"
"log"
"os"
"github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/dns"
)
func main() {
if len(os.Args) < 2 {
fmt.Printf("Usage: %s <address>\n", os.Args[0])
os.Exit(1)
}
address := os.Args[1]
lookup, err := dns.LookupAddress(address)
if err != nil {
log.Panicf("failed to lookup: %s", err.Error())
}
fmt.Println(lookup)
}
-
运行
go run main.go golang.org
命令。 -
您还可以运行以下命令:
$ go build $ ./example golang.org
您应该看到以下输出:
$ go run main.go golang.org
golang.org. IN A 172.217.5.17
golang.org. IN A 2607:f8b0:4009:809::2011
-
go.mod
文件可能已更新,go.sum
文件现在应该存在于顶级配方目录中。 -
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
本教程执行了提供的地址的CNAME
和主机查找。在我们的案例中,我们使用了golang.org
。我们将结果存储在一个查找结构中,该结构使用String()
方法打印输出结果。当我们将对象打印为字符串时,将自动调用此方法,或者我们可以直接调用该方法。我们在main.go
中实现了一些基本的参数检查,以确保在运行程序时提供了地址。
使用 WebSockets
WebSockets 允许服务器应用程序连接到用 JavaScript 编写的基于 Web 的客户端。这使您可以创建具有双向通信的 Web 应用程序,并创建更新,例如聊天室等。
本教程将探讨如何在 Go 中编写 WebSocket 服务器,并演示客户端使用 WebSocket 服务器的过程。它使用github.com/gorilla/websocket
将标准处理程序升级为 WebSocket 处理程序,并创建客户端应用程序。
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter5/websocket
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/websocket
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/websocket
-
从
~/projects/go-programming-cookbook-original/chapter5/websocket
复制测试,或者使用此作为练习来编写一些您自己的代码! -
创建一个名为
server
的新目录,并导航到该目录。 -
创建一个名为
handler.go
的文件,内容如下:
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
// upgrader takes an http connection and converts it
// to a websocket one, we're using some recommended
// basic buffer sizes
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
func wsHandler(w http.ResponseWriter, r *http.Request) {
// upgrade the connection
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("failed to upgrade connection: ", err)
return
}
for {
// read and echo back messages in a loop
messageType, p, err := conn.ReadMessage()
if err != nil {
log.Println("failed to read message: ", err)
return
}
log.Printf("received from client: %#v", string(p))
if err := conn.WriteMessage(messageType, p); err != nil {
log.Println("failed to write message: ", err)
return
}
}
}
- 创建一个名为
main.go
的文件,内容如下:
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
fmt.Println("Listening on port :8000")
// we mount our single handler on port localhost:8000 to handle all
// requests
log.Panic(http.ListenAndServe("localhost:8000", http.HandlerFunc(wsHandler)))
}
-
导航到上一个目录。
-
创建一个名为
client
的新目录,并导航到该目录。 -
创建一个名为
process.go
的文件,内容如下:
package main
import (
"bufio"
"fmt"
"log"
"os"
"strings"
"github.com/gorilla/websocket"
)
func process(c *websocket.Conn) {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Printf("Enter some text: ")
// this will block ctrl-c, to exit press it then hit enter
// or kill from another location
data, err := reader.ReadString('\n')
if err != nil {
log.Println("failed to read stdin", err)
}
// trim off the space from reading the string
data = strings.TrimSpace(data)
// write the message as a byte across the websocket
err = c.WriteMessage(websocket.TextMessage, []byte(data))
if err != nil {
log.Println("failed to write message:", err)
return
}
// this is an echo server, so we can always read after the write
_, message, err := c.ReadMessage()
if err != nil {
log.Println("failed to read:", err)
return
}
log.Printf("received back from server: %#v\n", string(message))
}
}
- 创建一个名为
main.go
的文件,内容如下:
package main
import (
"log"
"os"
"os/signal"
"github.com/gorilla/websocket"
)
// catchSig cleans up our websocket conenction if we kill the program
// with a ctrl-c
func catchSig(ch chan os.Signal, c *websocket.Conn) {
// block on waiting for a signal
<-ch
err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
if err != nil {
log.Println("write close:", err)
}
return
}
func main() {
// connect the os signal to our channel
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
// use the ws:// Scheme to connect to the websocket
u := "ws://localhost:8000/"
log.Printf("connecting to %s", u)
c, _, err := websocket.DefaultDialer.Dial(u, nil)
if err != nil {
log.Fatal("dial:", err)
}
defer c.Close()
// dispatch our signal catcher
go catchSig(interrupt, c)
process(c)
}
-
导航到上一个目录。
-
运行
go run ./server
,您将看到以下输出:
$ go run ./server
Listening on port :8000
- 在另一个终端中,从
websocket
目录运行go run ./client
,您将看到以下输出:
$ go run ./client
2019/05/26 11:53:20 connecting to ws://localhost:8000/
Enter some text:
- 输入
test
字符串,您应该看到以下内容:
$ go run ./client
2019/05/26 11:53:20 connecting to ws://localhost:8000/
Enter some text: test
2019/05/26 11:53:22 received back from server: "test"
Enter some text:
- 导航到运行服务器的终端,您应该看到类似以下内容的内容:
$ go run ./server
Listening on port :8000
2019/05/26 11:53:22 received from client: "test"
-
按下Ctrl + C退出服务器和客户端。在按下Ctrl + C后,您可能还需要按Enter。
-
go.mod
文件可能已更新,go.sum
文件现在应该存在于顶级配方目录中。 -
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
服务器正在端口8000
上监听 WebSocket 连接。当请求到来时,github.com/gorilla/websocket
包用于将请求升级为 WebSocket 连接。与先前的回声服务器示例类似,服务器等待在 WebSocket 连接上接收消息,并将相同的消息作为响应发送回客户端。因为它是一个处理程序,所以它可以异步处理许多 WebSocket 连接,并且它们将保持连接,直到客户端终止。
在客户端中,我们添加了一个catchsig
函数来处理Ctrl + C事件。这使我们能够在客户端退出时清楚地终止与服务器的连接。否则,客户端只是在STDIN
上接受用户输入并将其发送到服务器,记录响应,然后重复。
使用 net/rpc 调用远程方法
Go 通过net/rpc
包为您的系统提供基本的 RPC 功能。这是在不依赖于 GRPC 或其他更复杂的 RPC 包的情况下进行 RPC 调用的潜在替代方案。但是,它的功能相当有限,您可能希望导出的任何函数都必须符合非常特定的函数签名。
代码中的注释指出了一些可以远程调用的方法的限制。本配方演示了如何创建一个共享函数,该函数通过结构传递了许多参数,并且可以远程调用。
如何做...
这些步骤涵盖了编写和运行应用程序的过程:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter5/rpc
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/rpc
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/rpc
-
从
~/projects/go-programming-cookbook-original/chapter5/rpc
复制测试,或者利用这个机会编写一些您自己的代码! -
创建一个名为
tweak
的新目录,并导航到该目录。 -
创建一个名为
tweak.go
的文件,内容如下:
package tweak
import (
"strings"
)
// StringTweaker is a type of string
// that can reverse itself
type StringTweaker struct{}
// Args are a list of options for how to tweak
// the string
type Args struct {
String string
ToUpper bool
Reverse bool
}
// Tweak conforms to the RPC library which require:
// - the method's type is exported.
// - the method is exported.
// - the method has two arguments, both exported (or builtin) types.
// - the method's second argument is a pointer.
// - the method has return type error.
func (s StringTweaker) Tweak(args *Args, resp *string) error {
result := string(args.String)
if args.ToUpper {
result = strings.ToUpper(result)
}
if args.Reverse {
runes := []rune(result)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
result = string(runes)
}
*resp = result
return nil
}
-
导航到上一个目录。
-
创建一个名为
server
的新目录,并导航到该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"fmt"
"log"
"net"
"net/http"
"net/rpc"
"github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/rpc/tweak"
)
func main() {
s := new(tweak.StringTweaker)
if err := rpc.Register(s); err != nil {
log.Fatal("failed to register:", err)
}
rpc.HandleHTTP()
l, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal("listen error:", err)
}
fmt.Println("listening on :1234")
log.Panic(http.Serve(l, nil))
}
-
导航到上一个目录。
-
创建一个名为
client
的新目录,并导航到该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"fmt"
"log"
"net/rpc"
"github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/rpc/tweak"
)
func main() {
client, err := rpc.DialHTTP("tcp", "localhost:1234")
if err != nil {
log.Fatal("error dialing:", err)
}
args := tweak.Args{
String: "this string should be uppercase and reversed",
ToUpper: true,
Reverse: true,
}
var result string
err = client.Call("StringTweaker.Tweak", args, &result)
if err != nil {
log.Fatal("client call with error:", err)
}
fmt.Printf("the result is: %s", result)
}
-
导航到上一个目录。
-
运行
go run ./server
,您将看到以下输出:
$ go run ./server
Listening on :1234
- 在单独的终端中,从
rpc
目录运行go run ./client
,您将看到以下输出:
$ go run ./client
the result is: DESREVER DNA ESACREPPU EB DLUOHS GNIRTS SIHT
-
按Ctrl + C退出服务器。
-
如果您复制或编写了自己的测试,请返回上一个目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
StringTweaker
结构被放入一个单独的库中,以便客户端(用于设置参数)和服务器(用于注册 RPC 和启动服务器)可以访问其导出类型。它还符合本配方开头提到的规则,以便与net/rpc
一起使用。
StringTweaker
可用于接受输入字符串,并根据传递的选项,可选地反转和大写其中包含的所有字符。这种模式可以扩展为创建更复杂的函数,并且您还可以使用额外的函数使代码在增长时更易读。
使用 net/mail 解析电子邮件
net/mail
包提供了许多有用的函数,可在处理电子邮件时帮助您。如果您有电子邮件的原始文本,可以将其解析为提取标题、发送日期信息等。本配方将通过解析硬编码为字符串的原始电子邮件来演示其中的一些功能。
如何做...
这些步骤涵盖了编写和运行应用程序的过程:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter5/mail
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/mail
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/mail
-
从
~/projects/go-programming-cookbook-original/chapter5/mail
复制测试,或者利用这个机会编写一些自己的代码! -
创建一个名为
header.go
的文件,内容如下:
package main
import (
"fmt"
"net/mail"
"strings"
)
// extract header info and print it nicely
func printHeaderInfo(header mail.Header) {
// this works because we know it's a single address
// otherwise use ParseAddressList
toAddress, err := mail.ParseAddress(header.Get("To"))
if err == nil {
fmt.Printf("To: %s <%s>\n", toAddress.Name, toAddress.Address)
}
fromAddress, err := mail.ParseAddress(header.Get("From"))
if err == nil {
fmt.Printf("From: %s <%s>\n", fromAddress.Name,
fromAddress.Address)
}
fmt.Println("Subject:", header.Get("Subject"))
// this works for a valid RFC5322 date
// it does a header.Get("Date"), then a
// mail.ParseDate(that_result)
if date, err := header.Date(); err == nil {
fmt.Println("Date:", date)
}
fmt.Println(strings.Repeat("=", 40))
fmt.Println()
}
- 创建一个名为
main.go
的文件,内容如下:
package main
import (
"io"
"log"
"net/mail"
"os"
"strings"
)
// an example email message
const msg string = `Date: Thu, 24 Jul 2019 08:00:00 -0700
From: Aaron <fake_sender@example.com>
To: Reader <fake_receiver@example.com>
Subject: Gophercon 2019 is going to be awesome!
Feel free to share my book with others if you're attending.
This recipe can be used to process and parse email information.
`
func main() {
r := strings.NewReader(msg)
m, err := mail.ReadMessage(r)
if err != nil {
log.Fatal(err)
}
printHeaderInfo(m.Header)
// after printing the header, dump the body to stdout
if _, err := io.Copy(os.Stdout, m.Body); err != nil {
log.Fatal(err)
}
}
-
运行
go run .
命令。 -
您也可以运行以下内容:
$ go build $ ./mail
您应该看到以下输出:
$ go run .
To: Reader <fake_receiver@example.com>
From: Aaron <fake_sender@example.com>
Subject: Gophercon 2019 is going to be awesome!
Date: 2019-07-24 08:00:00 -0700 -0700
========================================
Feel free to share my book with others if you're attending.
This recipe can be used to process and parse email information.
- 如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
printHeaderInfo
函数为此示例大部分工作。它将地址从标题中解析为 *mail.Address
结构,并将日期标题解析为日期对象。然后,它将消息中的所有信息格式化为可读格式。主函数解析初始电子邮件并传递此标题。
第六章:关于数据库和存储的一切
Go 应用程序经常需要使用长期存储。这通常以关系和非关系数据库的形式存在,以及键值存储等。在处理这些存储应用程序时,将操作封装在接口中是有帮助的。本章的配方将检查各种存储接口,考虑诸如连接池等并行访问的问题,并查看集成新库的一般提示,这在使用新的存储技术时经常发生。
在本章中,将涵盖以下配方:
-
使用 database/sql 包与 MySQL
-
执行数据库事务接口
-
连接池、速率限制和 SQL 的超时
-
使用 Redis
-
使用 MongoDB 的 NoSQL
-
创建数据可移植性的存储接口
使用 database/sql 包与 MySQL
关系数据库是一些最为人熟知和常见的数据库选项。MySQL 和 PostgreSQL 是两种最流行的开源关系数据库。这个配方将演示database/sql
包,它提供了一些关系数据库的钩子,并自动处理连接池和连接持续时间,并提供了一些基本的数据库操作。
这个配方将使用 MySQL 数据库建立连接,插入一些简单的数据并查询它。它将在使用后通过删除表来清理数据库。
准备工作
根据以下步骤配置你的环境:
-
在你的操作系统上下载并安装 Go 1.12.6 或更高版本,网址为
golang.org/doc/install
。 -
打开一个终端或控制台应用程序,创建一个项目目录,比如
~/projects/go-programming-cookbook
,并导航到该目录。所有的代码都将在这个目录中运行和修改。 -
将最新的代码克隆到
~/projects/go-programming-cookbook-original
,并选择从该目录工作,而不是手动输入示例。
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
-
使用
dev.mysql.com/doc/mysql-getting-started/en/
安装和配置 MySQL。 -
运行
export MYSQLUSERNAME=<your mysql username>
命令。 -
运行
export MYSQLPASSWORD=<your mysql password>
命令。
如何做...
这些步骤涵盖了编写和运行应用程序的过程:
-
从你的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter6/database
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/database
你应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/database
-
从
~/projects/go-programming-cookbook-original/chapter6/database
复制测试,或者利用这个练习编写一些自己的代码! -
创建一个名为
config.go
的文件,内容如下:
package database
import (
"database/sql"
"fmt"
"os"
"time"
_ "github.com/go-sql-driver/mysql" //we import supported
libraries for database/sql
)
// Example hold the results of our queries
type Example struct {
Name string
Created *time.Time
}
// Setup configures and returns our database
// connection poold
func Setup() (*sql.DB, error) {
db, err := sql.Open("mysql",
fmt.Sprintf("%s:%s@/gocookbook?
parseTime=true", os.Getenv("MYSQLUSERNAME"),
os.Getenv("MYSQLPASSWORD")))
if err != nil {
return nil, err
}
return db, nil
}
- 创建一个名为
create.go
的文件,内容如下:
package database
import (
"database/sql"
_ "github.com/go-sql-driver/mysql" //we import supported
libraries for database/sql
)
// Create makes a table called example
// and populates it
func Create(db *sql.DB) error {
// create the database
if _, err := db.Exec("CREATE TABLE example (name
VARCHAR(20), created DATETIME)"); err != nil {
return err
}
if _, err := db.Exec(`INSERT INTO example (name, created)
values ("Aaron", NOW())`); err != nil {
return err
}
return nil
}
- 创建一个名为
query.go
的文件,内容如下:
package database
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql" //we import supported
libraries for database/sql
)
// Query grabs a new connection
// creates tables, and later drops them
// and issues some queries
func Query(db *sql.DB, name string) error {
name := "Aaron"
rows, err := db.Query("SELECT name, created FROM example
where name=?", name)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var e Example
if err := rows.Scan(&e.Name, &e.Created); err != nil {
return err
}
fmt.Printf("Results:\n\tName: %s\n\tCreated: %v\n",
e.Name, e.Created)
}
return rows.Err()
}
- 创建一个名为
exec.go
的文件,内容如下:
package database
// Exec replaces the Exec from the previous
// recipe
func Exec(db DB) error {
// uncaught error on cleanup, but we always
// want to cleanup
defer db.Exec("DROP TABLE example")
if err := Create(db); err != nil {
return err
}
if err := Query(db, "Aaron"); err != nil {
return err
}
return nil
}
-
创建并导航到
example
目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"PacktPublishing/Go-Programming-Cookbook-Second-Edition/
go-cookbook/chapter6/database"
_ "github.com/go-sql-driver/mysql" //we import supported
libraries for database/sql
)
func main() {
db, err := database.Setup()
if err != nil {
panic(err)
}
if err := database.Exec(db); err != nil {
panic(err)
}
}
-
运行
go run main.go
。 -
你也可以运行以下命令:
$ go build $ ./example
你应该看到以下输出:
$ go run main.go
Results:
Name: Aaron
Created: 2017-02-16 19:02:36 +0000 UTC
-
go.mod
文件可能会被更新,顶层配方目录中现在应该存在go.sum
文件。 -
如果你复制或编写了自己的测试,返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
代码中的_ "github.com/go-sql-driver/mysql"
行是将各种数据库连接器连接到database/sql
包的方法。还有其他可以以类似方式导入的 MySQL 包,以获得类似的结果。如果你要连接到 PostgreSQL、SQLite 或其他实现了database/sql
接口的数据库,命令也会类似。
一旦连接,该包将设置一个连接池,该连接池在SQL 的连接池、速率限制和超时配方中有所涵盖,您可以直接在连接上执行 SQL,也可以创建可以使用commit
和rollback
命令执行所有连接操作的事务对象。
当与数据库通信时,mysql
包为 Go 时间对象提供了一些便利支持。这个配方还从MYSQLUSERNAME
和MYSQLPASSWORD
环境变量中检索用户名和密码。
执行数据库事务接口
在与数据库等服务的连接工作时,编写测试可能会很困难。这是因为在 Go 中很难在运行时模拟或鸭子类型化。虽然我建议在处理数据库时使用存储接口,但在这个接口内部模拟数据库事务接口仍然很有用。为数据可移植性创建存储接口配方将涵盖存储接口;这个配方将专注于包装数据库连接和事务对象的接口。
为了展示这样一个接口的使用,我们将重写前一个配方中的创建和查询文件以使用我们的接口。最终输出将是相同的,但创建和查询操作将都在一个事务中执行。
准备工作
参考使用 database/sql 包与 MySQL配方中的准备工作部分。
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter6/dbinterface
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/dbinterface
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/dbinterface
-
从
~/projects/go-programming-cookbook-original/chapter6/dbinterface
复制测试,或者利用这个机会编写一些自己的代码! -
创建一个名为
transaction.go
的文件,内容如下:
package dbinterface
import "database/sql"
// DB is an interface that is satisfied
// by an sql.DB or an sql.Transaction
type DB interface {
Exec(query string, args ...interface{}) (sql.Result, error)
Prepare(query string) (*sql.Stmt, error)
Query(query string, args ...interface{}) (*sql.Rows, error)
QueryRow(query string, args ...interface{}) *sql.Row
}
// Transaction can do anything a Query can do
// plus Commit, Rollback, or Stmt
type Transaction interface {
DB
Commit() error
Rollback() error
}
- 创建一个名为
create.go
的文件,内容如下:
package dbinterface
import _ "github.com/go-sql-driver/mysql" //we import supported libraries for database/sql
// Create makes a table called example
// and populates it
func Create(db DB) error {
// create the database
if _, err := db.Exec("CREATE TABLE example (name VARCHAR(20), created DATETIME)"); err != nil {
return err
}
if _, err := db.Exec(`INSERT INTO example (name, created) values ("Aaron", NOW())`); err != nil {
return err
}
return nil
}
- 创建一个名为
query.go
的文件,内容如下:
package dbinterface
import (
"fmt"
"github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/database"
)
// Query grabs a new connection
// creates tables, and later drops them
// and issues some queries
func Query(db DB) error {
name := "Aaron"
rows, err := db.Query("SELECT name, created FROM example where name=?", name)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var e database.Example
if err := rows.Scan(&e.Name, &e.Created); err != nil {
return err
}
fmt.Printf("Results:\n\tName: %s\n\tCreated: %v\n", e.Name,
e.Created)
}
return rows.Err()
}
- 创建一个名为
exec.go
的文件,内容如下:
package dbinterface
// Exec replaces the Exec from the previous
// recipe
func Exec(db DB) error {
// uncaught error on cleanup, but we always
// want to cleanup
defer db.Exec("DROP TABLE example")
if err := Create(db); err != nil {
return err
}
if err := Query(db); err != nil {
return err
}
return nil
}
-
导航到
example
。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/database"
"github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/dbinterface"
_ "github.com/go-sql-driver/mysql" //we import supported libraries for database/sql
)
func main() {
db, err := database.Setup()
if err != nil {
panic(err)
}
tx, err := db.Begin()
if err != nil {
panic(err)
}
// this wont do anything if commit is successful
defer tx.Rollback()
if err := dbinterface.Exec(tx); err != nil {
panic(err)
}
if err := tx.Commit(); err != nil {
panic(err)
-
运行
go run main.go
。 -
您也可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
Results:
Name: Aaron
Created: 2017-02-16 20:00:00 +0000 UTC
-
go.mod
文件可能会被更新,顶级配方目录中现在应该存在go.sum
文件。 -
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
这个配方的工作方式与前一个数据库配方使用 database/sql 包与 MySQL非常相似。这个配方执行了创建数据和查询数据的相同操作,但也演示了使用事务和创建通用数据库函数,这些函数可以与sql.DB
连接和sql.Transaction
对象一起使用。
以这种方式编写的代码允许我们重用执行数据库操作的函数,这些函数可以单独运行或在事务中运行。这样可以实现更多的代码重用,同时仍然将功能隔离到在数据库上操作的函数或方法中。例如,您可以为多个表格编写Update(db DB)
函数,并将它们全部传递给一个共享的事务,以原子方式执行多个更新。这样也更容易模拟这些接口,正如您将在第九章中看到的,测试 Go 代码。
SQL 的连接池、速率限制和超时
虽然database/sql
包提供了连接池、速率限制和超时的支持,但通常需要调整默认值以更好地适应数据库配置。当您在微服务上进行水平扩展并且不希望保持太多活动连接到数据库时,这一点就变得很重要。
准备工作
参考使用 database/sql 包与 MySQL配方中的准备工作部分。
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter6/pools
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/pools
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/pools
-
从
~/projects/go-programming-cookbook-original/chapter6/pools
复制测试,或者利用这个练习编写一些自己的代码! -
创建一个名为
pools.go
的文件,并包含以下内容:
package pools
import (
"database/sql"
"fmt"
"os"
_ "github.com/go-sql-driver/mysql" //we import supported
libraries for database/sql
)
// Setup configures the db along with pools
// number of connections and more
func Setup() (*sql.DB, error) {
db, err := sql.Open("mysql",
fmt.Sprintf("%s:%s@/gocookbook?
parseTime=true", os.Getenv("MYSQLUSERNAME"),
os.Getenv("MYSQLPASSWORD")))
if err != nil {
return nil, err
}
// there will only ever be 24 open connections
db.SetMaxOpenConns(24)
// MaxIdleConns can never be less than max open
// SetMaxOpenConns otherwise it'll default to that value
db.SetMaxIdleConns(24)
return db, nil
}
- 创建一个名为
timeout.go
的文件,并包含以下内容:
package pools
import (
"context"
"time"
)
// ExecWithTimeout will timeout trying
// to get the current time
func ExecWithTimeout() error {
db, err := Setup()
if err != nil {
return err
}
ctx := context.Background()
// we want to timeout immediately
ctx, cancel := context.WithDeadline(ctx, time.Now())
// call cancel after we complete
defer cancel()
// our transaction is context aware
_, err = db.BeginTx(ctx, nil)
return err
}
-
导航到
example
。 -
创建一个名为
main.go
的文件,并包含以下内容:
package main
import "PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
go-cookbook/chapter6/pools"
func main() {
if err := pools.ExecWithTimeout(); err != nil {
panic(err)
}
}
-
运行
go run main.go
。 -
您也可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
panic: context deadline exceeded
goroutine 1 [running]:
main.main()
/go/src/PacktPublishing/Go-Programming-Cookbook-Second-
Edition/go-cookbook/chapter6/pools/example/main.go:7 +0x4e
exit status 2
-
go.mod
文件可能会被更新,顶级示例目录中现在应该存在go.sum
文件。 -
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
能够控制连接池的深度非常有用。这将防止我们过载数据库,但重要的是要考虑在超时的情况下会发生什么。如果您同时强制执行一组连接和严格基于上下文的超时,就像我们在这个示例中所做的那样,将会有一些情况下,您会发现请求经常在尝试建立太多连接的过载应用程序上超时。
这是因为连接将超时等待连接可用。对于database/sql
的新添加的上下文功能使得为整个请求设置共享超时变得更加简单,包括执行查询所涉及的步骤。
通过这个和其他的示例,使用一个全局的config
对象传递给Setup()
函数是有意义的,尽管这个示例只是使用环境变量。
使用 Redis
有时您需要持久存储或第三方库和服务提供的附加功能。这个示例将探讨 Redis 作为非关系型数据存储的形式,并展示 Go 语言如何与这些第三方服务进行交互。
由于 Redis 支持具有简单接口的键值存储,因此它是会话存储或具有持续时间的临时数据的绝佳候选者。在 Redis 中指定数据的超时是非常有价值的。这个示例将探讨从配置到查询再到使用自定义排序的基本 Redis 用法。
准备工作
根据以下步骤配置您的环境:
-
在您的操作系统上下载并安装 Go 1.11.1 或更高版本,网址为
golang.org/doc/install
。 -
打开一个终端或控制台应用程序,并创建并导航到一个项目目录,例如
~/projects/go-programming-cookbook
。所有的代码都将在这个目录中运行和修改。 -
将最新的代码克隆到
~/projects/go-programming-cookbook-original
,然后(可选)从该目录中工作,而不是手动输入示例:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
- 使用
redis.io/topics/quickstart
安装和配置 Redis。
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter6/redis
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/redis
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/redis
-
从
~/projects/go-programming-cookbook-original/chapter6/redis
复制测试,或者利用这个练习编写一些自己的代码! -
创建一个名为
config.go
的文件,并包含以下内容:
package redis
import (
"os"
redis "gopkg.in/redis.v5"
)
// Setup initializes a redis client
func Setup() (*redis.Client, error) {
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: os.Getenv("REDISPASSWORD"),
DB: 0, // use default DB
})
_, err := client.Ping().Result()
return client, err
}
- 创建一个名为
exec.go
的文件,并包含以下内容:
package redis
import (
"fmt"
"time"
redis "gopkg.in/redis.v5"
)
// Exec performs some redis operations
func Exec() error {
conn, err := Setup()
if err != nil {
return err
}
c1 := "value"
// value is an interface, we can store whatever
// the last argument is the redis expiration
conn.Set("key", c1, 5*time.Second)
var result string
if err := conn.Get("key").Scan(&result); err != nil {
switch err {
// this means the key
// was not found
case redis.Nil:
return nil
default:
return err
}
}
fmt.Println("result =", result)
return nil
}
- 创建一个名为
sort.go
的文件,并包含以下内容:
package redis
import (
"fmt"
redis "gopkg.in/redis.v5"
)
// Sort performs a sort redis operations
func Sort() error {
conn, err := Setup()
if err != nil {
return err
}
listkey := "list"
if err := conn.LPush(listkey, 1).Err(); err != nil {
return err
}
// this will clean up the list key if any of the subsequent commands error
defer conn.Del(listkey)
if err := conn.LPush(listkey, 3).Err(); err != nil {
return err
}
if err := conn.LPush(listkey, 2).Err(); err != nil {
return err
}
res, err := conn.Sort(listkey, redis.Sort{Order: "ASC"}).Result()
if err != nil {
return err
}
fmt.Println(res)
return nil
}
-
导航到
example
。 -
创建一个名为
main.go
的文件,内容如下:
package main
import "PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
go-cookbook/chapter6/redis"
func main() {
if err := redis.Exec(); err != nil {
panic(err)
}
if err := redis.Sort(); err != nil {
panic(err)
}
}
-
运行
go run main.go
。 -
您也可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
result = value
[1 2 3]
-
go.mod
文件可能已更新,顶级配方目录中现在应该存在go.sum
文件。 -
如果您复制或编写了自己的测试,请返回上一个目录并运行
go test
。确保所有测试都通过。
工作原理...
在 Go 中使用 Redis 与使用 MySQL 非常相似。虽然没有标准库,但是许多相同的约定都遵循了,例如使用Scan()
函数将数据从 Redis 读取到 Go 类型中。在这种情况下,选择最佳库可能会有挑战,我建议定期调查可用的内容,因为事情可能会迅速改变。
这个示例使用redis
包来进行基本的设置和获取,更复杂的排序功能以及基本的配置。与database/sql
一样,您可以以写超时、池大小等形式设置额外的配置。Redis 本身还提供了许多额外的功能,包括 Redis 集群支持、Zscore 和计数器对象以及分布式锁。
与前面的示例一样,我建议使用一个config
对象,它存储您的 Redis 设置和配置详细信息,以便轻松设置和安全性。
使用 NoSQL 与 MongoDB
您可能最初认为 Go 更适合关系数据库,因为 Go 结构和 Go 是一种类型化的语言。当使用github.com/mongodb/mongo-go-driver
包时,Go 可以几乎任意存储和检索结构对象。如果对对象进行版本控制,您的模式可以适应,并且可以提供一个非常灵活的开发环境。
有些库更擅长隐藏或提升这些抽象。mongo-go-driver
包就是一个很好的例子。下面的示例将以类似的方式创建一个连接,类似于 Redis 和 MySQL,但将存储和检索对象而无需定义具体的模式。
准备工作
根据以下步骤配置您的环境:
-
在您的操作系统上下载并安装 Go 1.11.1 或更高版本,网址为
golang.org/doc/install
。 -
打开一个终端或控制台应用程序,并创建并导航到一个项目目录,例如
~/projects/go-programming-cookbook
。所有的代码都将在这个目录中运行和修改。 -
将最新的代码克隆到
~/projects/go-programming-cookbook-original
,(可选)从该目录中工作,而不是手动输入示例:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
- 安装和配置 MongoDB(
docs.mongodb.com/getting-started/shell/
)。
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter6/mongodb
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/mongodb
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/mongodb
-
从
~/projects/go-programming-cookbook-original/chapter6/mongodb
复制测试,或者利用这个机会编写一些自己的代码! -
创建一个名为
config.go
的文件,内容如下:
package mongodb
import (
"context"
"time"
"github.com/mongodb/mongo-go-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// Setup initializes a mongo client
func Setup(ctx context.Context, address string) (*mongo.Client, error) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
// cancel will be called when setup exits
defer cancel()
client, err := mongo.NewClient(options.Client().ApplyURI(address))
if err != nil {
return nil, err
}
if err := client.Connect(ctx); err != nil {
return nil, err
}
return client, nil
}
- 创建一个名为
exec.go
的文件,内容如下:
package mongodb
import (
"context"
"fmt"
"github.com/mongodb/mongo-go-driver/bson"
)
// State is our data model
type State struct {
Name string `bson:"name"`
Population int `bson:"pop"`
}
// Exec creates then queries an Example
func Exec(address string) error {
ctx := context.Background()
db, err := Setup(ctx, address)
if err != nil {
return err
}
coll := db.Database("gocookbook").Collection("example")
vals := []interface{}{&State{"Washington", 7062000}, &State{"Oregon", 3970000}}
// we can inserts many rows at once
if _, err := coll.InsertMany(ctx, vals); err != nil {
return err
}
var s State
if err := coll.FindOne(ctx, bson.M{"name": "Washington"}).Decode(&s); err != nil {
return err
}
if err := coll.Drop(ctx); err != nil {
return err
}
fmt.Printf("State: %#v\n", s)
return nil
}
-
导航到
example
。 -
创建一个名为
main.go
的文件,内容如下:
package main
import "github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/mongodb"
func main() {
if err := mongodb.Exec("mongodb://localhost"); err != nil {
panic(err)
}
}
-
运行
go run main.go
。 -
您也可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
State: mongodb.State{Name:"Washington", Population:7062000}
-
go.mod
文件可能已更新,顶级配方目录中现在应该存在go.sum
文件。 -
如果您复制或编写了自己的测试,请返回上一个目录并运行
go test
。确保所有测试都通过。
工作原理...
mongo-go-driver
包还提供连接池,并且有许多方法可以调整和配置与mongodb
数据库的连接。本示例的示例相当基本,但它们说明了理解和查询基于文档的数据库是多么容易。该包实现了 BSON 数据类型,与处理 JSON 非常相似。
mongodb
的一致性保证和最佳实践超出了本书的范围。然而,在 Go 语言中使用这些功能是一种乐趣。
为数据可移植性创建存储接口
在使用外部存储接口时,将操作抽象化到接口后面可能会有所帮助。这是为了方便模拟,如果更改存储后端,则可移植性,以及关注点的隔离。这种方法的缺点可能在于,如果您需要在事务内执行多个操作,那么最好是进行组合操作,或者允许通过上下文对象或附加函数参数传递它们。
此示例将实现一个非常简单的接口,用于在 MongoDB 中处理项目。这些项目将具有名称和价格,我们将使用接口来持久化和检索这些对象。
准备工作
请参考使用 NoSQL 与 MongoDB中准备工作部分中给出的步骤。
如何做...
这些步骤涵盖了编写和运行应用程序的过程:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter6/storage
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/storage
您应该会看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/storage
-
从
~/projects/go-programming-cookbook-original/chapter6/storage
复制测试,或者利用这个机会编写一些自己的代码! -
创建一个名为
storage.go
的文件,内容如下:
package storage
import "context"
// Item represents an item at
// a shop
type Item struct {
Name string
Price int64
}
// Storage is our storage interface
// We'll implement it with Mongo
// storage
type Storage interface {
GetByName(context.Context, string) (*Item, error)
Put(context.Context, *Item) error
}
- 创建一个名为
mongoconfig.go
的文件,内容如下:
package storage
import (
"context"
"time"
"github.com/mongodb/mongo-go-driver/mongo"
)
// MongoStorage implements our storage interface
type MongoStorage struct {
*mongo.Client
DB string
Collection string
}
// NewMongoStorage initializes a MongoStorage
func NewMongoStorage(ctx context.Context, connection, db, collection string) (*MongoStorage, error) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
client, err := mongo.Connect(ctx, "mongodb://localhost")
if err != nil {
return nil, err
}
ms := MongoStorage{
Client: client,
DB: db,
Collection: collection,
}
return &ms, nil
}
- 创建一个名为
mongointerface.go
的文件,内容如下:
package storage
import (
"context"
"github.com/mongodb/mongo-go-driver/bson"
)
// GetByName queries mongodb for an item with
// the correct name
func (m *MongoStorage) GetByName(ctx context.Context, name string) (*Item, error) {
c := m.Client.Database(m.DB).Collection(m.Collection)
var i Item
if err := c.FindOne(ctx, bson.M{"name": name}).Decode(&i); err != nil {
return nil, err
}
return &i, nil
}
// Put adds an item to our mongo instance
func (m *MongoStorage) Put(ctx context.Context, i *Item) error {
c := m.Client.Database(m.DB).Collection(m.Collection)
_, err := c.InsertOne(ctx, i)
return err
}
- 创建一个名为
exec.go
的文件,内容如下:
package storage
import (
"context"
"fmt"
)
// Exec initializes storage, then performs operations
// using the storage interface
func Exec() error {
ctx := context.Background()
m, err := NewMongoStorage(ctx, "localhost", "gocookbook", "items")
if err != nil {
return err
}
if err := PerformOperations(m); err != nil {
return err
}
if err := m.Client.Database(m.DB).Collection(m.Collection).Drop(ctx); err != nil {
return err
}
return nil
}
// PerformOperations creates a candle item
// then gets it
func PerformOperations(s Storage) error {
ctx := context.Background()
i := Item{Name: "candles", Price: 100}
if err := s.Put(ctx, &i); err != nil {
return err
}
candles, err := s.GetByName(ctx, "candles")
if err != nil {
return err
}
fmt.Printf("Result: %#v\n", candles)
return nil
}
-
导航到
example
。 -
创建一个名为
main.go
的文件,内容如下:
package main
import "PacktPublishing/Go-Programming-Cookbook-Second-Edition/
go-cookbook/chapter6/storage"
func main() {
if err := storage.Exec(); err != nil {
panic(err)
}
}
-
运行
go run main.go
。 -
您还可以运行以下命令:
$ go build $ ./example
您应该会看到以下输出:
$ go run main.go
Result: &storage.Item{Name:"candles", Price:100}
-
go.mod
文件可能会被更新,顶级配方目录中现在应该存在go.sum
文件。 -
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
演示此示例最重要的函数是PerformOperations
。此函数将一个Storage
接口作为参数。这意味着我们可以动态替换底层存储,甚至无需修改此函数。例如,连接存储到单独的 API 以消费和修改它将是很简单的。
我们使用上下文来为这些接口添加额外的灵活性,并允许接口处理超时。将应用程序逻辑与底层存储分离提供了各种好处,但很难选择正确的划界线的位置,这将因应用程序而异。
第七章:Web 客户端和 API
使用 API 并编写 Web 客户端可能是一件棘手的事情。不同的 API 具有不同类型的授权、认证和协议。我们将探索http.Client
结构对象,使用 OAuth2 客户端和长期令牌存储,并最后使用 GRPC 和额外的 REST 接口。
在本章结束时,您应该知道如何与第三方或内部 API 进行交互,并且对于常见操作(如对 API 的异步请求)有一些模式。
在本章中,我们将涵盖以下的步骤:
-
初始化、存储和传递 http.Client 结构
-
为 REST API 编写客户端
-
执行并行和异步客户端请求
-
使用 OAuth2 客户端
-
实现 OAuth2 令牌存储接口
-
在添加功能和函数组合中包装客户端
-
理解 GRPC 客户端
-
使用 twitchtv/twirp 进行 RPC
技术要求
为了继续本章中的所有示例,根据以下步骤配置您的环境:
-
在您的操作系统上下载并安装 Go 1.12.6 或更高版本,网址为
golang.org/doc/install
。 -
打开终端或控制台应用程序,创建一个项目目录,例如
~/projects/go-programming-cookbook
,并导航到该目录。所有的代码都将在这个目录中运行和修改。 -
将最新的代码克隆到
~/projects/go-programming-cookbook-original
,并选择从该目录工作,而不是手动输入示例,如下所示:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
初始化、存储和传递 http.Client 结构
Go 的net/http
包为处理 HTTP API 公开了一个灵活的http.Client
结构。这个结构具有单独的传输功能,使得对请求进行短路、修改每个客户端操作的标头以及处理任何 REST 操作相对简单。创建客户端是一个非常常见的操作,这个示例将从工作和创建一个http.Client
对象的基础知识开始。
如何做...
这些步骤涵盖了编写和运行应用程序的步骤:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter7/client
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/client
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/client
-
从
~/projects/go-programming-cookbook-original/chapter7/client
复制测试,或者自己编写一些代码来练习! -
创建一个名为
client.go
的文件,内容如下:
package client
import (
"crypto/tls"
"net/http"
)
// Setup configures our client and redefines
// the global DefaultClient
func Setup(isSecure, nop bool) *http.Client {
c := http.DefaultClient
// Sometimes for testing, we want to
// turn off SSL verification
if !isSecure {
c.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: false,
},
}
}
if nop {
c.Transport = &NopTransport{}
}
http.DefaultClient = c
return c
}
// NopTransport is a No-Op Transport
type NopTransport struct {
}
// RoundTrip Implements RoundTripper interface
func (n *NopTransport) RoundTrip(*http.Request)
(*http.Response, error) {
// note this is an unitialized Response
// if you're looking at headers etc
return &http.Response{StatusCode: http.StatusTeapot}, nil
}
- 创建一个名为
exec.go
的文件,内容如下:
package client
import (
"fmt"
"net/http"
)
// DoOps takes a client, then fetches
// google.com
func DoOps(c *http.Client) error {
resp, err := c.Get("http://www.google.com")
if err != nil {
return err
}
fmt.Println("results of DoOps:", resp.StatusCode)
return nil
}
// DefaultGetGolang uses the default client
// to get golang.org
func DefaultGetGolang() error {
resp, err := http.Get("https://www.golang.org")
if err != nil {
return err
}
fmt.Println("results of DefaultGetGolang:",
resp.StatusCode)
return nil
}
- 创建一个名为
store.go
的文件,内容如下:
package client
import (
"fmt"
"net/http"
)
// Controller embeds an http.Client
// and uses it internally
type Controller struct {
*http.Client
}
// DoOps with a controller object
func (c *Controller) DoOps() error {
resp, err := c.Client.Get("http://www.google.com")
if err != nil {
return err
}
fmt.Println("results of client.DoOps", resp.StatusCode)
return nil
}
-
创建一个名为
example
的新目录并导航到该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import "github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter7/client"
func main() {
// secure and op!
cli := client.Setup(true, false)
if err := client.DefaultGetGolang(); err != nil {
panic(err)
}
if err := client.DoOps(cli); err != nil {
panic(err)
}
c := client.Controller{Client: cli}
if err := c.DoOps(); err != nil {
panic(err)
}
// secure and noop
// also modifies default
client.Setup(true, true)
if err := client.DefaultGetGolang(); err != nil {
panic(err)
}
}
-
运行
go run main.go
。 -
您也可以运行以下命令:
$ go build $ ./example
现在您应该看到以下输出:
$ go run main.go
results of DefaultGetGolang: 200
results of DoOps: 200
results of client.DoOps 200
results of DefaultGetGolang: 418
- 如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
net/http
包公开了一个DefaultClient
包变量,该变量被以下内部操作使用:Do
、GET
、POST
等。我们的Setup()
函数返回一个客户端,并将默认客户端设置为相同的客户端。在设置客户端时,大部分修改将发生在传输中,传输只需要实现RoundTripper
接口。
这个示例提供了一个总是返回 418 状态码的无操作往返器的示例。您可以想象这对于测试可能有多么有用。它还演示了将客户端作为函数参数传递,将它们用作结构参数,并使用默认客户端来处理请求。
为 REST API 编写客户端
为 REST API 编写客户端不仅有助于更好地理解相关的 API,还将为所有将来使用该 API 的应用程序提供一个有用的工具。这个配方将探讨构建客户端的结构,并展示一些您可以立即利用的策略。
对于这个客户端,我们将假设认证是由基本认证处理的,但也应该可以命中一个端点来检索令牌等。为了简单起见,我们假设我们的 API 公开了一个端点GetGoogle()
,它返回从www.google.com
进行GET
请求返回的状态码。
如何做...
这些步骤涵盖了编写和运行应用程序的过程:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter7/rest
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/rest
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/rest
-
从
~/projects/go-programming-cookbook-original/chapter7/rest
复制测试,或者利用这个机会编写一些自己的代码! -
创建一个名为
client.go
的文件,内容如下:
package rest
import "net/http"
// APIClient is our custom client
type APIClient struct {
*http.Client
}
// NewAPIClient constructor initializes the client with our
// custom Transport
func NewAPIClient(username, password string) *APIClient {
t := http.Transport{}
return &APIClient{
Client: &http.Client{
Transport: &APITransport{
Transport: &t,
username: username,
password: password,
},
},
}
}
// GetGoogle is an API Call - we abstract away
// the REST aspects
func (c *APIClient) GetGoogle() (int, error) {
resp, err := c.Get("http://www.google.com")
if err != nil {
return 0, err
}
return resp.StatusCode, nil
}
- 创建一个名为
transport.go
的文件,内容如下:
package rest
import "net/http"
// APITransport does a SetBasicAuth
// for every request
type APITransport struct {
*http.Transport
username, password string
}
// RoundTrip does the basic auth before deferring to the
// default transport
func (t *APITransport) RoundTrip(req *http.Request)
(*http.Response, error) {
req.SetBasicAuth(t.username, t.password)
return t.Transport.RoundTrip(req)
}
- 创建一个名为
exec.go
的文件,内容如下:
package rest
import "fmt"
// Exec creates an API Client and uses its
// GetGoogle method, then prints the result
func Exec() error {
c := NewAPIClient("username", "password")
StatusCode, err := c.GetGoogle()
if err != nil {
return err
}
fmt.Println("Result of GetGoogle:", StatusCode)
return nil
}
-
创建一个名为
example
的新目录并导航到该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import "github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter7/rest"
func main() {
if err := rest.Exec(); err != nil {
panic(err)
}
}
-
运行
go run main.go
。 -
您还可以运行以下命令:
$ go build $ ./example
现在您应该看到以下输出:
$ go run main.go
Result of GetGoogle: 200
- 如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
这段代码演示了如何隐藏诸如认证和使用Transport
接口执行令牌刷新等逻辑。它还演示了如何通过方法公开 API 调用。如果我们正在针对诸如用户 API 之类的东西进行实现,我们期望有以下方法:
type API interface{
GetUsers() (Users, error)
CreateUser(User) error
UpdateUser(User) error
DeleteUser(User)
}
如果您阅读了第五章关于数据库和存储的所有内容,这可能看起来与名为执行数据库事务接口的配方相似。通过接口进行组合,特别是像RoundTripper
接口这样的常见接口,为编写 API 提供了很大的灵活性。此外,编写一个顶层接口并传递接口而不是直接传递给客户端可能是有用的。在下一个配方中,我们将更详细地探讨这一点,因为我们将探讨编写 OAuth2 客户端。
执行并行和异步客户端请求
在 Go 中并行执行客户端请求相对简单。在下一个配方中,我们将使用客户端使用 Go 缓冲通道检索多个 URL。响应和错误都将发送到一个单独的通道,任何有权访问客户端的人都可以立即访问。
在这个配方的情况下,创建客户端,读取通道,处理响应和错误都将在main.go
文件中完成。
如何做...
这些步骤涵盖了编写和运行应用程序的过程:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter7/async
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/async
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/async
-
从
~/projects/go-programming-cookbook-original/chapter7/async
复制测试,或者利用这个机会编写一些自己的代码! -
创建一个名为
config.go
的文件,内容如下:
package async
import "net/http"
// NewClient creates a new client and
// sets its appropriate channels
func NewClient(client *http.Client, bufferSize int) *Client {
respch := make(chan *http.Response, bufferSize)
errch := make(chan error, bufferSize)
return &Client{
Client: client,
Resp: respch,
Err: errch,
}
}
// Client stores a client and has two channels to aggregate
// responses and errors
type Client struct {
*http.Client
Resp chan *http.Response
Err chan error
}
// AsyncGet performs a Get then returns
// the resp/error to the appropriate channel
func (c *Client) AsyncGet(url string) {
resp, err := c.Get(url)
if err != nil {
c.Err <- err
return
}
c.Resp <- resp
}
- 创建一个名为
exec.go
的文件,内容如下:
package async
// FetchAll grabs a list of urls
func FetchAll(urls []string, c *Client) {
for _, url := range urls {
go c.AsyncGet(url)
}
}
-
创建一个名为
example
的新目录并导航到该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"fmt"
"net/http"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/chapter7/async"
)
func main() {
urls := []string{
"https://www.google.com",
"https://golang.org",
"https://www.github.com",
}
c := async.NewClient(http.DefaultClient, len(urls))
async.FetchAll(urls, c)
for i := 0; i < len(urls); i++ {
select {
case resp := <-c.Resp:
fmt.Printf("Status received for %s: %d\n",
resp.Request.URL, resp.StatusCode)
case err := <-c.Err:
fmt.Printf("Error received: %s\n", err)
}
}
}
-
运行
go run main.go
。 -
您还可以运行以下命令:
$ go build $ ./example
现在您应该看到以下输出:
$ go run main.go
Status received for https://www.google.com: 200
Status received for https://golang.org: 200
Status received for https://github.com/: 200
- 如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
本配方创建了一个处理请求的框架,以一种async
方式使用单个客户端。它将尝试尽快检索您指定的尽可能多的 URL。在许多情况下,您可能希望进一步限制这一点,例如使用工作池。在客户端之外处理这些async
Go 例程并为特定的存储或检索接口处理这些也是有意义的。
本配方还探讨了使用 case 语句在多个通道上进行切换。由于获取是异步执行的,必须有一些机制等待它们完成。在这种情况下,只有当主函数读取与原始列表中的 URL 数量相同的响应和错误时,程序才会终止。在这种情况下,还需要考虑应用程序是否应该超时,或者是否有其他方法可以提前取消其操作。
利用 OAuth2 客户端
OAuth2 是一种与 API 通信的相对常见的协议。golang.org/x/oauth2
包提供了一个非常灵活的客户端,用于处理 OAuth2。它有子包指定各种提供程序的端点,如 Facebook、Google 和 GitHub。
本配方将演示如何创建一个新的 GitHub OAuth2 客户端以及一些基本用法。
准备工作
完成本章开头“技术要求”部分提到的初始设置步骤后,继续以下步骤:
-
在
github.com/settings/applications/new
上配置 OAuth 客户端。 -
使用您的客户端 ID 和密钥设置环境变量:
-
export GITHUB_CLIENT="your_client"
-
export GITHUB_SECRET="your_secret"
- 在
developer.github.com/v3/
上查看 GitHub API 文档。
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter7/oauthcli
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/oauthcli
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/oauthcli
-
从
~/projects/go-programming-cookbook-original/chapter7/oauthcli
复制测试,或者将其作为练习编写自己的代码! -
创建一个名为
config.go
的文件,内容如下:
package oauthcli
import (
"context"
"fmt"
"os"
"golang.org/x/oauth2"
"golang.org/x/oauth2/github"
)
// Setup return an oauth2Config configured to talk
// to github, you need environment variables set
// for your id and secret
func Setup() *oauth2.Config {
return &oauth2.Config{
ClientID: os.Getenv("GITHUB_CLIENT"),
ClientSecret: os.Getenv("GITHUB_SECRET"),
Scopes: []string{"repo", "user"},
Endpoint: github.Endpoint,
}
}
// GetToken retrieves a github oauth2 token
func GetToken(ctx context.Context, conf *oauth2.Config)
(*oauth2.Token, error) {
url := conf.AuthCodeURL("state")
fmt.Printf("Type the following url into your browser and
follow the directions on screen: %v\n", url)
fmt.Println("Paste the code returned in the redirect URL
and hit Enter:")
var code string
if _, err := fmt.Scan(&code); err != nil {
return nil, err
}
return conf.Exchange(ctx, code)
}
- 创建一个名为
exec.go
的文件,内容如下:
package oauthcli
import (
"fmt"
"net/http"
)
// GetUsers uses an initialized oauth2 client to get
// information about a user
func GetUser(client *http.Client) error {
url := fmt.Sprintf("https://api.github.com/user")
resp, err := client.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
fmt.Println("Status Code from", url, ":", resp.StatusCode)
io.Copy(os.Stdout, resp.Body)
return nil
}
-
创建一个名为
example
的新目录,并导航到该目录。 -
创建一个
main.go
文件,内容如下:
package main
import (
"context"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter7/oauthcli"
)
func main() {
ctx := context.Background()
conf := oauthcli.Setup()
tok, err := oauthcli.GetToken(ctx, conf)
if err != nil {
panic(err)
}
client := conf.Client(ctx, tok)
if err := oauthcli.GetUser(client); err != nil {
panic(err)
}
}
-
运行
go run main.go
。 -
您还可以运行以下命令:
$ go build $ ./example
现在应该看到以下输出:
$ go run main.go
Visit the URL for the auth dialog:
https://github.com/login/oauth/authorize?
access_type=offline&client_id=
<your_id>&response_type=code&scope=repo+user&state=state
Paste the code returned in the redirect URL and hit Enter:
<your_code>
Status Code from https://api.github.com/user: 200
{<json_payload>}
-
go.mod
文件可能会更新,顶级配方目录中现在应该存在go.sum
文件。 -
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
标准的 OAuth2 流程是基于重定向的,并以服务器重定向到您指定的端点结束。然后您的服务器负责抓取代码并将其交换为令牌。本配方通过允许我们使用诸如https://localhost
或https://a-domain-you-own
之类的 URL 绕过了这一要求,手动复制/粘贴代码,然后按Enter。令牌交换后,客户端将根据需要智能地刷新令牌。
重要的是要注意,我们没有以任何方式存储令牌。如果程序崩溃,必须重新交换令牌。还需要注意的是,除非刷新令牌过期、丢失或损坏,否则只需要显式检索一次令牌。一旦客户端配置完成,只要在 OAuth2 流程期间请求了适当的范围,它就应该能够执行所有典型的 HTTP 操作。本配方请求了"repo"
和"user"
范围,但可以根据需要添加更多或更少。
实现 OAuth2 令牌存储接口
在上一个配方中,我们为客户端检索了一个令牌并执行了 API 请求。这种方法的缺点是我们没有长期存储令牌。例如,在 HTTP 服务器中,我们希望在请求之间对令牌进行一致的存储。
这个配方将探讨修改 OAuth2 客户端以在请求之间存储令牌,并使用密钥根据需要检索它。为了简单起见,这个密钥将是一个文件,但也可以是数据库、Redis 等。
准备工作
参考准备工作部分中利用 OAuth2 客户端配方。
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter7/oauthstore
的新目录,并切换到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/oauthstore
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/oauthstore
-
从
~/projects/go-programming-cookbook-original/chapter7/oauthstore
复制测试,或者利用这个机会编写一些自己的代码! -
创建一个名为
config.go
的文件,内容如下:
package oauthstore
import (
"context"
"net/http"
"golang.org/x/oauth2"
)
// Config wraps the default oauth2.Config
// and adds our storage
type Config struct {
*oauth2.Config
Storage
}
// Exchange stores a token after retrieval
func (c *Config) Exchange(ctx context.Context, code string)
(*oauth2.Token, error) {
token, err := c.Config.Exchange(ctx, code)
if err != nil {
return nil, err
}
if err := c.Storage.SetToken(token); err != nil {
return nil, err
}
return token, nil
}
// TokenSource can be passed a token which
// is stored, or when a new one is retrieved,
// that's stored
func (c *Config) TokenSource(ctx context.Context, t
*oauth2.Token) oauth2.TokenSource {
return StorageTokenSource(ctx, c, t)
}
// Client is attached to our TokenSource
func (c *Config) Client(ctx context.Context, t *oauth2.Token)
*http.Client {
return oauth2.NewClient(ctx, c.TokenSource(ctx, t))
}
- 创建一个名为
tokensource.go
的文件,内容如下:
package oauthstore
import (
"context"
"golang.org/x/oauth2"
)
type storageTokenSource struct {
*Config
oauth2.TokenSource
}
// Token satisfies the TokenSource interface
func (s *storageTokenSource) Token() (*oauth2.Token, error) {
if token, err := s.Config.Storage.GetToken(); err == nil &&
token.Valid() {
return token, err
}
token, err := s.TokenSource.Token()
if err != nil {
return token, err
}
if err := s.Config.Storage.SetToken(token); err != nil {
return nil, err
}
return token, nil
}
// StorageTokenSource will be used by out configs TokenSource
// function
func StorageTokenSource(ctx context.Context, c *Config, t
*oauth2.Token) oauth2.TokenSource {
if t == nil || !t.Valid() {
if tok, err := c.Storage.GetToken(); err == nil {
t = tok
}
}
ts := c.Config.TokenSource(ctx, t)
return &storageTokenSource{c, ts}
}
- 创建一个名为
storage.go
的文件,内容如下:
package oauthstore
import (
"context"
"fmt"
"golang.org/x/oauth2"
)
// Storage is our generic storage interface
type Storage interface {
GetToken() (*oauth2.Token, error)
SetToken(*oauth2.Token) error
}
// GetToken retrieves a github oauth2 token
func GetToken(ctx context.Context, conf Config) (*oauth2.Token,
error) {
token, err := conf.Storage.GetToken()
if err == nil && token.Valid() {
return token, err
}
url := conf.AuthCodeURL("state")
fmt.Printf("Type the following url into your browser and
follow the directions on screen: %v\n", url)
fmt.Println("Paste the code returned in the redirect URL
and hit Enter:")
var code string
if _, err := fmt.Scan(&code); err != nil {
return nil, err
}
return conf.Exchange(ctx, code)
}
- 创建一个名为
filestorage.go
的文件,内容如下:
package oauthstore
import (
"encoding/json"
"errors"
"os"
"sync"
"golang.org/x/oauth2"
)
// FileStorage satisfies our storage interface
type FileStorage struct {
Path string
mu sync.RWMutex
}
// GetToken retrieves a token from a file
func (f *FileStorage) GetToken() (*oauth2.Token, error) {
f.mu.RLock()
defer f.mu.RUnlock()
in, err := os.Open(f.Path)
if err != nil {
return nil, err
}
defer in.Close()
var t *oauth2.Token
data := json.NewDecoder(in)
return t, data.Decode(&t)
}
// SetToken creates, truncates, then stores a token
// in a file
func (f *FileStorage) SetToken(t *oauth2.Token) error {
if t == nil || !t.Valid() {
return errors.New("bad token")
}
f.mu.Lock()
defer f.mu.Unlock()
out, err := os.OpenFile(f.Path,
os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
if err != nil {
return err
}
defer out.Close()
data, err := json.Marshal(&t)
if err != nil {
return err
}
_, err = out.Write(data)
return err
}
-
创建一个名为
example
的新目录并切换到该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"context"
"io"
"os"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter7/oauthstore"
"golang.org/x/oauth2"
"golang.org/x/oauth2/github"
)
func main() {
conf := oauthstore.Config{
Config: &oauth2.Config{
ClientID: os.Getenv("GITHUB_CLIENT"),
ClientSecret: os.Getenv("GITHUB_SECRET"),
Scopes: []string{"repo", "user"},
Endpoint: github.Endpoint,
},
Storage: &oauthstore.FileStorage{Path: "token.txt"},
}
ctx := context.Background()
token, err := oauthstore.GetToken(ctx, conf)
if err != nil {
panic(err)
}
cli := conf.Client(ctx, token)
resp, err := cli.Get("https://api.github.com/user")
if err != nil {
panic(err)
}
defer resp.Body.Close()
io.Copy(os.Stdout, resp.Body)
}
-
运行
go run main.go
。 -
您也可以运行以下命令:
$ go build $ ./example
您现在应该看到以下输出:
$ go run main.go
Visit the URL for the auth dialog:
https://github.com/login/oauth/authorize?
access_type=offline&client_id=
<your_id>&response_type=code&scope=repo+user&state=state
Paste the code returned in the redirect URL and hit Enter:
<your_code>
{<json_payload>}
$ go run main.go
{<json_payload>}
-
go.mod
文件可能已更新,顶级配方目录中现在应该存在go.sum
文件。 -
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
这个配方负责将令牌的内容存储和检索到文件中。如果是第一次运行,它必须执行整个代码交换,但后续运行将重用访问令牌,并且如果有一个可用,它将使用刷新令牌进行刷新。
目前在这段代码中没有办法区分用户/令牌,但可以通过 cookie 作为文件名的密钥或数据库中的一行来实现。让我们来看看这段代码的功能:
-
config.go
文件包装了标准的 OAuth2 配置。对于涉及检索令牌的每个方法,我们首先检查本地存储中是否有有效的令牌。如果没有,我们使用标准配置检索一个,然后存储它。 -
tokensource.go
文件实现了我们自定义的TokenSource
接口,与Config
配对。与Config
类似,我们总是首先尝试从文件中检索我们的令牌;如果失败,我们将使用新令牌设置它。 -
storage.go
文件是Config
和TokenSource
使用的storage
接口。它只定义了两种方法,我们还包括了一个辅助函数来启动 OAuth2 基于代码的流程,类似于我们在上一个配方中所做的,但如果已经存在一个有效令牌的文件,它将被使用。 -
filestorage.go
文件实现了storage
接口。当我们存储一个新令牌时,我们首先截断文件并写入token
结构的 JSON 表示。否则,我们解码文件并返回token
。
在客户端中添加功能和函数组合
2015 年,Tomás Senart 就如何使用接口包装http.Client
结构并利用中间件和函数组合进行了出色的演讲。您可以在github.com/gophercon/2015-talks
找到更多信息。这个配方借鉴了他的想法,并演示了在http.Client
结构的Transport
接口上执行相同操作的示例,类似于我们之前的配方为 REST API 编写客户端。
以下教程将为标准的http.Client
结构实现日志记录和基本 auth 中间件。它还包括一个decorate
函数,可以在需要时与各种中间件一起使用。
如何做...
这些步骤涵盖了编写和运行应用程序的过程:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter7/decorator
的新目录,并进入此目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/decorator
你应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/decorator
-
从
~/projects/go-programming-cookbook-original/chapter7/decorator
复制测试,或者利用这个机会编写一些自己的代码! -
创建一个名为
config.go
的文件,内容如下:
package decorator
import (
"log"
"net/http"
"os"
)
// Setup initializes our ClientInterface
func Setup() *http.Client {
c := http.Client{}
t := Decorate(&http.Transport{},
Logger(log.New(os.Stdout, "", 0)),
BasicAuth("username", "password"),
)
c.Transport = t
return &c
}
- 创建一个名为
decorator.go
的文件,内容如下:
package decorator
import "net/http"
// TransportFunc implements the RountTripper interface
type TransportFunc func(*http.Request) (*http.Response, error)
// RoundTrip just calls the original function
func (tf TransportFunc) RoundTrip(r *http.Request)
(*http.Response, error) {
return tf(r)
}
// Decorator is a convenience function to represent our
// middleware inner function
type Decorator func(http.RoundTripper) http.RoundTripper
// Decorate is a helper to wrap all the middleware
func Decorate(t http.RoundTripper, rts ...Decorator)
http.RoundTripper {
decorated := t
for _, rt := range rts {
decorated = rt(decorated)
}
return decorated
}
- 创建一个名为
middleware.go
的文件,内容如下:
package decorator
import (
"log"
"net/http"
"time"
)
// Logger is one of our 'middleware' decorators
func Logger(l *log.Logger) Decorator {
return func(c http.RoundTripper) http.RoundTripper {
return TransportFunc(func(r *http.Request)
(*http.Response, error) {
start := time.Now()
l.Printf("started request to %s at %s", r.URL,
start.Format("2006-01-02 15:04:05"))
resp, err := c.RoundTrip(r)
l.Printf("completed request to %s in %s", r.URL,
time.Since(start))
return resp, err
})
}
}
// BasicAuth is another of our 'middleware' decorators
func BasicAuth(username, password string) Decorator {
return func(c http.RoundTripper) http.RoundTripper {
return TransportFunc(func(r *http.Request)
(*http.Response, error) {
r.SetBasicAuth(username, password)
resp, err := c.RoundTrip(r)
return resp, err
})
}
}
- 创建一个名为
exec.go
的文件,内容如下:
package decorator
import "fmt"
// Exec creates a client, calls google.com
// then prints the response
func Exec() error {
c := Setup()
resp, err := c.Get("https://www.google.com")
if err != nil {
return err
}
fmt.Println("Response code:", resp.StatusCode)
return nil
}
-
创建一个名为
example
的新目录,并进入。 -
创建一个名为
main.go
的文件,内容如下:
package main
import "github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter7/decorator"
func main() {
if err := decorator.Exec(); err != nil {
panic(err)
}
}
-
运行
go run main.go
。 -
您也可以运行以下命令:
$ go build $ ./example
您现在应该看到以下输出:
$ go run main.go
started request to https://www.google.com at 2017-01-01 13:38:42
completed request to https://www.google.com in 194.013054ms
Response code: 200
- 如果您复制或编写了自己的测试,请返回到上一级目录并运行
go test
。确保所有测试都通过了。
工作原理...
这个教程利用了闭包作为一等公民和接口。实现这一点的主要技巧是让一个函数实现一个接口。这使我们能够用一个函数实现的接口来包装一个结构体实现的接口。
middleware.go
文件包含两个示例客户端中间件函数。这些可以扩展为包含其他中间件,比如更复杂的 auth 和 metrics。这个教程也可以与前一个教程结合起来,生成一个可以通过其他中间件扩展的 OAuth2 客户端。
Decorator
函数是一个方便的函数,允许以下操作:
Decorate(RoundTripper, Middleware1, Middleware2, etc)
vs
var t RoundTripper
t = Middleware1(t)
t = Middleware2(t)
etc
与包装客户端相比,这种方法的优势在于我们可以保持接口的稀疏性。如果您想要一个功能齐全的客户端,您还需要实现GET
、POST
和PostForm
等方法。
理解 GRPC 客户端
GRPC 是一个高性能的 RPC 框架,使用协议缓冲区(developers.google.com/protocol-buffers
)和 HTTP/2(http2.github.io
)构建。在 Go 中创建一个 GRPC 客户端涉及到与 Go HTTP 客户端相同的许多复杂性。为了演示基本客户端的使用,最容易的方法是同时实现一个服务器。这个教程将创建一个greeter
服务,它接受一个问候和一个名字,并返回句子<greeting> <name>!
。此外,服务器可以指定是否感叹!
或不是.
(句号)。
这个教程不会探讨 GRPC 的一些细节,比如流式传输;但是,它有望作为创建一个非常基本的服务器和客户端的介绍。
准备就绪
在本章开头的技术要求部分完成初始设置步骤后,安装 GRPC (grpc.io/docs/quickstart/go/
) 并运行以下命令:
-
go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
-
go get -u google.golang.org/grpc
如何做...
这些步骤涵盖了编写和运行应用程序的过程:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter7/grpc
的新目录,并进入此目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/grpc
你应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/grpc
-
从
~/projects/go-programming-cookbook-original/chapter7/grpc
复制测试,或者利用这个机会编写一些自己的代码! -
创建一个名为
greeter
的目录并进入。 -
创建一个名为
greeter.proto
的文件,内容如下:
syntax = "proto3";
package greeter;
service GreeterService{
rpc Greet(GreetRequest) returns (GreetResponse) {}
}
message GreetRequest {
string greeting = 1;
string name = 2;
}
message GreetResponse{
string response = 1;
}
-
返回到
grpc
目录。 -
运行以下命令:
$ protoc --go_out=plugins=grpc:. greeter/greeter.proto
-
创建一个名为
server
的新目录,并进入该目录。 -
创建一个名为
greeter.go
的文件,内容如下。确保修改greeter
导入,使用你在第 3 步设置的路径:
package main
import (
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter7/grpc/greeter"
"golang.org/x/net/context"
)
// Greeter implements the interface
// generated by protoc
type Greeter struct {
Exclaim bool
}
// Greet implements grpc Greet
func (g *Greeter) Greet(ctx context.Context, r
*greeter.GreetRequest) (*greeter.GreetResponse, error) {
msg := fmt.Sprintf("%s %s", r.GetGreeting(), r.GetName())
if g.Exclaim {
msg += "!"
} else {
msg += "."
}
return &greeter.GreetResponse{Response: msg}, nil
}
- 创建一个名为
server.go
的文件,内容如下。确保修改greeter
导入,使用你在第 3 步设置的路径:
package main
import (
"fmt"
"net"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter7/grpc/greeter"
"google.golang.org/grpc"
)
func main() {
grpcServer := grpc.NewServer()
greeter.RegisterGreeterServiceServer(grpcServer,
&Greeter{Exclaim: true})
lis, err := net.Listen("tcp", ":4444")
if err != nil {
panic(err)
}
fmt.Println("Listening on port :4444")
grpcServer.Serve(lis)
}
-
返回到
grpc
目录。 -
创建一个名为
client
的新目录,并进入该目录。 -
创建一个名为
client.go
的文件,内容如下。确保修改greeter
导入,使用你在第 3 步设置的路径:
package main
import (
"context"
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter7/grpc/greeter"
"google.golang.org/grpc"
)
func main() {
conn, err := grpc.Dial(":4444", grpc.WithInsecure())
if err != nil {
panic(err)
}
defer conn.Close()
client := greeter.NewGreeterServiceClient(conn)
ctx := context.Background()
req := greeter.GreetRequest{Greeting: "Hello", Name:
"Reader"}
resp, err := client.Greet(ctx, &req)
if err != nil {
panic(err)
}
fmt.Println(resp)
req.Greeting = "Goodbye"
resp, err = client.Greet(ctx, &req)
if err != nil {
panic(err)
}
fmt.Println(resp)
}
-
返回到
grpc
目录。 -
运行
go run ./server
,你会看到以下输出:
$ go run ./server
Listening on port :4444
- 在另一个终端中,从
grpc
目录运行go run ./client
,你会看到以下输出:
$ go run ./client
response:"Hello Reader!"
response:"Goodbye Reader!"
-
go.mod
文件可能会被更新,顶级示例目录中现在应该存在go.sum
文件。 -
如果你复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
GRPC 服务器设置为监听端口4444
。一旦客户端连接,它就可以向服务器发送请求并接收响应。请求、响应和支持的方法的结构由我们在第 4 步创建的.proto
文件所决定。在实践中,当集成到 GRPC 服务器时,它们应该提供.proto
文件,该文件可以用于自动生成客户端。
除了客户端,protoc
命令还会为服务器生成存根,所需的一切就是填写实现细节。生成的 Go 代码还具有 JSON 标记,相同的结构可以重用于 JSON REST 服务。我们的代码设置了一个不安全的客户端。要安全地处理 GRPC,你需要使用 SSL 证书。
使用 twitchtv/twirp 进行 RPC
twitchtv/twirp
RPC 框架提供了许多 GRPC 的优点,包括使用协议缓冲区(developers.google.com/protocol-buffers
)构建模型,并允许通过 HTTP 1.1 进行通信。它还可以使用 JSON 进行通信,因此可以使用curl
命令与twirp
RPC 服务进行通信。这个示例将实现与之前 GRPC 部分相同的greeter
。该服务接受一个问候和一个名字,并返回句子<greeting> <name>!
。此外,服务器可以指定是否感叹!
或不感叹.
。
这个示例不会探索twitchtv/twirp
的其他功能,主要关注基本的客户端-服务器通信。有关支持的更多信息,请访问他们的 GitHub 页面(github.com/twitchtv/twirp
)。
准备就绪
完成本章开头技术要求部分提到的初始设置步骤后,安装 twirp twitchtv.github.io/twirp/docs/install.html
,并运行以下命令:
-
go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
-
go get github.com/twitchtv/twirp/protoc-gen-twirp
如何操作...
这些步骤涵盖了编写和运行应用程序的过程:
-
从你的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter7/twirp
的新目录,并进入该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/twirp
你应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/twirp
-
从
~/projects/go-programming-cookbook-original/chapter7/twirp
复制测试,或者将其作为练习编写一些自己的代码! -
创建一个名为
rpc/greeter
的目录,并进入该目录。 -
创建一个名为
greeter.proto
的文件,内容如下:
syntax = "proto3";
package greeter;
service GreeterService{
rpc Greet(GreetRequest) returns (GreetResponse) {}
}
message GreetRequest {
string greeting = 1;
string name = 2;
}
message GreetResponse{
string response = 1;
}
-
返回到
twirp
目录。 -
运行以下命令:
$ protoc --proto_path=$GOPATH/src:. --twirp_out=. --go_out=. ./rpc/greeter/greeter.proto
-
创建一个名为
server
的新目录,并进入该目录。 -
创建一个名为
greeter.go
的文件,内容如下。确保修改greeter
导入,使用你在第 3 步设置的路径:
package main
import (
"context"
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter7/twirp/rpc/greeter"
)
// Greeter implements the interface
// generated by protoc
type Greeter struct {
Exclaim bool
}
// Greet implements twirp Greet
func (g *Greeter) Greet(ctx context.Context, r *greeter.GreetRequest) (*greeter.GreetResponse, error) {
msg := fmt.Sprintf("%s %s", r.GetGreeting(), r.GetName())
if g.Exclaim {
msg += "!"
} else {
msg += "."
}
return &greeter.GreetResponse{Response: msg}, nil
}
- 创建一个名为
server.go
的文件,内容如下。确保修改greeter
导入以使用您在第 3 步设置的路径:
package main
import (
"fmt"
"net/http"
"github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/twirp/rpc/greeter"
)
func main() {
server := &Greeter{}
twirpHandler := greeter.NewGreeterServiceServer(server, nil)
fmt.Println("Listening on port :4444")
http.ListenAndServe(":4444", twirpHandler)
}
-
导航回到
twirp
目录的上一级目录。 -
创建一个名为
client
的新目录并导航到该目录。 -
创建一个名为
client.go
的文件,内容如下。确保修改greeter
导入以使用您在第 3 步设置的路径:
package main
import (
"context"
"fmt"
"net/http"
"github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/twirp/rpc/greeter"
)
func main() {
// you can put in a custom client for tighter controls on timeouts etc.
client := greeter.NewGreeterServiceProtobufClient("http://localhost:4444", &http.Client{})
ctx := context.Background()
req := greeter.GreetRequest{Greeting: "Hello", Name: "Reader"}
resp, err := client.Greet(ctx, &req)
if err != nil {
panic(err)
}
fmt.Println(resp)
req.Greeting = "Goodbye"
resp, err = client.Greet(ctx, &req)
if err != nil {
panic(err)
}
fmt.Println(resp)
}
-
导航回到
twirp
目录的上一级目录。 -
运行
go run ./server
,您将看到以下输出:
$ go run ./server
Listening on port :4444
- 在另一个终端中,从
twirp
目录运行go run ./client
。您应该会看到以下输出:
$ go run ./client
response:"Hello Reader."
response:"Goodbye Reader."
-
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶层配方目录中。 -
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
我们设置了twitchtv/twirp
RPC 服务器监听端口4444
。与 GRPC 一样,protoc
可以用于为许多语言生成客户端,并且例如生成 Swagger (swagger.io/
)文档。
与 GRPC 一样,我们首先将我们的模型定义为.proto
文件,生成 Go 绑定,最后实现生成的接口。由于使用了.proto
文件,只要您不依赖于任何框架的更高级功能,代码在 GRPC 和twitchtv/twirp
之间相对可移植。
此外,因为twitchtv/twirp
服务器支持 HTTP 1.1,我们可以使用curl
进行如下操作:
$ curl --request "POST" \
--location "http://localhost:4444/twirp/greeter.GreeterService/Greet" \
--header "Content-Type:application/json" \
--data '{"greeting": "Greetings to", "name":"you"}'
{"response":"Greetings to you."}
第八章:Go 应用程序的微服务
Go 是一个编写 Web 应用程序的绝佳选择。内置的net/http
包结合html/template
等包,可以实现现代完整功能的 Web 应用程序。它如此简单,以至于它鼓励为管理甚至是基本的长期运行的应用程序启动 Web 界面。尽管标准库功能齐全,但仍然有大量第三方 Web 包,涵盖从路由到全栈框架的各种功能,包括以下内容:
本章的食谱将侧重于处理程序、响应和请求对象以及处理中间件等概念时可能遇到的基本任务。
在本章中,将涵盖以下食谱:
-
处理 web 处理程序、请求和 ResponseWriter 实例
-
使用结构和闭包进行有状态处理程序
-
验证 Go 结构和用户输入的输入
-
渲染和内容协商
-
实现和使用中间件
-
构建一个反向代理应用程序
-
将 GRPC 导出为 JSON API
技术要求
为了继续本章中的所有食谱,根据以下步骤配置您的环境:
-
从
golang.org/doc/install
下载并安装 Go 1.12.6 或更高版本到您的操作系统上。 -
打开终端或控制台应用程序;创建一个项目目录,例如
~/projects/go-programming-cookbook
,并导航到该目录。所有的代码都将在这个目录中运行和修改。 -
将最新的代码克隆到
~/projects/go-programming-cookbook-original
,或者选择从该目录工作,而不是手动输入示例,如下所示:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
- 从
curl.haxx.se/download.html
安装curl
命令。
处理 web 处理程序、请求和 ResponseWriter 实例
Go 定义了具有以下签名的HandlerFunc
和Handler
接口:
// HandlerFunc implements the Handler interface
type HandlerFunc func(http.ResponseWriter, *http.Request)
type Handler interface {
ServeHTTP(http.ResponseWriter, *http.Request)
}
默认情况下,net/http
包广泛使用这些类型。例如,路由可以附加到Handler
或HandlerFunc
接口。本教程将探讨创建Handler
接口,监听本地端口,并在处理http.Request
后对http.ResponseWriter
接口执行一些操作。这应该被视为 Go Web 应用程序和 RESTful API 的基础。
如何做...
以下步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter8/handlers
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter8/handlers
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter8/handlers
-
从
~/projects/go-programming-cookbook-original/chapter8/handlers
复制测试,或者使用这个作为练习来编写一些自己的代码! -
创建一个名为
get.go
的文件,内容如下:
package handlers
import (
"fmt"
"net/http"
)
// HelloHandler takes a GET parameter "name" and responds
// with Hello <name>! in plaintext
func HelloHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
name := r.URL.Query().Get("name")
w.WriteHeader(http.StatusOK)
w.Write([]byte(fmt.Sprintf("Hello %s!", name)))
}
- 创建一个名为
post.go
的文件,内容如下:
package handlers
import (
"encoding/json"
"net/http"
)
// GreetingResponse is the JSON Response that
// GreetingHandler returns
type GreetingResponse struct {
Payload struct {
Greeting string `json:"greeting,omitempty"`
Name string `json:"name,omitempty"`
Error string `json:"error,omitempty"`
} `json:"payload"`
Successful bool `json:"successful"`
}
// GreetingHandler returns a GreetingResponse which either has
// errors or a useful payload
func GreetingHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
var gr GreetingResponse
if err := r.ParseForm(); err != nil {
gr.Payload.Error = "bad request"
if payload, err := json.Marshal(gr); err == nil {
w.Write(payload)
} else if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
}
name := r.FormValue("name")
greeting := r.FormValue("greeting")
w.WriteHeader(http.StatusOK)
gr.Successful = true
gr.Payload.Name = name
gr.Payload.Greeting = greeting
if payload, err := json.Marshal(gr); err == nil {
w.Write(payload)
}
}
-
创建一个名为
example
的新目录,并导航到该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"fmt"
"net/http"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
$ chapter8/handlers"
)
func main() {
http.HandleFunc("/name", handlers.HelloHandler)
http.HandleFunc("/greeting", handlers.GreetingHandler)
fmt.Println("Listening on port :3333")
err := http.ListenAndServe(":3333", nil)
panic(err)
}
-
运行
go run main.go
。 -
您也可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
Listening on port :3333
- 在一个单独的终端中,运行以下命令:
$ curl "http://localhost:3333/name?name=Reader" -X GET $ curl "http://localhost:3333/greeting" -X POST -d
'name=Reader;greeting=Goodbye'
您应该看到以下输出:
$ curl "http://localhost:3333/name?name=Reader" -X GET
Hello Reader!
$ curl "http://localhost:3333/greeting" -X POST -d 'name=Reader;greeting=Goodbye'
{"payload":{"greeting":"Goodbye","name":"Reader"},"successful":true}
-
go.mod
文件可能会被更新,顶级食谱目录中现在应该存在go.sum
文件。 -
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
对于这个示例,我们设置了两个处理程序。第一个处理程序期望使用名为name
的GET
参数的GET
请求。当我们使用curl
时,它返回纯文本字符串Hello <name>!
。
第二个处理程序期望使用PostForm
请求的POST
方法。这是如果您使用标准 HTML 表单而没有任何 AJAX 调用时会得到的结果。或者,我们可以从请求体中解析 JSON。这通常使用json.Decoder
来完成。我建议您也尝试这个练习。最后,处理程序发送一个 JSON 格式的响应并设置所有适当的标头。
尽管所有这些都是明确写出的,但有许多方法可以使代码更简洁,包括以下方法:
-
使用
github.com/unrolled/render
来处理响应 -
使用本章中提到的各种 Web 框架来解析路由参数,限制路由到特定的 HTTP 动词,处理优雅的关闭等
使用结构和闭包进行有状态的处理程序
由于 HTTP 处理程序函数的签名稀疏,向处理程序添加状态可能会显得棘手。例如,有多种方法可以包含数据库连接。实现这一点的两种方法是通过闭包传递状态,这对于在单个处理程序上实现灵活性非常有用,或者使用结构。
这个示例将演示两者。我们将使用一个struct
控制器来存储一个存储接口,并创建两个由外部函数修改的单个处理程序的路由。
如何做...
以下步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter8/controllers
的新目录,并进入该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter8/controllers
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter8/controllers
-
从
~/projects/go-programming-cookbook-original/chapter8/controllers
复制测试,或者将其用作编写自己代码的练习! -
创建一个名为
controller.go
的文件,内容如下:
package controllers
// Controller passes state to our handlers
type Controller struct {
storage Storage
}
// New is a Controller 'constructor'
func New(storage Storage) *Controller {
return &Controller{
storage: storage,
}
}
// Payload is our common response
type Payload struct {
Value string `json:"value"`
}
- 创建一个名为
storage.go
的文件,内容如下:
package controllers
// Storage Interface Supports Get and Put
// of a single value
type Storage interface {
Get() string
Put(string)
}
// MemStorage implements Storage
type MemStorage struct {
value string
}
// Get our in-memory value
func (m *MemStorage) Get() string {
return m.value
}
// Put our in-memory value
func (m *MemStorage) Put(s string) {
m.value = s
}
- 创建一个名为
post.go
的文件,内容如下:
package controllers
import (
"encoding/json"
"net/http"
)
// SetValue modifies the underlying storage of the controller
// object
func (c *Controller) SetValue(w http.ResponseWriter, r
*http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if err := r.ParseForm(); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
value := r.FormValue("value")
c.storage.Put(value)
w.WriteHeader(http.StatusOK)
p := Payload{Value: value}
if payload, err := json.Marshal(p); err == nil {
w.Write(payload)
} else if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
}
- 创建一个名为
get.go
的文件,内容如下:
package controllers
import (
"encoding/json"
"net/http"
)
// GetValue is a closure that wraps a HandlerFunc, if
// UseDefault is true value will always be "default" else it'll
// be whatever is stored in storage
func (c *Controller) GetValue(UseDefault bool) http.HandlerFunc
{
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
value := "default"
if !UseDefault {
value = c.storage.Get()
}
w.WriteHeader(http.StatusOK)
p := Payload{Value: value}
if payload, err := json.Marshal(p); err == nil {
w.Write(payload)
}
}
}
-
创建一个名为
example
的新目录并进入该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"fmt"
"net/http"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter8/controllers"
)
func main() {
storage := controllers.MemStorage{}
c := controllers.New(&storage)
http.HandleFunc("/get", c.GetValue(false))
http.HandleFunc("/get/default", c.GetValue(true))
http.HandleFunc("/set", c.SetValue)
fmt.Println("Listening on port :3333")
err := http.ListenAndServe(":3333", nil)
panic(err)
}
-
运行
go run main.go
。 -
您还可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
Listening on port :3333
- 在另一个终端中,运行以下命令:
$ curl "http://localhost:3333/set" -X POST -d "value=value"
$ curl "http://localhost:3333/get" -X GET
$ curl "http://localhost:3333/get/default" -X GET
您应该看到以下输出:
$ curl "http://localhost:3333/set" -X POST -d "value=value"
{"value":"value"}
$ curl "http://localhost:3333/get" -X GET
{"value":"value"}
$ curl "http://localhost:3333/get/default" -X GET
{"value":"default"}
-
go.mod
文件可能会被更新,顶级配方目录中现在应该存在go.sum
文件。 -
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
这些策略有效是因为 Go 允许方法满足诸如http.HandlerFunc
之类的类型化函数。通过使用结构,我们可以在main.go
中注入各种部分,其中包括数据库连接,日志记录等。在这个示例中,我们插入了一个Storage
接口。连接到控制器的所有处理程序都可以使用它的方法和属性。
GetValue
方法没有http.HandlerFunc
签名,而是返回一个。这就是我们可以使用闭包来注入状态的方式。在main.go
中,我们定义了两个路由,一个将UseDefault
设置为false
,另一个将其设置为true
。这可以在定义跨多个路由的函数时使用,或者在使用结构时,您的处理程序感觉太繁琐时使用。
验证 Go 结构和用户输入的输入
Web 验证可能会有问题。这个示例将探讨使用闭包来支持验证函数的易于模拟,并在初始化控制器结构时允许对验证类型的灵活性,正如前面的示例所描述的那样。
我们将对一个结构执行此验证,但不探讨如何填充这个结构。我们可以假设数据是通过解析 JSON 有效载荷、明确从表单输入中填充或其他方法填充的。
如何做...
以下步骤涵盖了编写和运行应用程序的过程:
-
从你的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter8/validation
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter8/validation
你应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter8/validation
-
从
~/projects/go-programming-cookbook-original/chapter8/validation
复制测试,或者利用这个机会编写一些你自己的代码! -
创建一个名为
controller.go
的文件,内容如下:
package validation
// Controller holds our validation functions
type Controller struct {
ValidatePayload func(p *Payload) error
}
// New initializes a controller with our
// local validation, it can be overwritten
func New() *Controller {
return &Controller{
ValidatePayload: ValidatePayload,
}
}
- 创建一个名为
validate.go
的文件,内容如下:
package validation
import "errors"
// Verror is an error that occurs
// during validation, we can
// return this to a user
type Verror struct {
error
}
// Payload is the value we
// process
type Payload struct {
Name string `json:"name"`
Age int `json:"age"`
}
// ValidatePayload is 1 implementation of
// the closure in our controller
func ValidatePayload(p *Payload) error {
if p.Name == "" {
return Verror{errors.New("name is required")}
}
if p.Age <= 0 || p.Age >= 120 {
return Verror{errors.New("age is required and must be a
value greater than 0 and less than 120")}
}
return nil
}
- 创建一个名为
process.go
的文件,内容如下:
package validation
import (
"encoding/json"
"fmt"
"net/http"
)
// Process is a handler that validates a post payload
func (c *Controller) Process(w http.ResponseWriter, r
*http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
decoder := json.NewDecoder(r.Body)
defer r.Body.Close()
var p Payload
if err := decoder.Decode(&p); err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusBadRequest)
return
}
if err := c.ValidatePayload(&p); err != nil {
switch err.(type) {
case Verror:
w.WriteHeader(http.StatusBadRequest)
// pass the Verror along
w.Write([]byte(err.Error()))
return
default:
w.WriteHeader(http.StatusInternalServerError)
return
}
}
}
-
创建一个名为
example
的新目录,并导航到该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"fmt"
"net/http"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter8/validation"
)
func main() {
c := validation.New()
http.HandleFunc("/", c.Process)
fmt.Println("Listening on port :3333")
err := http.ListenAndServe(":3333", nil)
panic(err)
}
-
运行
go run main.go
。 -
你也可以运行以下命令:
$ go build $ ./example
你应该看到以下输出:
$ go run main.go
Listening on port :3333
- 在另一个终端中,运行以下命令:
$ curl "http://localhost:3333/" -X POST -d '{}' $ curl "http://localhost:3333/" -X POST -d '{"name":"test"}' $ curl "http://localhost:3333/" -X POST -d '{"name":"test",
"age": 5}' -v
你应该看到以下输出:
$ curl "http://localhost:3333/" -X POST -d '{}'
name is required
$ curl "http://localhost:3333/" -X POST -d '{"name":"test"}'
age is required and must be a value greater than 0 and
less than 120
$ curl "http://localhost:3333/" -X POST -d '{"name":"test",
"age": 5}' -v
<lots of output, should contain a 200 OK status code>
-
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶层食谱目录中。 -
如果你复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
我们通过向我们的控制器结构传递一个闭包来处理验证。对于控制器可能需要验证的任何输入,我们都需要一个这样的闭包。这种方法的优势在于我们可以在运行时模拟和替换验证函数,因此测试变得更简单。此外,我们不受限于单个函数签名,可以传递诸如数据库连接之类的东西给我们的验证函数。
这个食谱展示的另一件事是返回一个名为Verror
的类型错误。这种类型保存了可以显示给用户的验证错误消息。这种方法的一个缺点是它不能一次处理多个验证消息。通过修改Verror
类型以允许更多状态,例如通过包含一个映射,来容纳多个验证错误,然后从我们的ValidatePayload
函数返回,这是可能的。
渲染和内容协商
Web 处理程序可以返回各种内容类型;例如,它们可以返回 JSON、纯文本、图像等。在与 API 通信时,通常可以指定和接受内容类型,以澄清你将以什么格式传递数据,以及你想要接收什么数据。
这个食谱将探讨使用unrolled/render
和一个自定义函数来协商内容类型并相应地做出响应。
如何做...
以下步骤涵盖了编写和运行应用程序的过程:
-
从你的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter8/negotiate
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter8/negotiate
你应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter8/negotiate
-
复制来自
~/projects/go-programming-cookbook-original/chapter8/negotiate
的测试,或者利用这个机会编写一些你自己的代码! -
创建一个名为
negotiate.go
的文件,内容如下:
package negotiate
import (
"net/http"
"github.com/unrolled/render"
)
// Negotiator wraps render and does
// some switching on ContentType
type Negotiator struct {
ContentType string
*render.Render
}
// GetNegotiator takes a request, and figures
// out the ContentType from the Content-Type header
func GetNegotiator(r *http.Request) *Negotiator {
contentType := r.Header.Get("Content-Type")
return &Negotiator{
ContentType: contentType,
Render: render.New(),
}
}
- 创建一个名为
respond.go
的文件,内容如下:
package negotiate
import "io"
import "github.com/unrolled/render"
// Respond switches on Content Type to determine
// the response
func (n *Negotiator) Respond(w io.Writer, status int, v
interface{}) {
switch n.ContentType {
case render.ContentJSON:
n.Render.JSON(w, status, v)
case render.ContentXML:
n.Render.XML(w, status, v)
default:
n.Render.JSON(w, status, v)
}
}
- 创建一个名为
handler.go
的文件,内容如下:
package negotiate
import (
"encoding/xml"
"net/http"
)
// Payload defines it's layout in xml and json
type Payload struct {
XMLName xml.Name `xml:"payload" json:"-"`
Status string `xml:"status" json:"status"`
}
// Handler gets a negotiator using the request,
// then renders a Payload
func Handler(w http.ResponseWriter, r *http.Request) {
n := GetNegotiator(r)
n.Respond(w, http.StatusOK, &Payload{Status:
"Successful!"})
}
-
创建一个名为
example
的新目录,并导航到该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"fmt"
"net/http"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter8/negotiate"
)
func main() {
http.HandleFunc("/", negotiate.Handler)
fmt.Println("Listening on port :3333")
err := http.ListenAndServe(":3333", nil)
panic(err)
}
-
运行
go run main.go
。 -
你也可以运行以下命令:
$ go build $ ./example
你应该看到以下输出:
$ go run main.go
Listening on port :3333
- 在另一个终端中,运行以下命令:
$ curl "http://localhost:3333" -H "Content-Type: text/xml" $ curl "http://localhost:3333" -H "Content-Type: application/json"
你应该看到以下输出:
$ curl "http://localhost:3333" -H "Content-Type: text/xml"
<payload><status>Successful!</status></payload>
$ curl "http://localhost:3333" -H "Content-Type: application/json"
{"status":"Successful!"}
-
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶层食谱目录中。 -
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
github.com/unrolled/render
包为这个教程做了大量的工作。如果需要处理 HTML 模板等,还有大量其他选项可以输入。这个教程可以用于在通过传递各种内容类型标头来自动协商工作时,或者通过直接操作结构来演示如何解耦中间件逻辑和处理程序。
类似的模式可以应用于接受标头,但要注意这些标头通常包含多个值,您的代码将不得不考虑到这一点。
实现和使用中间件
Go 中用于处理程序的中间件是一个被广泛探索的领域。有各种各样的包用于处理中间件。这个教程将从头开始创建中间件,并实现一个ApplyMiddleware
函数来链接一系列中间件。
它还将探讨在请求上下文对象中设置值并稍后使用中间件检索它们。这将通过一个非常基本的处理程序来完成,以帮助演示如何将中间件逻辑与处理程序解耦。
如何做...
以下步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter8/middleware
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter8/middleware
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter8/middleware
-
复制
~/projects/go-programming-cookbook-original/chapter8/middleware
中的测试,或者将其用作编写一些自己代码的练习! -
创建一个名为
middleware.go
的文件,其中包含以下内容:
package middleware
import (
"log"
"net/http"
"time"
)
// Middleware is what all middleware functions will return
type Middleware func(http.HandlerFunc) http.HandlerFunc
// ApplyMiddleware will apply all middleware, the last
// arguments will be the
// outer wrap for context passing purposes
func ApplyMiddleware(h http.HandlerFunc, middleware
...Middleware) http.HandlerFunc {
applied := h
for _, m := range middleware {
applied = m(applied)
}
return applied
}
// Logger logs requests, this will use an id passed in via
// SetID()
func Logger(l *log.Logger) Middleware {
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
l.Printf("started request to %s with id %s", r.URL,
GetID(r.Context()))
next(w, r)
l.Printf("completed request to %s with id %s in
%s", r.URL, GetID(r.Context()), time.Since(start))
}
}
}
- 创建一个名为
context.go
的文件,其中包含以下内容:
package middleware
import (
"context"
"net/http"
"strconv"
)
// ContextID is our type to retrieve our context
// objects
type ContextID int
// ID is the only ID we've defined
const ID ContextID = 0
// SetID updates context with the id then
// increments it
func SetID(start int64) Middleware {
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), ID,
strconv.FormatInt(start, 10))
start++
r = r.WithContext(ctx)
next(w, r)
}
}
}
// GetID grabs an ID from a context if set
// otherwise it returns an empty string
func GetID(ctx context.Context) string {
if val, ok := ctx.Value(ID).(string); ok {
return val
}
return ""
}
- 创建一个名为
handler.go
的文件,其中包含以下内容:
package middleware
import (
"net/http"
)
// Handler is very basic
func Handler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("success"))
}
-
创建一个名为
example
的新目录并导航到该目录。 -
创建一个名为
main.go
的文件,其中包含以下内容:
package main
import (
"fmt"
"log"
"net/http"
"os"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter8/middleware"
)
func main() {
// We apply from bottom up
h := middleware.ApplyMiddleware(
middleware.Handler,
middleware.Logger(log.New(os.Stdout, "", 0)),
middleware.SetID(100),
)
http.HandleFunc("/", h)
fmt.Println("Listening on port :3333")
err := http.ListenAndServe(":3333", nil)
panic(err)
}
-
运行
go run main.go
。 -
您还可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
Listening on port :3333
- 在另一个终端中,运行以下
curl
命令多次:
$ curl http://localhost:3333
您应该看到以下输出:
$ curl http://localhost:3333
success
$ curl http://localhost:3333
success
$ curl http://localhost:3333
success
- 在原始的
main.go
中,您应该看到以下内容:
Listening on port :3333
started request to / with id 100
completed request to / with id 100 in 52.284µs
started request to / with id 101
completed request to / with id 101 in 40.273µs
started request to / with id 102
-
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶级教程目录中。 -
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
中间件可以用于执行简单的操作,比如日志记录、度量收集和分析。中间件也可以用于在每个请求上动态填充变量。例如,可以收集请求中的 X-header 来设置一个 ID 或生成一个 ID,就像我们在这个教程中所做的那样。另一种 ID 策略可能是为每个请求生成一个通用唯一标识符(UUID)—这样我们就可以轻松地将日志消息关联在一起,并跟踪您的请求穿越不同的应用程序,如果多个微服务参与构建响应的话。
在处理上下文值时,考虑中间件的顺序是很重要的。通常,最好不要让中间件相互依赖。例如,在这个教程中,最好在日志中间件本身生成 UUID。然而,这个教程应该作为分层中间件和在main.go
中初始化它们的指南。
构建反向代理应用程序
在这个教程中,我们将开发一个反向代理应用程序。这个想法是,通过在浏览器中访问http://localhost:3333
,所有流量将被转发到一个可配置的主机,并且响应将被转发到您的浏览器。最终结果应该是通过我们的代理应用程序在浏览器中呈现www.golang.org
。
这可以与端口转发和 SSH 隧道结合使用,以便通过中间服务器安全地访问网站。这个配方将从头开始构建一个反向代理,但这个功能也由net/http/httputil
包提供。使用这个包,传入的请求可以通过Director func(*http.Request)
进行修改,传出的响应可以通过ModifyResponse func(*http.Response) error
进行修改。此外,还支持对响应进行缓冲。
如何做...
以下步骤涵盖了编写和运行应用程序的过程:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter8/proxy
的新目录,并进入该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter8/proxy
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter8/proxy
-
从
~/projects/go-programming-cookbook-original/chapter8/proxy
复制测试,或者将其作为练习编写一些自己的代码! -
创建一个名为
proxy.go
的文件,内容如下:
package proxy
import (
"log"
"net/http"
)
// Proxy holds our configured client
// and BaseURL to proxy to
type Proxy struct {
Client *http.Client
BaseURL string
}
// ServeHTTP means that proxy implements the Handler interface
// It manipulates the request, forwards it to BaseURL, then
// returns the response
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r
*http.Request) {
if err := p.ProcessRequest(r); err != nil {
log.Printf("error occurred during process request: %s",
err.Error())
w.WriteHeader(http.StatusBadRequest)
return
}
resp, err := p.Client.Do(r)
if err != nil {
log.Printf("error occurred during client operation:
%s", err.Error())
w.WriteHeader(http.StatusInternalServerError)
return
}
defer resp.Body.Close()
CopyResponse(w, resp)
}
- 创建一个名为
process.go
的文件,内容如下:
package proxy
import (
"bytes"
"net/http"
"net/url"
)
// ProcessRequest modifies the request in accordnance
// with Proxy settings
func (p *Proxy) ProcessRequest(r *http.Request) error {
proxyURLRaw := p.BaseURL + r.URL.String()
proxyURL, err := url.Parse(proxyURLRaw)
if err != nil {
return err
}
r.URL = proxyURL
r.Host = proxyURL.Host
r.RequestURI = ""
return nil
}
// CopyResponse takes the client response and writes everything
// to the ResponseWriter in the original handler
func CopyResponse(w http.ResponseWriter, resp *http.Response) {
var out bytes.Buffer
out.ReadFrom(resp.Body)
for key, values := range resp.Header {
for _, value := range values {
w.Header().Add(key, value)
}
}
w.WriteHeader(resp.StatusCode)
w.Write(out.Bytes())
}
-
创建一个名为
example
的新目录并进入。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"fmt"
"net/http"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter8/proxy"
)
func main() {
p := &proxy.Proxy{
Client: http.DefaultClient,
BaseURL: "https://www.golang.org",
}
http.Handle("/", p)
fmt.Println("Listening on port :3333")
err := http.ListenAndServe(":3333", nil)
panic(err)
}
-
运行
go run main.go
。 -
您也可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
Listening on port :3333
-
将浏览器导航到
localhost:3333/
。您应该看到golang.org/
网站呈现出来! -
go.mod
文件可能已更新,go.sum
文件现在应该存在于顶级配方目录中。 -
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
Go 请求和响应对象在客户端和处理程序之间大部分是可共享的。这段代码使用了一个满足Handler
接口的Proxy
结构获取的请求。main.go
文件使用了Handle
而不是其他地方使用的HandleFunc
。一旦请求可用,它就被修改为在请求中添加Proxy.BaseURL
,然后客户端进行分发。最后,响应被复制回ResponseWriter
接口。这包括所有标头、正文和状态。
如果需要,我们还可以添加一些额外的功能,比如基本的auth
请求,令牌管理等。这对于代理管理 JavaScript 或其他客户端应用程序的会话非常有用。
将 GRPC 导出为 JSON API
在第七章Web Clients and APIs的理解 GRPC 客户端配方中,我们编写了一个基本的 GRPC 服务器和客户端。这个配方将扩展这个想法,将常见的 RPC 函数放在一个包中,并将它们包装在一个 GRPC 服务器和一个标准的 Web 处理程序中。当您的 API 希望支持两种类型的客户端,但又不想为常见功能复制代码时,这将非常有用。
准备工作
根据以下步骤配置您的环境:
-
参考本章开头的技术要求部分中给出的步骤。
-
安装 GRPC (
grpc.io/docs/quickstart/go/
)并运行以下命令:
-
go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
-
go get -u google.golang.org/grpc
如何做...
以下步骤涵盖了编写和运行应用程序的过程:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter8/grpcjson
的新目录,并进入该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter8/grpcjson
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter8/grpcjson
-
从
~/projects/go-programming-cookbook-original/chapter8/grpcjson
复制测试,或者将其作为练习编写一些自己的代码! -
创建一个名为
keyvalue
的新目录并进入。 -
创建一个名为
keyvalue.proto
的文件,内容如下:
syntax = "proto3";
package keyvalue;
service KeyValue{
rpc Set(SetKeyValueRequest) returns (KeyValueResponse){}
rpc Get(GetKeyValueRequest) returns (KeyValueResponse){}
}
message SetKeyValueRequest {
string key = 1;
string value = 2;
}
message GetKeyValueRequest{
string key = 1;
}
message KeyValueResponse{
string success = 1;
string value = 2;
}
- 运行以下命令:
$ protoc --go_out=plugins=grpc:. keyvalue.proto
-
返回上一级目录。
-
创建一个名为
internal
的新目录。 -
创建一个名为
internal/keyvalue.go
的文件,内容如下:
package internal
import (
"golang.org/x/net/context"
"sync"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter8/grpcjson/keyvalue"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)
// KeyValue is a struct that holds a map
type KeyValue struct {
mutex sync.RWMutex
m map[string]string
}
// NewKeyValue initializes the KeyValue struct and its map
func NewKeyValue() *KeyValue {
return &KeyValue{
m: make(map[string]string),
}
}
// Set sets a value to a key, then returns the value
func (k *KeyValue) Set(ctx context.Context, r
*keyvalue.SetKeyValueRequest) (*keyvalue.KeyValueResponse,
error) {
k.mutex.Lock()
k.m[r.GetKey()] = r.GetValue()
k.mutex.Unlock()
return &keyvalue.KeyValueResponse{Value: r.GetValue()}, nil
}
// Get gets a value given a key, or say not found if
// it doesn't exist
func (k *KeyValue) Get(ctx context.Context, r
*keyvalue.GetKeyValueRequest) (*keyvalue.KeyValueResponse,
error) {
k.mutex.RLock()
defer k.mutex.RUnlock()
val, ok := k.m[r.GetKey()]
if !ok {
return nil, grpc.Errorf(codes.NotFound, "key not set")
}
return &keyvalue.KeyValueResponse{Value: val}, nil
}
-
创建一个名为
grpc
的新目录。 -
创建一个名为
grpc/main.go
的文件,内容如下:
package main
import (
"fmt"
"net"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter8/grpcjson/internal"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter8/grpcjson/keyvalue"
"google.golang.org/grpc"
)
func main() {
grpcServer := grpc.NewServer()
keyvalue.RegisterKeyValueServer(grpcServer,
internal.NewKeyValue())
lis, err := net.Listen("tcp", ":4444")
if err != nil {
panic(err)
}
fmt.Println("Listening on port :4444")
grpcServer.Serve(lis)
}
-
创建一个名为
http
的新目录。 -
创建一个名为
http/set.go
的文件,内容如下:
package main
import (
"encoding/json"
"net/http"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter8/grpcjson/internal"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter8/grpcjson/keyvalue"
"github.com/apex/log"
)
// Controller holds an internal KeyValueObject
type Controller struct {
*internal.KeyValue
}
// SetHandler wraps our GRPC Set
func (c *Controller) SetHandler(w http.ResponseWriter, r
*http.Request) {
var kv keyvalue.SetKeyValueRequest
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&kv); err != nil {
log.Errorf("failed to decode: %s", err.Error())
w.WriteHeader(http.StatusBadRequest)
return
}
gresp, err := c.Set(r.Context(), &kv)
if err != nil {
log.Errorf("failed to set: %s", err.Error())
w.WriteHeader(http.StatusInternalServerError)
return
}
resp, err := json.Marshal(gresp)
if err != nil {
log.Errorf("failed to marshal: %s", err.Error())
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write(resp)
}
- 创建一个名为
http/get.go
的文件,内容如下:
package main
import (
"encoding/json"
"net/http"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter8/grpcjson/keyvalue"
"github.com/apex/log"
)
// GetHandler wraps our RPC Get call
func (c *Controller) GetHandler(w http.ResponseWriter, r
*http.Request) {
key := r.URL.Query().Get("key")
kv := keyvalue.GetKeyValueRequest{Key: key}
gresp, err := c.Get(r.Context(), &kv)
if err != nil {
if grpc.Code(err) == codes.NotFound {
w.WriteHeader(http.StatusNotFound)
return
}
log.Errorf("failed to get: %s", err.Error())
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
resp, err := json.Marshal(gresp)
if err != nil {
log.Errorf("failed to marshal: %s", err.Error())
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Write(resp)
}
- 创建一个名为
http/main.go
的文件,内容如下:
package main
import (
"fmt"
"net/http"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter8/grpcjson/internal"
)
func main() {
c := Controller{KeyValue: internal.NewKeyValue()}
http.HandleFunc("/set", c.SetHandler)
http.HandleFunc("/get", c.GetHandler)
fmt.Println("Listening on port :3333")
err := http.ListenAndServe(":3333", nil)
panic(err)
}
- 运行
go run ./http
命令。您应该会看到以下输出:
$ go run ./http
Listening on port :3333
- 在单独的终端中,运行以下命令:
$ curl "http://localhost:3333/set" -d '{"key":"test",
"value":"123"}' -v $ curl "http://localhost:3333/get?key=badtest" -v $ curl "http://localhost:3333/get?key=test" -v
您应该会看到以下输出:
$ curl "http://localhost:3333/set" -d '{"key":"test",
"value":"123"}' -v
{"value":"123"}
$ curl "http://localhost:3333/get?key=badtest" -v
<should return a 404>
$ curl "http://localhost:3333/get?key=test" -v
{"value":"123"}
-
go.mod
文件可能已更新,go.sum
文件现在应该存在于顶层配方目录中。 -
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
尽管这个配方省略了客户端,但您可以复制第七章中理解 GRPC 客户端配方中的步骤,并且您应该会看到与我们的 curls 看到的相同的结果。http
和grpc
目录都使用相同的内部包。在这个包中,我们必须小心返回适当的 GRPC 错误代码,并将这些错误代码正确映射到我们的 HTTP 响应中。在这种情况下,我们使用codes.NotFound
,将其映射到http.StatusNotFound
。如果您需要处理多个错误,使用switch
语句可能比if...else
语句更合适。
您可能还注意到的另一件事是,GRPC 签名通常非常一致。它们接受一个请求并返回一个可选的响应和一个错误。如果您的 GRPC 调用足够重复,并且似乎很适合代码生成,那么可能可以创建一个通用的处理程序shim
;最终您可能会看到类似goadesign/goa
这样的包。
第九章:测试 Go 代码
这一章将与之前的章节不同;这一章将专注于测试和测试方法。Go 提供了出色的测试支持。但是,对于来自更动态语言的开发人员来说,理解它可能会有困难,因为猴子补丁和模拟相对来说比较简单。
Go 测试鼓励为您的代码使用特定的结构。特别是,测试和模拟接口非常简单并且得到了很好的支持。某些类型的代码可能更难测试。例如,对于使用包级全局变量的代码、尚未抽象为接口的地方以及具有非导出变量或方法的结构,测试可能更加困难。本章将分享一些测试 Go 代码的示例。
在本章中,我们将涵盖以下示例:
-
使用标准库进行模拟
-
使用 Mockgen 包来模拟接口
-
使用表驱动测试来提高覆盖率
-
使用第三方测试工具
-
使用 Go 进行行为测试
技术要求
为了继续本章中的所有示例,根据以下步骤配置您的环境:
-
从
golang.org/doc/install
下载并在您的操作系统上安装 Go 1.12.6 或更高版本。 -
打开一个终端或控制台应用程序,然后创建并导航到一个项目目录,例如
~/projects/go-programming-cookbook
。所有代码将从该目录运行和修改。 -
将最新的代码克隆到
~/projects/go-programming-cookbook-original
,您可以选择从该目录中工作,而不是手动输入示例:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
使用标准库进行模拟
在 Go 中,模拟通常意味着使用测试版本实现一个接口,允许您从测试中控制运行时行为。它也可能指模拟函数和方法,对于这一点,我们将在本示例中探讨另一个技巧。这个技巧使用了在play.golang.org/p/oLF1XnRX3C
定义的Patch
和Restore
函数。
一般来说,最好组合代码,以便您可以经常使用接口,并且代码是由小的、可测试的块组成的。包含大量分支条件或深度嵌套逻辑的代码可能很难测试,测试结果往往更加脆弱。这是因为开发人员需要在测试中跟踪更多的模拟对象、补丁、返回值和状态。
如何做...
这些步骤涵盖了编写和运行您的应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter9/mocking
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter9/mocking
您应该会看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter9/mocking
- 创建一个名为
mock.go
的文件,内容如下:
package mocking
// DoStuffer is a simple interface
type DoStuffer interface {
DoStuff(input string) error
}
- 创建一个名为
patch.go
的文件,内容如下:
package mocking
import "reflect"
// Restorer holds a function that can be used
// to restore some previous state.
type Restorer func()
// Restore restores some previous state.
func (r Restorer) Restore() {
r()
}
// Patch sets the value pointed to by the given destination to
// the given value, and returns a function to restore it to its
// original value. The value must be assignable to the element
//type of the destination.
func Patch(dest, value interface{}) Restorer {
destv := reflect.ValueOf(dest).Elem()
oldv := reflect.New(destv.Type()).Elem()
oldv.Set(destv)
valuev := reflect.ValueOf(value)
if !valuev.IsValid() {
// This isn't quite right when the destination type is
// not nilable, but it's better than the complex
// alternative.
valuev = reflect.Zero(destv.Type())
}
destv.Set(valuev)
return func() {
destv.Set(oldv)
}
}
- 创建一个名为
exec.go
的文件,内容如下:
package mocking
import "errors"
var ThrowError = func() error {
return errors.New("always fails")
}
func DoSomeStuff(d DoStuffer) error {
if err := d.DoStuff("test"); err != nil {
return err
}
if err := ThrowError(); err != nil {
return err
}
return nil
}
- 创建一个名为
mock_test.go
的文件,内容如下:
package mocking
type MockDoStuffer struct {
// closure to assist with mocking
MockDoStuff func(input string) error
}
func (m *MockDoStuffer) DoStuff(input string) error {
if m.MockDoStuff != nil {
return m.MockDoStuff(input)
}
// if we don't mock, return a common case
return nil
}
- 创建一个名为
exec_test.go
的文件,内容如下:
package mocking
import (
"errors"
"testing"
)
func TestDoSomeStuff(t *testing.T) {
tests := []struct {
name string
DoStuff error
ThrowError error
wantErr bool
}{
{"base-case", nil, nil, false},
{"DoStuff error", errors.New("failed"), nil, true},
{"ThrowError error", nil, errors.New("failed"), true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// An example of mocking an interface
// with our mock struct
d := MockDoStuffer{}
d.MockDoStuff = func(string) error {
return tt.DoStuff }
// mocking a function that is declared as a variable
// will not work for func A(),
// must be var A = func()
defer Patch(&ThrowError, func() error { return
tt.ThrowError }).Restore()
if err := DoSomeStuff(&d); (err != nil) != tt.wantErr
{
t.Errorf("DoSomeStuff() error = %v,
wantErr %v", err, tt.wantErr)
}
})
}
}
- 为剩余的函数填写测试,并进入上一级目录运行
go test
。确保所有测试都通过:
$go test
PASS
ok github.com/PacktPublishing/Go-Programming-Cookbook-Second-
Edition/chapter9/mocking 0.006s
go.mod
文件可能会被更新,顶级示例目录中现在应该存在go.sum
文件。
工作原理...
这个示例演示了如何模拟接口以及已声明为变量的函数。还有一些库可以直接模拟这些已声明函数的补丁/恢复,但它们绕过了很多 Go 的类型安全来实现这一功能。如果您需要对外部包中的函数进行补丁,可以使用以下技巧:
// Whatever package you wanna patch
import "github.com/package"
// This is patchable using the method described in this recipe
var packageDoSomething = package.DoSomething
对于这个示例,我们首先设置我们的测试并使用表驱动测试。关于这种技术有很多文献,比如github.com/golang/go/wiki/TableDrivenTests
,我建议进一步探索。一旦我们设置了测试,我们就为我们的模拟函数选择输出。为了模拟我们的接口,我们的模拟对象定义了可以在运行时重写的闭包。补丁/恢复技术被应用于在每次循环后更改我们的全局函数并恢复它。这要归功于t.Run
,它为测试的每次循环设置了一个新函数。
使用 Mockgen 包来模拟接口
前面的示例使用了我们的自定义模拟对象。当您使用大量接口时,编写这些内容可能会变得繁琐且容易出错。这是生成代码非常有意义的地方。幸运的是,有一个名为github.com/golang/mock/gomock
的包,它提供了模拟对象的生成,并为我们提供了一个非常有用的库,可以与接口测试一起使用。
这个示例将探讨gomock
的一些功能,并涵盖在何时、何地以及如何使用和生成模拟对象的权衡。
准备工作
根据以下步骤配置您的环境:
-
请参阅本章开头的技术要求部分。
-
运行
go get github.com/golang/mock/mockgen
命令。
如何做...
这些步骤涵盖了编写和运行应用程序的过程:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter9/mockgen
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter9/mockgen
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter9/mockgen
- 创建一个名为
interface.go
的文件,内容如下:
package mockgen
// GetSetter implements get a set of a
// key value pair
type GetSetter interface {
Set(key, val string) error
Get(key string) (string, error)
}
-
创建一个名为
internal
的目录。 -
运行
mockgen -destination internal/mocks.go -package internal github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter9/mockgen GetSetter
命令。这将创建一个名为internal/mocks.go
的文件。 -
创建一个名为
exec.go
的文件,内容如下:
package mockgen
// Controller is a struct demonstrating
// one way to initialize interfaces
type Controller struct {
GetSetter
}
// GetThenSet checks if a value is set. If not
// it sets it.
func (c *Controller) GetThenSet(key, value string) error {
val, err := c.Get(key)
if err != nil {
return err
}
if val != value {
return c.Set(key, value)
}
return nil
}
- 创建一个名为
interface_test.go
的文件,内容如下:
package mockgen
import (
"errors"
"testing"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter9/mockgen/internal"
"github.com/golang/mock/gomock"
)
func TestExample(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockGetSetter := internal.NewMockGetSetter(ctrl)
var k string
mockGetSetter.EXPECT().Get("we can put anything
here!").Do(func(key string) {
k = key
}).Return("", nil)
customError := errors.New("failed this time")
mockGetSetter.EXPECT().Get(gomock.Any()).Return("",
customError)
if _, err := mockGetSetter.Get("we can put anything
here!"); err != nil {
t.Errorf("got %#v; want %#v", err, nil)
}
if k != "we can put anything here!" {
t.Errorf("bad key")
}
if _, err := mockGetSetter.Get("key"); err == nil {
t.Errorf("got %#v; want %#v", err, customError)
}
}
- 创建一个名为
exec_test.go
的文件,内容如下:
package mockgen
import (
"errors"
"testing"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter9/mockgen/internal"
"github.com/golang/mock/gomock"
)
func TestController_Set(t *testing.T) {
tests := []struct {
name string
getReturnVal string
getReturnErr error
setReturnErr error
wantErr bool
}{
{"get error", "value", errors.New("failed"), nil,
true},
{"value match", "value", nil, nil, false},
{"no errors", "not set", nil, nil, false},
{"set error", "not set", nil, errors.New("failed"),
true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockGetSetter := internal.NewMockGetSetter(ctrl)
mockGetSetter.EXPECT().Get("key").AnyTimes()
.Return(tt.getReturnVal, tt.getReturnErr)
mockGetSetter.EXPECT().Set("key",
gomock.Any()).AnyTimes().Return(tt.setReturnErr)
c := &Controller{
GetSetter: mockGetSetter,
}
if err := c.GetThenSet("key", "value"); (err !=
nil) != tt.wantErr {
t.Errorf("Controller.Set() error = %v, wantErr
%v", err, tt.wantErr)
}
})
}
}
-
为剩余的函数填写测试,返回上一级目录,并运行
go test
。确保所有测试都通过。 -
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶级配方目录中。
工作原理...
生成的模拟对象允许测试指定预期的参数、函数将被调用的次数以及返回的内容。它们还允许我们设置额外的工件。例如,如果原始函数具有类似的工作流程,我们可以直接写入通道。interface_test.go
文件展示了在调用模拟对象时使用一些示例。通常,测试看起来更像exec_test.go
,在这里我们希望拦截实际代码执行的接口函数调用,并在测试时更改它们的行为。
exec_test.go
文件还展示了如何在表驱动测试环境中使用模拟对象。Any()
函数表示模拟函数可以被调用零次或多次,这对于代码提前终止的情况非常有用。
在这个示例中演示的最后一个技巧是将模拟对象放入internal
包中。当您需要模拟在您自己之外的包中声明的函数时,这是很有用的。这允许这些方法在non _test.go
文件中定义,但它们对您的库的用户不可见,因为他们无法从内部包导入。通常,最容易的方法是将模拟对象放入与您当前编写的测试相同的包名的_test.go
文件中。
使用表驱动测试来提高覆盖率
这个示例将演示如何编写表驱动测试、收集测试覆盖率并改进它的过程。它还将使用github.com/cweill/gotests
包来生成测试。如果您已经下载了其他章节的测试代码,这些内容应该会很熟悉。通过结合这个示例和前两个示例,您应该能够在所有情况下通过一些工作实现 100%的测试覆盖率。
如何做...
这些步骤涵盖了编写和运行应用程序的过程:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter9/coverage
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter9/coverage
您应该会看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter9/coverage
- 创建一个名为
coverage.go
的文件,内容如下:
package main
import "errors"
// Coverage is a simple function with some branching conditions
func Coverage(condition bool) error {
if condition {
return errors.New("condition was set")
}
return nil
}
-
运行
gotests -all -w
命令。 -
这将生成一个名为
coverage_test.go
的文件,内容如下:
package main
import "testing"
func TestCoverage(t *testing.T) {
type args struct {
condition bool
}
tests := []struct {
name string
args args
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := Coverage(tt.args.condition); (err != nil)
!= tt.wantErr {
t.Errorf("Coverage() error = %v, wantErr %v",
err, tt.wantErr)
}
})
}
}
- 使用以下内容填写
TODO
部分:
{"no condition", args{true}, true},
- 运行
go test -cover
命令,您将看到以下输出:
$ go test -cover
PASS
coverage: 66.7% of statements
ok github.com/PacktPublishing/Go-Programming-Cookbook-Second-
Edition/chapter9/coverage 0.007s
- 将以下内容添加到
TODO
部分:
{"condition", args{false}, false},
- 运行
go test -cover
命令,您将看到以下输出:
$ go test -cover
PASS
coverage: 100.0% of statements
ok github.com/PacktPublishing/Go-Programming-Cookbook-Second-
Edition/chapter9/coverage 0.007s
- 运行以下命令:
$ go test -coverprofile=cover.out
$ go tool cover -html=cover.out -o coverage.html
-
在浏览器中打开
coverage.html
文件,以查看图形覆盖报告。 -
go.mod
文件可能会被更新,顶级示例目录中现在应该存在go.sum
文件。
它是如何工作的...
go test -cover
命令是基本的 Go 安装中自带的。它可以用来收集您的 Go 应用程序的覆盖报告。此外,它还可以输出覆盖度指标和 HTML 覆盖报告。这个工具通常被其他工具包装,下一个示例将介绍这些内容。这些表驱动测试样式在github.com/golang/go/wiki/TableDrivenTests
中有介绍,是一种优秀的方式,可以处理许多情况而不需要编写大量额外的代码。
这个示例首先通过自动生成测试代码,然后根据需要填写测试用例来帮助创建更多的覆盖。唯一特别棘手的时候是当您调用非变量函数或方法时。例如,让gob.Encode()
返回一个错误以增加测试覆盖率可能会很棘手。使用本章的使用标准库进行模拟示例中描述的方法,并使用var gobEncode = gob.Encode
来允许打补丁,也可能看起来有些古怪。因此,很难主张 100%的测试覆盖率,而是主张集中测试外部接口的广泛性——也就是测试输入和输出的许多变化,有时,正如我们将在本章的使用 Go 进行行为测试示例中看到的那样,模糊测试可能会变得有用。
使用第三方测试工具
有许多有用的 Go 测试工具:可以更轻松地了解每个函数级别的代码覆盖情况的工具,可以实现断言以减少测试代码行数的工具,以及测试运行器。这个示例将介绍github.com/axw/gocov
和github.com/smartystreets/goconvey
包,以演示其中一些功能。根据您的需求,还有许多其他值得注意的测试框架。github.com/smartystreets/goconvey
包支持断言和是一个测试运行器。在 Go 1.7 之前,这是最干净的方法来拥有带标签的子测试。
准备工作
根据以下步骤配置您的环境:
-
请参考本章开头的技术要求部分。
-
运行
go get github.com/axw/gocov/gocov
命令。 -
运行
go get github.com/smartystreets/goconvey
命令。
如何做...
这些步骤涵盖了编写和运行应用程序的过程:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter9/tools
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter9/tools
你应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter9/tools
- 创建一个名为
funcs.go
的文件,内容如下:
package tools
import (
"fmt"
)
func example() error {
fmt.Println("in example")
return nil
}
var example2 = func() int {
fmt.Println("in example2")
return 10
}
- 创建一个名为
structs.go
的文件,内容如下:
package tools
import (
"errors"
"fmt"
)
type c struct {
Branch bool
}
func (c *c) example3() error {
fmt.Println("in example3")
if c.Branch {
fmt.Println("branching code!")
return errors.New("bad branch")
}
return nil
}
- 创建一个名为
funcs_test.go
的文件,内容如下:
package tools
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func Test_example(t *testing.T) {
tests := []struct {
name string
}{
{"base-case"},
}
for _, tt := range tests {
Convey(tt.name, t, func() {
res := example()
So(res, ShouldBeNil)
})
}
}
func Test_example2(t *testing.T) {
tests := []struct {
name string
}{
{"base-case"},
}
for _, tt := range tests {
Convey(tt.name, t, func() {
res := example2()
So(res, ShouldBeGreaterThanOrEqualTo, 1)
})
}
}
- 创建一个名为
structs_test.go
的文件,内容如下:
package tools
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func Test_c_example3(t *testing.T) {
type fields struct {
Branch bool
}
tests := []struct {
name string
fields fields
wantErr bool
}{
{"no branch", fields{false}, false},
{"branch", fields{true}, true},
}
for _, tt := range tests {
Convey(tt.name, t, func() {
c := &c{
Branch: tt.fields.Branch,
}
So((c.example3() != nil), ShouldEqual, tt.wantErr)
})
}
}
- 运行
gocov test | gocov report
命令,你会看到以下输出:
$ gocov test | gocov report
ok github.com/PacktPublishing/Go-Programming-Cookbook-Second-
Edition/chapter9/tools 0.006s
coverage: 100.0% of statements
github.com/PacktPublishing/Go-Programming-Cookbook-Second-
Edition/chapter9/tools/struct.go
c.example3 100.00% (5/5)
github.com/PacktPublishing/Go-Programming-Cookbook-Second-
Edition/chapter9/tools/funcs.go example
100.00% (2/2)
github.com/PacktPublishing/Go-Programming-Cookbook-Second-
Edition/chapter9/tools/funcs.go @12:16
100.00% (2/2)
github.com/PacktPublishing/Go-Programming-Cookbook-Second-
Edition/chapter9/tools ----------
100.00% (9/9)
Total Coverage: 100.00% (9/9)
- 运行
goconvey
命令,它将打开一个看起来像这样的浏览器:
-
确保所有测试都通过。
-
go.mod
文件可能已更新,go.sum
文件现在应该存在于顶级配方目录中。
它是如何工作的...
本教程演示了如何将goconvey
命令与你的测试连接起来。Convey
关键字基本上取代了t.Run
,并在goconvey
Web UI 中添加了额外的标签,但它的行为略有不同。如果你有嵌套的Convey
块,它们总是按顺序重新执行,如下所示:
Convey("Outer loop", t, func(){
a := 1
Convey("Inner loop", t, func() {
a = 2
})
Convey ("Inner loop2", t, func(){
fmt.Println(a)
})
})
使用goconvey
命令,上面的代码将打印1
。如果我们使用内置的t.Run
,它将打印2
。换句话说,Go 的t.Run
测试是顺序运行的,永远不会重复。这种行为对于将设置代码放入外部Convey
块非常有用,但如果你必须同时使用,记住这个区别是很重要的。
当使用Convey
断言时,在 Web UI 和额外的统计信息中会有成功的勾号。它还可以将检查的大小减少到一行,甚至可以创建自定义断言。
如果你保持goconvey
Web 界面打开并打开通知,当你保存代码时,测试将自动运行,并且你将收到有关覆盖率增加或减少以及构建失败的通知。
所有三个工具断言、测试运行器和 Web UI 都可以独立或一起使用。
gocov
工具在提高测试覆盖率时非常有用。它可以快速识别缺乏覆盖的函数,并帮助你深入了解你的覆盖报告。此外,gocov
还可以用来生成一个随 Go 代码一起提供的替代 HTML 报告,使用github.com/matm/gocov-html
包。
使用 Go 进行行为测试
行为测试或集成测试是实现端到端黑盒测试的一种好方法。这种类型测试的一个流行框架是 Cucumber(cucumber.io/
),它使用 Gherkin 语言来描述测试的步骤,然后在代码中实现这些步骤。Go 也有一个 Cucumber 库(github.com/DATA-DOG/godog
)。本教程将使用godog
包来编写行为测试。
准备就绪
根据以下步骤配置你的环境:
-
请参考本章开头的技术要求部分。
-
运行
go get github.com/DATA-DOG/godog/cmd/godog
命令。
如何做...
这些步骤涵盖了编写和运行你的应用程序:
-
从你的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter9/bdd
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter9/bdd
你应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter9/bdd
- 创建一个名为
handler.go
的文件,内容如下:
package bdd
import (
"encoding/json"
"fmt"
"net/http"
)
// HandlerRequest will be json decoded
// into by Handler
type HandlerRequest struct {
Name string `json:"name"`
}
// Handler takes a request and renders a response
func Handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
dec := json.NewDecoder(r.Body)
var req HandlerRequest
if err := dec.Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(fmt.Sprintf("BDD testing %s", req.Name)))
}
- 创建一个名为
features
的新目录,并创建一个名为features/handler.go
的文件,内容如下:
Feature: Bad Method
Scenario: Good request
Given we create a HandlerRequest payload with:
| reader |
| coder |
| other |
And we POST the HandlerRequest to /hello
Then the response code should be 200
And the response body should be:
| BDD testing reader |
| BDD testing coder |
| BDD testing other |
- 运行
godog
命令,你会看到以下输出:
$ godog
.
1 scenarios (1 undefined)
4 steps (4 undefined)
89.062µs
.
- 这将为你提供一个骨架来实现我们在特性文件中编写的测试;将它们复制到
handler_test.go
中并实现前两个步骤:
package bdd
import (
"bytes"
"encoding/json"
"fmt"
"net/http/httptest"
"github.com/DATA-DOG/godog"
"github.com/DATA-DOG/godog/gherkin"
)
var payloads []HandlerRequest
var resps []*httptest.ResponseRecorder
func weCreateAHandlerRequestPayloadWith(arg1
*gherkin.DataTable) error {
for _, row := range arg1.Rows {
h := HandlerRequest{
Name: row.Cells[0].Value,
}
payloads = append(payloads, h)
}
return nil
}
func wePOSTTheHandlerRequestToHello() error {
for _, p := range payloads {
v, err := json.Marshal(p)
if err != nil {
return err
}
w := httptest.NewRecorder()
r := httptest.NewRequest("POST", "/hello",
bytes.NewBuffer(v))
Handler(w, r)
resps = append(resps, w)
}
return nil
}
- 运行
godog
命令,你会看到以下输出:
$ godog
.
1 scenarios (1 pending)
4 steps (2 passed, 1 pending, 1 skipped)
.
- 填写剩下的两个步骤:
func theResponseCodeShouldBe(arg1 int) error {
for _, r := range resps {
if got, want := r.Code, arg1; got != want {
return fmt.Errorf("got: %d; want %d", got, want)
}
}
return nil
}
func theResponseBodyShouldBe(arg1 *gherkin.DataTable) error {
for c, row := range arg1.Rows {
b := bytes.Buffer{}
b.ReadFrom(resps[c].Body)
if got, want := b.String(), row.Cells[0].Value;
got != want
{
return fmt.Errorf("got: %s; want %s", got, want)
}
}
return nil
}
func FeatureContext(s *godog.Suite) {
s.Step(`^we create a HandlerRequest payload with:$`,
weCreateAHandlerRequestPayloadWith)
s.Step(`^we POST the HandlerRequest to /hello$`,
wePOSTTheHandlerRequestToHello)
s.Step(`^the response code should be (d+)$`,
theResponseCodeShouldBe)
s.Step(`^the response body should be:$`,
theResponseBodyShouldBe)
}
- 运行
godog
命令,你会看到以下输出:
$ godog
.
1 scenarios (1 passed)
4 steps (4 passed)
552.605µs
.
它是如何工作的...
Cucumber 框架非常适用于配对编程、端到端测试以及任何需要通过书面说明进行最佳沟通并且非技术人员可以理解的测试。一旦一个步骤被实现,通常可以在需要的任何地方重复使用它。如果您想要测试服务之间的集成,可以编写测试来使用实际的 HTTP 客户端,只要首先确保您的环境已设置为接收 HTTP 连接。
Datadog 对行为驱动开发(BDD)的实现缺少一些功能,如果您曾经使用过其他 Cucumber 框架,可能会期望这些功能,包括缺乏示例、在函数之间传递上下文,以及许多其他关键字。然而,这是一个很好的开始,通过在这个配方中使用一些技巧,比如使用全局变量来跟踪状态(并确保在场景之间清理这些全局变量),可以构建一个相当健壮的测试集。Datadog 测试包还使用了第三方测试运行器,因此无法与诸如gocov
或go test -cover
等包一起使用。
第十章:并行和并发
本章中的示例涵盖了工作池、异步操作的等待组以及context
包的使用。并行和并发是 Go 语言最广告和推广的特性之一。本章将提供一些有用的模式,帮助您入门并了解这些特性。
Go 提供了使并行应用程序成为可能的原语。Goroutines 允许任何函数变成异步和并发的。通道允许应用程序与 Goroutines 建立通信。Go 语言中有一句著名的话是:“不要通过共享内存进行通信;相反,通过通信共享内存”,出自blog.golang.org/share-memory-by-communicating
。
在本章中,我们将涵盖以下示例:
-
使用通道和 select 语句
-
使用 sync.WaitGroup 执行异步操作
-
使用原子操作和互斥锁
-
使用上下文包
-
执行通道的状态管理
-
使用工作池设计模式
-
使用工作进程创建管道
技术要求
为了继续本章中的所有示例,请按照以下步骤配置您的环境:
-
在您的操作系统上下载并安装 Go 1.12.6 或更高版本,网址为
golang.org/doc/install
。 -
打开终端或控制台应用程序,并创建并转到一个项目目录,例如
~/projects/go-programming-cookbook
。所有的代码都将在这个目录中运行和修改。 -
将最新的代码克隆到
~/projects/go-programming-cookbook-original
,(可选)从该目录中工作,而不是手动输入示例:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
使用通道和 select 语句
Go 通道与 Goroutines 结合使用,是异步通信的一等公民。当我们使用 select 语句时,通道变得特别强大。这些语句允许 Goroutine 智能地处理来自多个通道的请求。
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter10/channels
的新目录,并转到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter10/channels
您应该看到一个名为go.mod
的文件,其中包含以下代码:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter10/channels
-
复制
~/projects/go-programming-cookbook-original/chapter10/channels
中的测试,或者利用这个机会编写一些您自己的代码! -
创建一个名为
sender.go
的文件,内容如下:
package channels
import "time"
// Sender sends "tick"" on ch until done is
// written to, then it sends "sender done."
// and exits
func Sender(ch chan string, done chan bool) {
t := time.Tick(100 * time.Millisecond)
for {
select {
case <-done:
ch <- "sender done."
return
case <-t:
ch <- "tick"
}
}
}
- 创建一个名为
printer.go
的文件,内容如下:
package channels
import (
"context"
"fmt"
"time"
)
// Printer will print anything sent on the ch chan
// and will print tock every 200 milliseconds
// this will repeat forever until a context is
// Done, i.e. timed out or cancelled
func Printer(ctx context.Context, ch chan string) {
t := time.Tick(200 * time.Millisecond)
for {
select {
case <-ctx.Done():
fmt.Println("printer done.")
return
case res := <-ch:
fmt.Println(res)
case <-t:
fmt.Println("tock")
}
}
}
-
创建一个名为
example
的新目录,并转到该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"context"
"time"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter10/channels"
)
func main() {
ch := make(chan string)
done := make(chan bool)
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
go channels.Printer(ctx, ch)
go channels.Sender(ch, done)
time.Sleep(2 * time.Second)
done <- true
cancel()
//sleep a bit extra so channels can clean up
time.Sleep(3 * time.Second)
}
-
运行
go run main.go
。 -
您还可以运行以下命令:
$ go build $ ./example
您现在应该看到以下输出,但打印顺序可能会有所不同:
$ go run main.go
tick
tock
tick
tick
tock
tick
tick
tock
tick
.
.
.
sender done.
printer done.
-
go.mod
文件可能会被更新,顶级示例目录中现在应该存在go.sum
文件。 -
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
此示例演示了启动读取或写入通道的工作进程的两种方法,并且可能同时执行两者。当写入done
通道或通过调用取消函数或超时取消context
时,工作进程将终止。使用上下文包示例将更详细地介绍context
包。
main
包用于将各个函数连接在一起;由于这一点,可以设置多个成对,只要通道不共享。除此之外,可以有多个 Goroutines 监听同一个通道,我们将在使用工作池设计模式示例中探讨。
最后,由于 Goroutines 的异步性质,建立清理和终止条件可能会很棘手;例如,一个常见的错误是执行以下操作:
select{
case <-time.Tick(200 * time.Millisecond):
//this resets whenever any other 'lane' is chosen
}
通过将Tick
放在select
语句中,可以防止这种情况发生。在select
语句中也没有简单的方法来优先处理流量。
使用 sync.WaitGroup 执行异步操作
有时,异步执行一些操作并等待它们完成是有用的。例如,如果一个操作需要从多个 API 中提取信息并聚合该信息,那么将这些客户端请求异步化将会很有帮助。这个示例将探讨如何使用sync.WaitGroup
来编排并行的非依赖任务。
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter10/waitgroup
的新目录,并转到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter10/waitgroup
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter10/waitgroup
-
从
~/projects/go-programming-cookbook-original/chapter10/waitgroup
复制测试,或者利用这个机会编写一些自己的代码! -
创建一个名为
tasks.go
的文件,其中包含以下内容:
package waitgroup
import (
"fmt"
"log"
"net/http"
"strings"
"time"
)
// GetURL gets a url, and logs the time it took
func GetURL(url string) (*http.Response, error) {
start := time.Now()
log.Printf("getting %s", url)
resp, err := http.Get(url)
log.Printf("completed getting %s in %s", url,
time.Since(start))
return resp, err
}
// CrawlError is our custom error type
// for aggregating errors
type CrawlError struct {
Errors []string
}
// Add adds another error
func (c *CrawlError) Add(err error) {
c.Errors = append(c.Errors, err.Error())
}
// Error implements the error interface
func (c *CrawlError) Error() string {
return fmt.Sprintf("All Errors: %s", strings.Join(c.Errors,
","))
}
// Present can be used to determine if
// we should return this
func (c *CrawlError) Present() bool {
return len(c.Errors) != 0
}
- 创建一个名为
process.go
的文件,其中包含以下内容:
package waitgroup
import (
"log"
"sync"
"time"
)
// Crawl collects responses from a list of urls
// that are passed in. It waits for all requests
// to complete before returning.
func Crawl(sites []string) ([]int, error) {
start := time.Now()
log.Printf("starting crawling")
wg := &sync.WaitGroup{}
var resps []int
cerr := &CrawlError{}
for _, v := range sites {
wg.Add(1)
go func(v string) {
defer wg.Done()
resp, err := GetURL(v)
if err != nil {
cerr.Add(err)
return
}
resps = append(resps, resp.StatusCode)
}(v)
}
wg.Wait()
// we encountered a crawl error
if cerr.Present() {
return resps, cerr
}
log.Printf("completed crawling in %s", time.Since(start))
return resps, nil
}
-
创建一个名为
example
的新目录,并转到该目录。 -
创建一个名为
main.go
的文件,其中包含以下内容:
package main
import (
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter10/waitgroup"
)
func main() {
sites := []string{
"https://golang.org",
"https://godoc.org",
"https://www.google.com/search?q=golang",
}
resps, err := waitgroup.Crawl(sites)
if err != nil {
panic(err)
}
fmt.Println("Resps received:", resps)
}
-
运行
go run main.go
。 -
您还可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
2017/04/05 19:45:07 starting crawling
2017/04/05 19:45:07 getting https://www.google.com/search?
q=golang
2017/04/05 19:45:07 getting https://golang.org
2017/04/05 19:45:07 getting https://godoc.org
2017/04/05 19:45:07 completed getting https://golang.org in
178.22407ms
2017/04/05 19:45:07 completed getting https://godoc.org in
181.400873ms
2017/04/05 19:45:07 completed getting
https://www.google.com/search?q=golang in 238.019327ms
2017/04/05 19:45:07 completed crawling in 238.191791ms
Resps received: [200 200 200]
-
go.mod
文件可能会更新,顶级配方目录中现在应该存在go.sum
文件。 -
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
这个示例向您展示了如何在等待工作时使用waitgroups
作为同步机制。实质上,waitgroup.Wait()
将等待其内部计数器达到0
。waitgroup.Add(int)
方法将按输入的数量递增计数器,waitgroup.Done()
将递减计数器1
。因此,必须异步Wait()
,而各种 Goroutines 标记waitgroup
为Done()
。
在这个示例中,我们在分派每个 HTTP 请求之前递增,然后调用 defer wg.Done()
方法,这样我们就可以在 Goroutine 终止时递减。然后我们等待所有 Goroutines 完成,然后返回我们聚合的结果。
实际上,最好使用通道来传递错误和响应。
在执行此类异步操作时,您应该考虑诸如修改共享映射之类的事物的线程安全性。如果您记住这一点,waitgroups
是等待任何类型的异步操作的有用功能。
使用原子操作和互斥
在诸如 Go 之类的语言中,您可以构建异步操作和并行性,考虑诸如线程安全之类的事情变得很重要。例如,同时从多个 Goroutines 访问映射是危险的。Go 在sync
和sync/atomic
包中提供了许多辅助工具,以确保某些事件仅发生一次,或者 Goroutines 可以在操作上进行序列化。
这个示例将演示使用这些包来安全地修改具有各种 Goroutines 的映射,并保持可以被多个 Goroutines 安全访问的全局序数值。它还将展示Once.Do
方法,该方法可用于确保 Go 应用程序只执行一次某些操作,例如读取配置文件或初始化变量。
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter10/atomic
的新目录,并转到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter10/atomic
你应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter10/atomic
-
从
~/projects/go-programming-cookbook-original/chapter10/atomic
复制测试,或者利用这个机会编写一些自己的代码! -
创建一个名为
map.go
的文件,内容如下:
package atomic
import (
"errors"
"sync"
)
// SafeMap uses a mutex to allow
// getting and setting in a thread-safe way
type SafeMap struct {
m map[string]string
mu *sync.RWMutex
}
// NewSafeMap creates a SafeMap
func NewSafeMap() SafeMap {
return SafeMap{m: make(map[string]string), mu:
&sync.RWMutex{}}
}
// Set uses a write lock and sets the value given
// a key
func (t *SafeMap) Set(key, value string) {
t.mu.Lock()
defer t.mu.Unlock()
t.m[key] = value
}
// Get uses a RW lock and gets the value if it exists,
// otherwise an error is returned
func (t *SafeMap) Get(key string) (string, error) {
t.mu.RLock()
defer t.mu.RUnlock()
if v, ok := t.m[key]; ok {
return v, nil
}
return "", errors.New("key not found")
}
- 创建一个名为
ordinal.go
的文件,内容如下:
package atomic
import (
"sync"
"sync/atomic"
)
// Ordinal holds a global a value
// and can only be initialized once
type Ordinal struct {
ordinal uint64
once *sync.Once
}
// NewOrdinal returns ordinal with once
// setup
func NewOrdinal() *Ordinal {
return &Ordinal{once: &sync.Once{}}
}
// Init sets the ordinal value
// can only be done once
func (o *Ordinal) Init(val uint64) {
o.once.Do(func() {
atomic.StoreUint64(&o.ordinal, val)
})
}
// GetOrdinal will return the current
// ordinal
func (o *Ordinal) GetOrdinal() uint64 {
return atomic.LoadUint64(&o.ordinal)
}
// Increment will increment the current
// ordinal
func (o *Ordinal) Increment() {
atomic.AddUint64(&o.ordinal, 1)
}
-
创建一个名为
example
的新目录并进入。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"fmt"
"sync"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter10/atomic"
)
func main() {
o := atomic.NewOrdinal()
m := atomic.NewSafeMap()
o.Init(1123)
fmt.Println("initial ordinal is:", o.GetOrdinal())
wg := sync.WaitGroup{}
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
m.Set(fmt.Sprint(i), "success")
o.Increment()
}(i)
}
wg.Wait()
for i := 0; i < 10; i++ {
v, err := m.Get(fmt.Sprint(i))
if err != nil || v != "success" {
panic(err)
}
}
fmt.Println("final ordinal is:", o.GetOrdinal())
fmt.Println("all keys found and marked as: 'success'")
}
-
运行
go run main.go
。 -
你也可以运行以下命令:
$ go build $ ./example
现在你应该看到以下输出:
$ go run main.go
initial ordinal is: 1123
final ordinal is: 1133
all keys found and marked as: 'success'
-
go.mod
文件可能已更新,go.sum
文件现在应该存在于顶级配方目录中。 -
如果你复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
对于我们的 map 配方,我们使用了ReadWrite
互斥锁。这个互斥锁的思想是任意数量的读取者可以获取读取锁,但只有一个写入者可以获取写入锁。此外,当其他人(读取者或写入者)拥有锁时,写入者不能获取锁。这很有用,因为读取非常快速且非阻塞,与标准互斥锁相比。每当我们想要设置数据时,我们使用Lock()
对象,每当我们想要读取数据时,我们使用RLock()
。关键是你最终要使用Unlock()
或RUnlock()
,这样你就不会使你的应用程序死锁。延迟的Unlock()
对象可能很有用,但可能比手动调用Unlock()
慢。
当你想要将额外的操作与锁定的值分组时,这种模式可能不够灵活。例如,在某些情况下,你可能想要锁定,进行一些额外的处理,只有在完成这些处理后才解锁。对于你的设计来说,考虑这一点是很重要的。
sync/atmoic
包被Ordinal
用来获取和设置值。还有原子比较操作,比如atomic.CompareAndSwapUInt64()
,非常有价值。这个配方允许只能在Ordinal
对象上调用Init
一次;否则,它只能被原子地递增。
我们循环创建 10 个 Goroutines(与sync.Waitgroup
同步),并展示序数正确递增了 10 次,我们的 map 中的每个键都被适当地设置。
使用上下文包
本书中的几个配方都使用了context
包。这个配方将探讨创建和管理上下文的基础知识。理解上下文的一个很好的参考是blog.golang.org/context
。自从写这篇博客以来,上下文已经从net/context
移动到一个叫做context
的包中。这在与 GRPC 等第三方库交互时仍然偶尔会引起问题。
这个配方将探讨为上下文设置和获取值,取消和超时。
如何做...
这些步骤涵盖了编写和运行你的应用程序:
-
从你的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter10/context
的新目录并进入。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter10/context
你应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter10/context
-
从
~/projects/go-programming-cookbook-original/chapter10/context
复制测试,或者利用这个机会编写一些自己的代码! -
创建一个名为
values.go
的文件,内容如下:
package context
import "context"
type key string
const (
timeoutKey key = "TimeoutKey"
deadlineKey key = "DeadlineKey"
)
// Setup sets some values
func Setup(ctx context.Context) context.Context {
ctx = context.WithValue(ctx, timeoutKey,
"timeout exceeded")
ctx = context.WithValue(ctx, deadlineKey,
"deadline exceeded")
return ctx
}
// GetValue grabs a value given a key and
// returns a string representation of the
// value
func GetValue(ctx context.Context, k key) string {
if val, ok := ctx.Value(k).(string); ok {
return val
}
return ""
}
- 创建一个名为
exec.go
的文件,内容如下:
package context
import (
"context"
"fmt"
"math/rand"
"time"
)
// Exec sets two random timers and prints
// a different context value for whichever
// fires first
func Exec() {
// a base context
ctx := context.Background()
ctx = Setup(ctx)
rand.Seed(time.Now().UnixNano())
timeoutCtx, cancel := context.WithTimeout(ctx,
(time.Duration(rand.Intn(2)) * time.Millisecond))
defer cancel()
deadlineCtx, cancel := context.WithDeadline(ctx,
time.Now().Add(time.Duration(rand.Intn(2))
*time.Millisecond))
defer cancel()
for {
select {
case <-timeoutCtx.Done():
fmt.Println(GetValue(ctx, timeoutKey))
return
case <-deadlineCtx.Done():
fmt.Println(GetValue(ctx, deadlineKey))
return
}
}
}
-
创建一个名为
example
的新目录并进入。 -
创建一个名为
main.go
的文件,内容如下:
package main
import "github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter10/context"
func main() {
context.Exec()
}
-
运行
go run main.go
。 -
你也可以运行以下命令:
$ go build $ ./example
现在你应该看到以下输出:
$ go run main.go
timeout exceeded
OR
$ go run main.go
deadline exceeded
-
go.mod
文件可能已更新,go.sum
文件现在应该存在于顶级配方目录中。 -
如果你复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
当使用上下文值时,最好创建一个新类型来表示键。在这种情况下,我们创建了一个key
类型,然后声明了一些对应的const
值来表示所有可能的键。
在这种情况下,我们使用Setup()
函数同时初始化所有的键/值对。在修改上下文时,函数通常需要一个context
参数并返回一个context
值。因此,签名通常如下所示:
func ModifyContext(ctx context.Context) context.Context
有时,这些方法还会返回错误或cancel()
函数,例如context.WithCancel
、context.WithTimeout
和context.WithDeadline
的情况。所有子上下文都继承父上下文的属性。
在这个示例中,我们创建了两个子上下文,一个带有截止日期,一个带有超时。我们将这些超时设置为随机范围,然后在接收到任何一个超时时终止。最后,我们提取了给定键的值并打印出来。
执行通道的状态管理
在 Go 中,通道可以是任何类型。结构体通道允许您通过单个消息传递大量状态。本示例将探讨使用通道传递复杂请求结构并在复杂响应结构中返回它们的结果。
在下一个示例中,使用工作池设计模式,这种价值变得更加明显,因为您可以创建能够执行各种任务的通用工作程序。
如何做...
这些步骤涵盖了编写和运行应用程序的过程:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter10/state
的新目录并进入该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter10/state
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter10/state
-
复制
~/projects/go-programming-cookbook-original/chapter10/state
中的测试,或者利用这个机会编写一些自己的代码! -
创建一个名为
state.go
的文件,内容如下:
package state
type op string
const (
// Add values
Add op = "add"
// Subtract values
Subtract = "sub"
// Multiply values
Multiply = "mult"
// Divide values
Divide = "div"
)
// WorkRequest perform an op
// on two values
type WorkRequest struct {
Operation op
Value1 int64
Value2 int64
}
// WorkResponse returns the result
// and any errors
type WorkResponse struct {
Wr *WorkRequest
Result int64
Err error
}
- 创建一个名为
processor.go
的文件,内容如下:
package state
import "context"
// Processor routes work to Process
func Processor(ctx context.Context, in chan *WorkRequest, out
chan *WorkResponse) {
for {
select {
case <-ctx.Done():
return
case wr := <-in:
out <- Process(wr)
}
}
}
- 创建一个名为
process.go
的文件,内容如下:
package state
import "errors"
// Process switches on operation type
// Then does work
func Process(wr *WorkRequest) *WorkResponse {
resp := WorkResponse{Wr: wr}
switch wr.Operation {
case Add:
resp.Result = wr.Value1 + wr.Value2
case Subtract:
resp.Result = wr.Value1 - wr.Value2
case Multiply:
resp.Result = wr.Value1 * wr.Value2
case Divide:
if wr.Value2 == 0 {
resp.Err = errors.New("divide by 0")
break
}
resp.Result = wr.Value1 / wr.Value2
default:
resp.Err = errors.New("unsupported operation")
}
return &resp
}
-
创建一个名为
example
的新目录并进入该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"context"
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter10/state"
)
func main() {
in := make(chan *state.WorkRequest, 10)
out := make(chan *state.WorkResponse, 10)
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
go state.Processor(ctx, in, out)
req := state.WorkRequest{state.Add, 3, 4}
in <- &req
req2 := state.WorkRequest{state.Subtract, 5, 2}
in <- &req2
req3 := state.WorkRequest{state.Multiply, 9, 9}
in <- &req3
req4 := state.WorkRequest{state.Divide, 8, 2}
in <- &req4
req5 := state.WorkRequest{state.Divide, 8, 0}
in <- &req5
for i := 0; i < 5; i++ {
resp := <-out
fmt.Printf("Request: %v; Result: %v, Error: %vn",
resp.Wr, resp.Result, resp.Err)
}
}
-
运行
go run main.go
。 -
您还可以运行以下命令:
$ go build $ ./example
现在应该看到以下输出:
$ go run main.go
Request: &{add 3 4}; Result: 7, Error: <nil>
Request: &{sub 5 2}; Result: 3, Error: <nil>
Request: &{mult 9 9}; Result: 81, Error: <nil>
Request: &{div 8 2}; Result: 4, Error: <nil>
Request: &{div 8 0}; Result: 0, Error: divide by 0
-
go.mod
文件可能会被更新,顶级示例目录中现在应该存在go.sum
文件。 -
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
本示例中的Processor()
函数是一个循环函数,直到其上下文被取消为止,可以通过显式调用取消或超时来取消。它将所有工作分派给Process()
,当给定各种操作时,它可以处理不同的函数。也可以让每个这些情况分派另一个函数,以获得更模块化的代码。
最终,响应被返回到响应通道,并且我们在最后循环打印所有结果。我们还演示了divide by 0
示例中的错误情况。
使用工作池设计模式
工作池设计模式是一种将长时间运行的 Goroutines 作为工作程序分派的模式。这些工作程序可以使用多个通道处理各种工作,也可以使用描述类型的有状态请求结构,如前面的示例所述。本示例将创建有状态的工作程序,并演示如何协调和启动多个工作程序,它们都在同一个通道上并发处理请求。这些工作程序将是crypto
工作程序,就像在 Web 身份验证应用程序中一样。它们的目的将是使用bcrypt
包对明文字符串进行哈希处理,并将文本密码与哈希进行比较。
如何做...
这些步骤涵盖了编写和运行应用程序的过程:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter10/pool
的新目录并进入该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter10/pool
你应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter10/pool
-
复制
~/projects/go-programming-cookbook-original/chapter10/pool
中的测试,或者利用这个机会编写一些你自己的代码! -
创建一个名为
worker.go
的文件,其中包含以下内容:
package pool
import (
"context"
"fmt"
)
// Dispatch creates numWorker workers, returns a cancel
// function channels for adding work and responses,
// cancel must be called
func Dispatch(numWorker int) (context.CancelFunc, chan
WorkRequest, chan WorkResponse) {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
in := make(chan WorkRequest, 10)
out := make(chan WorkResponse, 10)
for i := 0; i < numWorker; i++ {
go Worker(ctx, i, in, out)
}
return cancel, in, out
}
// Worker loops forever and is part of the worker pool
func Worker(ctx context.Context, id int, in chan WorkRequest,
out chan WorkResponse) {
for {
select {
case <-ctx.Done():
return
case wr := <-in:
fmt.Printf("worker id: %d, performing %s
workn", id, wr.Op)
out <- Process(wr)
}
}
}
- 创建一个名为
work.go
的文件,其中包含以下内容:
package pool
import "errors"
type op string
const (
// Hash is the bcrypt work type
Hash op = "encrypt"
// Compare is bcrypt compare work
Compare = "decrypt"
)
// WorkRequest is a worker req
type WorkRequest struct {
Op op
Text []byte
Compare []byte // optional
}
// WorkResponse is a worker resp
type WorkResponse struct {
Wr WorkRequest
Result []byte
Matched bool
Err error
}
// Process dispatches work to the worker pool channel
func Process(wr WorkRequest) WorkResponse {
switch wr.Op {
case Hash:
return hashWork(wr)
case Compare:
return compareWork(wr)
default:
return WorkResponse{Err: errors.New("unsupported
operation")}
}
}
- 创建一个名为
crypto.go
的文件,其中包含以下内容:
package pool
import "golang.org/x/crypto/bcrypt"
func hashWork(wr WorkRequest) WorkResponse {
val, err := bcrypt.GenerateFromPassword(wr.Text,
bcrypt.DefaultCost)
return WorkResponse{
Result: val,
Err: err,
Wr: wr,
}
}
func compareWork(wr WorkRequest) WorkResponse {
var matched bool
err := bcrypt.CompareHashAndPassword(wr.Compare, wr.Text)
if err == nil {
matched = true
}
return WorkResponse{
Matched: matched,
Err: err,
Wr: wr,
}
}
-
创建一个名为
example
的新目录,并进入该目录。 -
创建一个名为
main.go
的文件,其中包含以下内容:
package main
import (
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter10/pool"
)
func main() {
cancel, in, out := pool.Dispatch(10)
defer cancel()
for i := 0; i < 10; i++ {
in <- pool.WorkRequest{Op: pool.Hash, Text:
[]byte(fmt.Sprintf("messages %d", i))}
}
for i := 0; i < 10; i++ {
res := <-out
if res.Err != nil {
panic(res.Err)
}
in <- pool.WorkRequest{Op: pool.Compare, Text:
res.Wr.Text, Compare: res.Result}
}
for i := 0; i < 10; i++ {
res := <-out
if res.Err != nil {
panic(res.Err)
}
fmt.Printf("string: "%s"; matched: %vn",
string(res.Wr.Text), res.Matched)
}
}
-
运行
go run main.go
。 -
你也可以运行以下命令:
$ go build $ ./example
现在你应该看到以下输出:
$ go run main.go
worker id: 9, performing encrypt work
worker id: 5, performing encrypt work
worker id: 2, performing encrypt work
worker id: 8, performing encrypt work
worker id: 6, performing encrypt work
worker id: 1, performing encrypt work
worker id: 0, performing encrypt work
worker id: 4, performing encrypt work
worker id: 3, performing encrypt work
worker id: 7, performing encrypt work
worker id: 2, performing decrypt work
worker id: 6, performing decrypt work
worker id: 8, performing decrypt work
worker id: 1, performing decrypt work
worker id: 0, performing decrypt work
worker id: 9, performing decrypt work
worker id: 3, performing decrypt work
worker id: 4, performing decrypt work
worker id: 7, performing decrypt work
worker id: 5, performing decrypt work
string: "messages 9"; matched: true
string: "messages 3"; matched: true
string: "messages 4"; matched: true
string: "messages 0"; matched: true
string: "messages 1"; matched: true
string: "messages 8"; matched: true
string: "messages 5"; matched: true
string: "messages 7"; matched: true
string: "messages 2"; matched: true
string: "messages 6"; matched: true
-
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶层示例目录中。 -
如果你复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
这个示例使用Dispatch()
方法在单个输入通道、输出通道和连接到单个cancel()
函数上创建多个工作人员。如果你想为不同的目的创建不同的池,这个方法就可以使用。例如,你可以通过使用单独的池创建 10 个crypto
和 20 个compare
工作人员。对于这个示例,我们使用一个单一的池,将哈希请求发送给工作人员,检索响应,然后将compare
请求发送到同一个池中。因此,执行工作的工作人员每次都会不同,但它们都能执行任何类型的工作。
这种方法的优点是,这两种请求都允许并行处理,并且还可以控制最大并发数。限制 Goroutines 的最大数量对于限制内存也很重要。我选择了crypto
作为这个示例,因为crypto
是一个很好的例子,它可以通过为每个新请求启动一个新的 Goroutine 来压倒你的 CPU 或内存;例如,在一个 web 服务中。
使用工作人员创建管道
这个示例演示了创建工作池组并将它们连接在一起形成一个管道。对于这个示例,我们将两个池连接在一起,但这种模式可以用于更复杂的操作,类似于中间件。
工作池对于保持工作人员相对简单并进一步控制并发非常有用。例如,将日志串行化,同时并行化其他操作可能很有用。对于更昂贵的操作,拥有一个较小的池也可能很有用,这样你就不会过载机器资源。
操作步骤如下...
这些步骤涵盖了编写和运行你的应用程序:
-
从你的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter10/pipeline
的新目录,并进入该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter10/pipeline
你应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter10/pipeline
-
复制
~/projects/go-programming-cookbook-original/chapter10/pipeline
中的测试,或者利用这个机会编写一些你自己的代码! -
创建一个名为
worker.go
的文件,其中包含以下内容:
package pipeline
import "context"
// Worker have one role
// that is determined when
// Work is called
type Worker struct {
in chan string
out chan string
}
// Job is a job a worker can do
type Job string
const (
// Print echo's all input to
// stdout
Print Job = "print"
// Encode base64 encodes input
Encode Job = "encode"
)
// Work is how to dispatch a worker, they are assigned
// a job here
func (w *Worker) Work(ctx context.Context, j Job) {
switch j {
case Print:
w.Print(ctx)
case Encode:
w.Encode(ctx)
default:
return
}
}
- 创建一个名为
print.go
的文件,其中包含以下内容:
package pipeline
import (
"context"
"fmt"
)
// Print prints w.in and repalys it
// on w.out
func (w *Worker) Print(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case val := <-w.in:
fmt.Println(val)
w.out <- val
}
}
}
- 创建一个名为
encode.go
的文件,其中包含以下内容:
package pipeline
import (
"context"
"encoding/base64"
"fmt"
)
// Encode takes plain text as int
// and returns "string => <base64 string encoding>
// as out
func (w *Worker) Encode(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case val := <-w.in:
w.out <- fmt.Sprintf("%s => %s", val,
base64.StdEncoding.EncodeToString([]byte(val)))
}
}
}
- 创建一个名为
pipeline.go
的文件,其中包含以下内容:
package pipeline
import "context"
// NewPipeline initializes the workers and
// connects them, it returns the input of the pipeline
// and the final output
func NewPipeline(ctx context.Context, numEncoders, numPrinters
int) (chan string, chan string) {
inEncode := make(chan string, numEncoders)
inPrint := make(chan string, numPrinters)
outPrint := make(chan string, numPrinters)
for i := 0; i < numEncoders; i++ {
w := Worker{
in: inEncode,
out: inPrint,
}
go w.Work(ctx, Encode)
}
for i := 0; i < numPrinters; i++ {
w := Worker{
in: inPrint,
out: outPrint,
}
go w.Work(ctx, Print)
}
return inEncode, outPrint
}
-
创建一个名为
example
的新目录,并进入该目录。 -
创建一个名为
main.go
的文件,其中包含以下内容:
package main
import (
"context"
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter10/pipeline"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
in, out := pipeline.NewPipeline(ctx, 10, 2)
go func() {
for i := 0; i < 20; i++ {
in <- fmt.Sprint("Message", i)
}
}()
for i := 0; i < 20; i++ {
<-out
}
}
-
运行
go run main.go
。 -
你也可以运行以下命令:
$ go build $ ./example
现在你应该看到以下输出:
$ go run main.go
Message3 => TWVzc2FnZTM=
Message7 => TWVzc2FnZTc=
Message8 => TWVzc2FnZTg=
Message9 => TWVzc2FnZTk=
Message5 => TWVzc2FnZTU=
Message11 => TWVzc2FnZTEx
Message10 => TWVzc2FnZTEw
Message4 => TWVzc2FnZTQ=
Message12 => TWVzc2FnZTEy
Message6 => TWVzc2FnZTY=
Message14 => TWVzc2FnZTE0
Message13 => TWVzc2FnZTEz
Message0 => TWVzc2FnZTA=
Message15 => TWVzc2FnZTE1
Message1 => TWVzc2FnZTE=
Message17 => TWVzc2FnZTE3
Message16 => TWVzc2FnZTE2
Message19 => TWVzc2FnZTE5
Message18 => TWVzc2FnZTE4
Message2 => TWVzc2FnZTI=
-
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶层示例目录中。 -
如果你复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
main
包创建了一个包含 10 个编码器和 2 个打印机的管道。它在输入通道上排队了 20 个字符串,并等待在输出通道上获得 20 个响应。如果消息到达输出通道,表示它们已经成功通过整个管道。
NewPipeline
函数用于连接管道。它确保通道以适当的缓冲区大小创建,并且一些池的输出通道连接到其他池的适当输入通道。还可以通过在每个工作器上使用输入通道数组和输出通道数组,多个命名通道,或通道映射来扩展管道。这将允许诸如在每个步骤发送消息到记录器之类的操作。
第十一章:分布式系统
有时,应用级并行性是不够的,开发中看起来简单的事情在部署过程中可能变得复杂。分布式系统在开发单台机器时找不到的一些挑战。这些应用程序增加了一些复杂性,比如监控、编写需要强一致性保证的应用程序和服务发现。此外,你必须时刻注意单点故障,比如数据库,否则你的分布式应用在这个单一组件失败时也会失败。
本章将探讨管理分布式数据、编排、容器化、指标和监控的方法。这些将成为你编写和维护微服务和大型分布式应用程序的工具箱的一部分。
在本章中,我们将涵盖以下配方:
-
使用 Consul 进行服务发现
-
使用 Raft 实现基本共识
-
使用 Docker 进行容器化
-
编排和部署策略
-
监控应用程序
-
收集指标
技术要求
要遵循本章中的所有配方,根据以下步骤配置你的环境:
-
从
golang.org/doc/install
在你的操作系统上下载并安装 Go 1.12.6 或更高版本。 -
打开一个终端或控制台应用程序,并创建并进入一个项目目录,比如
~/projects/go-programming-cookbook
。所有的代码都将在这个目录中运行和修改。 -
将最新的代码克隆到
~/projects/go-programming-cookbook-original
,(可选)从该目录中工作,而不是手动输入示例。
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
使用 Consul 进行服务发现
当使用微服务方法来开发应用程序时,你最终会得到很多服务器监听各种 IP、域和端口。这些 IP 地址会因环境(测试与生产)而异,并且在服务之间保持静态以进行配置可能会很棘手。你还想知道何时一台机器或服务因网络分区而宕机或不可达。网络分区发生在网络的两个部分无法相互到达时。例如,如果两个数据中心之间的交换机失败,那么一个数据中心内的服务就无法到达另一个数据中心内的服务。Consul 是一个提供了很多功能的工具,但在这里,我们将探索如何使用 Consul 注册服务并从其他服务中查询它们。
如何做...
这些步骤涵盖了编写和运行你的应用程序:
-
从你的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter11/discovery
的新目录并进入。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/discovery
你应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/discovery
-
从
~/projects/go-programming-cookbook-original/chapter11/discovery
复制测试,或者利用这个机会编写一些你自己的代码! -
创建一个名为
client.go
的文件,内容如下:
package discovery
import "github.com/hashicorp/consul/api"
// Client exposes api methods we care
// about
type Client interface {
Register(tags []string) error
Service(service, tag string) ([]*api.ServiceEntry,
*api.QueryMeta, error)
}
type client struct {
client *api.Client
address string
name string
port int
}
//NewClient iniitalizes a consul client
func NewClient(config *api.Config, address, name string, port
int) (Client, error) {
c, err := api.NewClient(config)
if err != nil {
return nil, err
}
cli := &client{
client: c,
name: name,
address: address,
port: port,
}
return cli, nil
}
- 创建一个名为
operations.go
的文件,内容如下:
package discovery
import "github.com/hashicorp/consul/api"
// Register adds our service to consul
func (c *client) Register(tags []string) error {
reg := &api.AgentServiceRegistration{
ID: c.name,
Name: c.name,
Port: c.port,
Address: c.address,
Tags: tags,
}
return c.client.Agent().ServiceRegister(reg)
}
// Service return a service
func (c *client) Service(service, tag string)
([]*api.ServiceEntry, *api.QueryMeta, error) {
return c.client.Health().Service(service, tag, false,
nil)
}
- 创建一个名为
exec.go
的文件,内容如下:
package discovery
import "fmt"
// Exec creates a consul entry then queries it
func Exec(cli Client) error {
if err := cli.Register([]string{"Go", "Awesome"}); err != nil {
return err
}
entries, _, err := cli.Service("discovery", "Go")
if err != nil {
return err
}
for _, entry := range entries {
fmt.Printf("%#v\n", entry.Service)
}
return nil
}
-
创建一个名为
example
的新目录并进入。 -
创建一个名为
main.go
的文件,内容如下:
package main
import "github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter11/discovery"
func main() {
if err := discovery.Exec(); err != nil {
panic(err)
}
}
-
使用
consul agent -dev -node=localhost
命令在一个单独的终端中启动 Consul。 -
运行
go run main.go
命令。 -
你也可以运行以下命令:
$ go build $ ./example
你应该看到以下输出:
$ go run main.go
&api.AgentService{ID:"discovery", Service:"discovery", Tags:
[]string{"Go", "Awesome"}, Port:8080, Address:"localhost",
EnableTagOverride:false, CreateIndex:0x23, ModifyIndex:0x23}
-
go.mod
文件可能会被更新,顶级配方目录中现在应该存在go.sum
文件。 -
如果你复制或编写了自己的测试,返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
Consul 提供了一个强大的 Go API 库。当您第一次开始时,可能会感到令人生畏,但这个配方展示了您可能如何封装它。进一步配置 Consul 超出了此配方的范围;这显示了注册服务和在给定密钥和标签时查询其他服务的基础知识。
可以使用此功能在启动时注册新的微服务,查询所有依赖服务,并在关闭时注销。您可能还希望缓存此信息,以便不必为每个请求访问 Consul,但此配方提供了您可以扩展的基本工具。Consul 代理还使这些重复请求变得快速和高效(www.consul.io/intro/getting-started/agent.html
)。一旦您
使用 Raft 实现基本共识
Raft 是一种共识算法。它允许分布式系统保持共享和受控状态(raft.github.io/
)。建立 Raft 系统在许多方面都很复杂-首先,您需要共识才能进行选举并成功。当您使用多个节点时,这可能很难引导,并且可能很难开始。在单个节点/领导者上可以运行基本集群。但是,如果您需要冗余性,至少需要三个节点,以防止单个节点故障导致数据丢失。这个概念被称为法定人数,您必须维护(n/2)+1 个可用节点,以确保可以将新日志提交到 Raft 集群。基本上,如果您可以维持法定人数,集群将保持健康和可用。
此配方实现了一个基本的内存 Raft 集群,构建了一个可以在某些允许的状态之间转换的状态机,并将分布式状态机连接到可以触发转换的 Web 处理程序。在实现 Raft 所需的基本有限状态机接口或进行测试时,这可能非常有用。此配方使用github.com/hashicorp/raft
作为基本 Raft 实现。
如何做...
以下步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter11/consensus
的新目录并转到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/consensus
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/consensus
-
复制
~/projects/go-programming-cookbook-original/chapter11/consensus
中的测试,或者利用这个机会编写一些自己的代码! -
创建一个名为
state.go
的文件,其中包含以下内容:
package consensus
type state string
const (
first state = "first"
second = "second"
third = "third"
)
var allowedState map[state][]state
func init() {
// setup valid states
allowedState = make(map[state][]state)
allowedState[first] = []state{second, third}
allowedState[second] = []state{third}
allowedState[third] = []state{first}
}
// CanTransition checks if a new state is valid
func (s *state) CanTransition(next state) bool {
for _, n := range allowedState[*s] {
if n == next {
return true
}
}
return false
}
// Transition will move a state to the next
// state if able
func (s *state) Transition(next state) {
if s.CanTransition(next) {
*s = next
}
}
- 创建一个名为
raftset.go
的文件,其中包含以下内容:
package consensus
import (
"fmt"
"github.com/hashicorp/raft"
)
// keep a map of rafts for later
var rafts map[raft.ServerAddress]*raft.Raft
func init() {
rafts = make(map[raft.ServerAddress]*raft.Raft)
}
// raftSet stores all the setup material we need
type raftSet struct {
Config *raft.Config
Store *raft.InmemStore
SnapShotStore raft.SnapshotStore
FSM *FSM
Transport raft.LoopbackTransport
Configuration raft.Configuration
}
// generate n raft sets to bootstrap the raft cluster
func getRaftSet(num int) []*raftSet {
rs := make([]*raftSet, num)
servers := make([]raft.Server, num)
for i := 0; i < num; i++ {
addr := raft.ServerAddress(fmt.Sprint(i))
_, transport := raft.NewInmemTransport(addr)
servers[i] = raft.Server{
Suffrage: raft.Voter,
ID: raft.ServerID(addr),
Address: addr,
}
config := raft.DefaultConfig()
config.LocalID = raft.ServerID(addr)
rs[i] = &raftSet{
Config: config,
Store: raft.NewInmemStore(),
SnapShotStore: raft.NewInmemSnapshotStore(),
FSM: NewFSM(),
Transport: transport,
}
}
// configuration needs to be consistent between
// services and so we need the full serverlist in this
// case
for _, r := range rs {
r.Configuration = raft.Configuration{Servers: servers}
}
return rs
}
- 创建一个名为
config.go
的文件,其中包含以下内容:
package consensus
import (
"github.com/hashicorp/raft"
)
// Config creates num in-memory raft
// nodes and connects them
func Config(num int) {
// create n "raft-sets" consisting of
// everything needed to represent a node
rs := getRaftSet(num)
//connect all of the transports
for _, r1 := range rs {
for _, r2 := range rs {
r1.Transport.Connect(r2.Transport.LocalAddr(), r2.Transport)
}
}
// for each node, bootstrap then connect
for _, r := range rs {
if err := raft.BootstrapCluster(r.Config, r.Store, r.Store, r.SnapShotStore, r.Transport, r.Configuration); err != nil {
panic(err)
}
raft, err := raft.NewRaft(r.Config, r.FSM, r.Store, r.Store, r.SnapShotStore, r.Transport)
if err != nil {
panic(err)
}
rafts[r.Transport.LocalAddr()] = raft
}
}
- 创建一个名为
fsm.go
的文件,其中包含以下内容:
package consensus
import (
"io"
"github.com/hashicorp/raft"
)
// FSM implements the raft FSM interface
// and holds a state
type FSM struct {
state state
}
// NewFSM creates a new FSM with
// start state of "first"
func NewFSM() *FSM {
return &FSM{state: first}
}
// Apply updates our FSM
func (f *FSM) Apply(r *raft.Log) interface{} {
f.state.Transition(state(r.Data))
return string(f.state)
}
// Snapshot needed to satisfy the raft FSM interface
func (f *FSM) Snapshot() (raft.FSMSnapshot, error) {
return nil, nil
}
// Restore needed to satisfy the raft FSM interface
func (f *FSM) Restore(io.ReadCloser) error {
return nil
}
- 创建一个名为
handler.go
的文件,其中包含以下内容:
package consensus
import (
"net/http"
"time"
)
// Handler grabs the get param ?next= and tries
// to transition to the state contained there
func Handler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
state := r.FormValue("next")
for address, raft := range rafts {
if address != raft.Leader() {
continue
}
result := raft.Apply([]byte(state), 1*time.Second)
if result.Error() != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
newState, ok := result.Response().(string)
if !ok {
w.WriteHeader(http.StatusInternalServerError)
return
}
if newState != state {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("invalid transition"))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(newState))
return
}
}
-
创建一个名为
example
的新目录并转到该目录。 -
创建一个名为
main.go
的文件,其中包含以下内容:
package main
import (
"net/http"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter11/consensus"
)
func main() {
consensus.Config(3)
http.HandleFunc("/", consensus.Handler)
err := http.ListenAndServe(":3333", nil)
panic(err)
}
- 运行
go run main.go
命令。或者,您也可以运行以下命令:
$ go build
$ ./example
现在您应该看到以下输出:
$ go run main.go
2019/05/04 21:06:46 [INFO] raft: Initial configuration (index=1): [{Suffrage:Voter ID:0 Address:0} {Suffrage:Voter ID:1 Address:1} {Suffrage:Voter ID:2 Address:2}]
2019/05/04 21:06:46 [INFO] raft: Initial configuration (index=1): [{Suffrage:Voter ID:0 Address:0} {Suffrage:Voter ID:1 Address:1} {Suffrage:Voter ID:2 Address:2}]
2019/05/04 21:06:46 [INFO] raft: Node at 0 [Follower] entering Follower state (Leader: "")
2019/05/04 21:06:46 [INFO] raft: Node at 1 [Follower] entering Follower state (Leader: "")
2019/05/04 21:06:46 [INFO] raft: Initial configuration (index=1): [{Suffrage:Voter ID:0 Address:0} {Suffrage:Voter ID:1 Address:1} {Suffrage:Voter ID:2 Address:2}]
2019/05/04 21:06:46 [INFO] raft: Node at 2 [Follower] entering Follower state (Leader: "")
2019/05/04 21:06:47 [WARN] raft: Heartbeat timeout from "" reached, starting election
2019/05/04 21:06:47 [INFO] raft: Node at 0 [Candidate] entering Candidate state in term 2
2019/05/04 21:06:47 [DEBUG] raft: Votes needed: 2
2019/05/04 21:06:47 [DEBUG] raft: Vote granted from 0 in term 2\. Tally: 1
2019/05/04 21:06:47 [DEBUG] raft: Vote granted from 1 in term 2\. Tally: 2
2019/05/04 21:06:47 [INFO] raft: Election won. Tally: 2
2019/05/04 21:06:47 [INFO] raft: Node at 0 [Leader] entering Leader state
2019/05/04 21:06:47 [INFO] raft: Added peer 1, starting replication
2019/05/04 21:06:47 [INFO] raft: Added peer 2, starting replication
2019/05/04 21:06:47 [INFO] raft: pipelining replication to peer {Voter 1 1}
2019/05/04 21:06:47 [INFO] raft: pipelining replication to peer {Voter 2 2}
- 在另一个终端中,运行以下命令:
$ curl "http://localhost:3333/?next=second"
second
$ curl "http://localhost:3333/?next=third"
third
$ curl "http://localhost:3333/?next=second"
invalid transition
$ curl "http://localhost:3333/?next=first"
first
-
go.mod
文件可能会更新,go.sum
文件现在应该存在于顶级配方目录中。 -
如果您复制或编写了自己的测试,请返回到上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
当应用程序启动时,我们初始化多个 Raft 对象。每个对象都有自己的地址和传输方式。InmemTransport{}
函数还提供了一个连接其他传输方式的方法,称为Connect()
。一旦建立了这些连接,Raft 集群就会进行选举。在 Raft 集群中通信时,客户端必须与领导者通信。在我们的情况下,一个处理程序可以与所有节点通信,因此处理程序负责拥有Raft
领导者的call Apply()
对象。这反过来又在所有其他节点上运行apply()
。
InmemTransport{}
函数通过允许所有内容驻留在内存中来简化选举和引导过程。在实践中,除了测试和概念验证之外,这并不是很有帮助,因为 Goroutines 可以自由访问共享内存。一个更适合生产的实现会使用类似 HTTP 传输的东西,这样服务实例可以跨机器通信。这可能需要一些额外的簿记或服务发现,因为服务实例必须监听和提供服务,同时还必须能够发现并建立彼此的连接。
使用 Docker 进行容器化
Docker 是一种用于打包和运输应用程序的容器技术。其他优势包括可移植性,因为容器无论在哪个主机操作系统上都会以相同的方式运行。它提供了虚拟机的许多优势,但是以更轻量的容器形式。可以限制单个容器的资源消耗并隔离您的环境。在本地为应用程序和在生产环境中部署代码时,拥有一个共同的环境非常有用。Docker 是用 Go 语言编写的,是开源的,因此很容易利用客户端和库。这个配方将为一个基本的 Go 应用程序设置一个 Docker 容器,存储一些关于容器的版本信息,并演示如何从 Docker 端点访问处理程序。
准备工作
根据以下步骤配置您的环境:
-
参考本章的技术要求部分,配置环境的步骤。
-
从
docs.docker.com/install
安装 Docker。这也将包括 Docker Compose。
如何做...
这些步骤涵盖了编写和运行应用程序的过程:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter11/docker
的新目录,并进入该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/docker
你应该会看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/docker
-
从
~/projects/go-programming-cookbook-original/chapter11/docker
复制测试,或者利用这个机会编写一些自己的代码! -
创建一个名为
dockerfile
的文件,内容如下:
FROM alpine
ADD ./example/example /example
EXPOSE 8000
ENTRYPOINT /example
- 创建一个名为
setup.sh
的文件,内容如下:
#!/usr/bin/env bash
pushd example
env GOOS=linux go build -ldflags "-X main.version=1.0 -X
main.builddate=$(date +%s)"
popd
docker build . -t example
docker run -d -p 8000:8000 example
- 创建一个名为
version.go
的文件,内容如下:
package docker
import (
"encoding/json"
"net/http"
"time"
)
// VersionInfo holds artifacts passed in
// at build time
type VersionInfo struct {
Version string
BuildDate time.Time
Uptime time.Duration
}
// VersionHandler writes the latest version info
func VersionHandler(v *VersionInfo) http.HandlerFunc {
t := time.Now()
return func(w http.ResponseWriter, r *http.Request) {
v.Uptime = time.Since(t)
vers, err := json.Marshal(v)
if err != nil {
w.WriteHeader
(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write(vers)
}
}
-
创建一个名为
example
的新目录并进入。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter11/docker"
)
// these are set at build time
var (
version string
builddate string
)
var versioninfo docker.VersionInfo
func init() {
// parse buildtime variables
versioninfo.Version = version
i, err := strconv.ParseInt(builddate, 10, 64)
if err != nil {
panic(err)
}
tm := time.Unix(i, 0)
versioninfo.BuildDate = tm
}
func main() {
http.HandleFunc("/version",
docker.VersionHandler(&versioninfo))
fmt.Printf("version %s listening on :8000\n",
versioninfo.Version)
panic(http.ListenAndServe(":8000", nil))
}
-
导航回起始目录。
-
运行以下命令:
$ bash setup.sh
现在你应该会看到以下输出:
$ bash setup.sh
~/go/src/github.com/PacktPublishing/Go-Programming-Cookbook-
Second-Edition/chapter11/docker/example
~/go/src/github.com/PacktPublishing/Go-Programming-Cookbook-
Second-Edition/chapter11/docker
~/go/src/github.com/PacktPublishing/Go-Programming-Cookbook-
Second-Edition/chapter11/docker
Sending build context to Docker daemon 6.031 MB
Step 1/4 : FROM alpine
---> 4a415e366388
Step 2/4 : ADD ./example/example /example
---> de34c3c5451e
Removing intermediate container bdcd9c4f4381
Step 3/4 : EXPOSE 8000
---> Running in 188f450d4e7b
---> 35d1a2652b43
Removing intermediate container 188f450d4e7b
Step 4/4 : ENTRYPOINT /example
---> Running in cf0af4f48c3a
---> 3d737fc4e6e2
Removing intermediate container cf0af4f48c3a
Successfully built 3d737fc4e6e2
b390ef429fbd6e7ff87058dc82e15c3e7a8b2e
69a601892700d1d434e9e8e43b
- 运行以下命令:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b390ef429fbd example "/bin/sh -c /example" 22 seconds ago Up 23
seconds 0.0.0.0:8000->8000/tcp optimistic_wescoff
$ curl localhost:8000/version
{"Version":"1.0","BuildDate":"2017-04-
30T21:55:56Z","Uptime":48132111264}
$docker kill optimistic_wescoff # grab from first output
optimistic_wescoff
-
go.mod
文件可能会更新,go.sum
文件现在应该存在于顶层配方目录中。 -
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
这个示例创建了一个脚本,用于为 Linux 架构编译 Go 二进制文件,并在main.go
中设置各种私有变量。这些变量用于在版本端点上返回版本信息。一旦编译了二进制文件,就会创建一个包含二进制文件的 Docker 容器。这允许我们使用非常小的容器映像,因为 Go 运行时在二进制文件中是自包含的。然后我们运行容器,同时暴露容器监听 HTTP 流量的端口。最后,我们在本地主机上curl
端口,并看到我们的版本信息返回。
编排和部署策略
Docker 使编排和部署变得更加简单。在这个示例中,我们将建立与 MongoDB 的连接,然后从 Docker 容器中插入文档并查询它。这个示例将设置与第六章中使用 NoSQL 与 MongoDB 和 mgo配方相同的环境,关于数据库和存储的一切,但将应用程序和环境运行在容器内,并使用 Docker Compose 进行编排和连接。
这可以与 Docker Swarm 一起使用,Docker Swarm 是一个集成的 Docker 工具,允许您管理集群,创建和部署可以轻松扩展或缩减的节点,并管理负载平衡(docs.docker.com/engine/swarm/
)。另一个很好的容器编排示例是 Kubernetes(kubernetes.io/
),这是一个由 Google 使用 Go 编程语言编写的容器编排框架。
操作步骤...
以下步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter11/orchestrate
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/orchestrate
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/orchestrate
-
从
~/projects/go-programming-cookbook-original/chapter11/orchestrate
复制测试,或者利用这个机会编写一些自己的代码! -
创建一个名为
Dockerfile
的文件,内容如下:
FROM golang:1.12.4-alpine3.9
ENV GOPATH /code/
ADD . /code/src/github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/docker
WORKDIR /code/src/github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/docker/example
RUN GO111MODULE=on GOPROXY=off go build -mod=vendor
ENTRYPOINT /code/src/github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/docker/example/example
- 创建一个名为
docker-compose.yml
的文件,内容如下:
version: '2'
services:
app:
build: .
mongodb:
image: "mongo:latest"
- 创建一个名为
config.go
的文件,内容如下:
package mongodb
import (
"context"
"fmt"
"time"
"github.com/mongodb/mongo-go-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// Setup initializes a mongo client
func Setup(ctx context.Context, address string) (*mongo.Client, error) {
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()
fmt.Println(address)
client, err := mongo.NewClient(options.Client().ApplyURI(address))
if err != nil {
return nil, err
}
if err := client.Connect(ctx); err != nil {
return nil, err
}
return client, nil
}
- 创建一个名为
exec.go
的文件,内容如下:
package mongodb
import (
"context"
"fmt"
"github.com/mongodb/mongo-go-driver/bson"
)
// State is our data model
type State struct {
Name string `bson:"name"`
Population int `bson:"pop"`
}
// Exec creates then queries an Example
func Exec(address string) error {
ctx := context.Background()
db, err := Setup(ctx, address)
if err != nil {
return err
}
conn := db.Database("gocookbook").Collection("example")
vals := []interface{}{&State{"Washington", 7062000}, &State{"Oregon", 3970000}}
// we can inserts many rows at once
if _, err := conn.InsertMany(ctx, vals); err != nil {
return err
}
var s State
if err := conn.FindOne(ctx, bson.M{"name": "Washington"}).Decode(&s); err != nil {
return err
}
if err := conn.Drop(ctx); err != nil {
return err
}
fmt.Printf("State: %#v\n", s)
return nil
}
-
创建一个名为
example
的新目录,并导航到该目录。 -
创建一个
main.go
文件,内容如下:
package main
import mongodb "github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/orchestrate"
func main() {
if err := mongodb.Exec("mongodb://mongodb:27017"); err != nil {
panic(err)
}
}
-
返回到起始目录。
-
运行
go mod vendor
命令。 -
运行
docker-compose up -d
命令。 -
运行
docker logs orchestrate_app_1
命令。现在应该看到以下输出:
$ docker logs orchestrate_app_1
State: docker.State{Name:"Washington", Population:7062000}
-
go.mod
文件可能会被更新,顶级配方目录中现在应该存在go.sum
文件。 -
如果您复制或编写了自己的测试,请返回到上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
这个配置适用于本地开发。一旦运行docker-compose up
命令,本地目录将被重建,Docker 将使用最新版本与 MongoDB 实例建立连接,并开始对其进行操作。此示例使用 go mod vendor 进行依赖管理。因此,我们禁用go mod cache
并告诉go build
命令使用我们创建的 vendor 目录。
这可以为需要连接到外部服务的应用程序提供一个良好的基线;第六章中的所有配方,关于数据库和存储的一切,都可以使用这种方法,而不是创建数据库的本地实例。对于生产环境,您可能不希望在 Docker 容器后面运行数据存储,但通常也会有静态主机名用于配置。
监控应用程序
有多种方法可以监视 Go 应用程序。其中最简单的方法之一是设置 Prometheus,这是一个用 Go 编写的监视应用程序(prometheus.io
)。这是一个根据您的配置文件轮询端点并收集有关您的应用程序的大量信息的应用程序,包括 Goroutines 的数量、内存使用情况等等。这个应用程序将使用上一个教程中的技术来设置一个 Docker 环境来托管 Prometheus 并连接到它。
操作步骤...
以下步骤涵盖了编写和运行您的应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter11/monitoring
的新目录,并转到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/monitoring
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/monitoring
-
从
~/projects/go-programming-cookbook-original/chapter11/monitoring
复制测试,或者利用这个机会编写一些自己的代码! -
创建一个名为
Dockerfile
的文件,内容如下:
FROM golang:1.12.4-alpine3.9
ENV GOPATH /code/
ADD . /code/src/github.com/agtorre/go-
cookbook/chapter11/monitoring
WORKDIR /code/src/github.com/agtorre/go-
cookbook/chapter11/monitoring
RUN GO111MODULE=on GOPROXY=off go build -mod=vendor
ENTRYPOINT /code/src/github.com/agtorre/go-
cookbook/chapter11/monitoring/monitoring
- 创建一个名为
docker-compose.yml
的文件,内容如下:
version: '2'
services:
app:
build: .
prometheus:
ports:
- 9090:9090
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
image: "prom/prometheus"
- 创建一个名为
main.go
的文件,内容如下:
package main
import (
"net/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
http.Handle("/metrics", promhttp.Handler())
panic(http.ListenAndServe(":80", nil))
}
- 创建一个名为
prometheus.yml
的文件,内容如下:
global:
scrape_interval: 15s # By default, scrape targets every 15
seconds.
# A scrape configuration containing exactly one endpoint to
scrape:
# Here it's Prometheus itself.
scrape_configs:
# The job name is added as a label `job=<job_name>` to any
timeseries scraped from this config.
- job_name: 'app'
# Override the global default and scrape targets from this job
every 5 seconds.
scrape_interval: 5s
static_configs:
- targets: ['app:80']
-
运行
go mod vendor
命令。 -
运行
docker-compose up
命令。现在您应该看到以下输出:
$ docker-compose up
Starting monitoring_prometheus_1 ... done
Starting monitoring_app_1 ... done
Attaching to monitoring_app_1, monitoring_prometheus_1
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Starting prometheus (version=1.6.1, branch=master, revision=4666df502c0e239ed4aa1d80abbbfb54f61b23c3)" source="main.go:88"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Build context (go=go1.8.1, user=root@7e45fa0366a7, date=20170419-14:32:22)" source="main.go:89"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Loading configuration file /etc/prometheus/prometheus.yml" source="main.go:251"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Loading series map and head chunks..." source="storage.go:421"
prometheus_1 | time="2019-05-05T03:10:25Z" level=warning msg="Persistence layer appears dirty." source="persistence.go:846"
prometheus_1 | time="2019-05-05T03:10:25Z" level=warning msg="Starting crash recovery. Prometheus is inoperational until complete." source="crashrecovery.go:40"
prometheus_1 | time="2019-05-05T03:10:25Z" level=warning msg="To avoid crash recovery in the future, shut down Prometheus with SIGTERM or a HTTP POST to /-/quit." source="crashrecovery.go:41"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Scanning files." source="crashrecovery.go:55"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="File scan complete. 43 series found." source="crashrecovery.go:83"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Checking for series without series file." source="crashrecovery.go:85"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Check for series without series file complete." source="crashrecovery.go:131"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Cleaning up archive indexes." source="crashrecovery.go:411"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Clean-up of archive indexes complete." source="crashrecovery.go:504"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Rebuilding label indexes." source="crashrecovery.go:512"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Indexing metrics in memory." source="crashrecovery.go:513"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Indexing archived metrics." source="crashrecovery.go:521"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="All requests for rebuilding the label indexes queued. (Actual processing may lag behind.)" source="crashrecovery.go:540"
prometheus_1 | time="2019-05-05T03:10:25Z" level=warning msg="Crash recovery complete." source="crashrecovery.go:153"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="43 series loaded." source="storage.go:432"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Starting target manager..." source="targetmanager.go:61"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Listening on :9090" source="web.go:259"
-
go.mod
文件可能已更新,并且顶级配方目录中现在应该存在go.sum
文件。 -
在浏览器中导航到
http://localhost:9090/
。您应该看到与您的应用程序相关的各种指标!
工作原理...
这个教程在 Go 中创建了一个简单的处理程序,使用 prometheus go 客户端将有关正在运行的应用程序的统计信息导出到 prometheus。我们将我们的应用程序连接到在 docker 中运行的 prometheus 服务器,并使用 docker-compose 处理网络连接和启动。收集数据的频率、应用程序提供服务的端口以及应用程序的名称都在prometheus.yml
文件中指定。一旦两个容器都启动,prometheus 服务器就开始在指定的端口上收集和监控应用程序。它还公开了一个 Web 界面,我们可以在浏览器中访问以查看有关我们的应用程序的更多信息。
Prometheus 客户端处理程序将向 Prometheus 服务器返回有关您的应用程序的各种统计信息。这使您可以将多个 Prometheus 服务器指向一个应用程序,而无需重新配置或部署该应用程序。其中大多数统计信息是通用的,并且对于诸如检测内存泄漏之类的事情非常有益。许多其他解决方案要求您定期向服务器发送信息。下一个教程,收集指标,将演示如何将自定义指标发送到 Prometheus 服务器。
收集指标
除了关于您的应用程序的一般信息之外,发出特定于应用程序的指标也可能有所帮助。例如,我们可能希望收集定时数据或跟踪事件发生的次数。
这个教程将使用github.com/rcrowley/go-metrics
包来收集指标并通过一个端点公开它们。有各种导出工具可以用来将指标导出到诸如 Prometheus 和 InfluxDB 之类的地方,这些工具也是用 Go 编写的。
准备工作
根据以下步骤配置您的环境:
-
请参阅本章的技术要求部分,了解配置环境的步骤。
-
运行
go get github.com/rcrowley/go-metrics
命令。
操作步骤...
这些步骤涵盖了编写和运行您的应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter11/metrics
的新目录,并转到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/metrics
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/metrics
-
从
~/projects/go-programming-cookbook-original/chapter11/metrics
复制测试,或者利用这个机会编写一些自己的代码! -
创建一个名为
handler.go
的文件,内容如下:
package metrics
import (
"net/http"
"time"
metrics "github.com/rcrowley/go-metrics"
)
// CounterHandler will update a counter each time it's called
func CounterHandler(w http.ResponseWriter, r *http.Request) {
c := metrics.GetOrRegisterCounter("counterhandler.counter",
nil)
c.Inc(1)
w.WriteHeader(http.StatusOK)
w.Write([]byte("success"))
}
// TimerHandler records the duration required to compelete
func TimerHandler(w http.ResponseWriter, r *http.Request) {
currt := time.Now()
t := metrics.GetOrRegisterTimer("timerhandler.timer", nil)
w.WriteHeader(http.StatusOK)
w.Write([]byte("success"))
t.UpdateSince(currt)
}
- 创建一个名为
report.go
的文件,内容如下:
package metrics
import (
"net/http"
gometrics "github.com/rcrowley/go-metrics"
)
// ReportHandler will emit the current metrics in json format
func ReportHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
t := gometrics.GetOrRegisterTimer(
"reporthandler.writemetrics", nil)
t.Time(func() {
gometrics.WriteJSONOnce(gometrics.DefaultRegistry, w)
})
}
-
创建一个名为
example
的新目录并进入。 -
创建一个名为
main.go
的文件:
package main
import (
"net/http"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter11/metrics"
)
func main() {
// handler to populate metrics
http.HandleFunc("/counter", metrics.CounterHandler)
http.HandleFunc("/timer", metrics.TimerHandler)
http.HandleFunc("/report", metrics.ReportHandler)
fmt.Println("listening on :8080")
panic(http.ListenAndServe(":8080", nil))
}
- 运行
go run main.go
。或者,您也可以运行以下命令:
$ go build $ ./example
现在您应该看到以下输出:
$ go run main.go
listening on :8080
- 从单独的 shell 中运行以下命令:
$ curl localhost:8080/counter
success
$ curl localhost:8080/timer
success
$ curl localhost:8080/report
{"counterhandler.counter":{"count":1},
"reporthandler.writemetrics": {"15m.rate":0,"1m.rate":0,"5m.rate":0,"75%":0,"95%":0,"99%":0,"99.9%":0,"count":0,"max":0,"mean":0,"mean.rate":0,"median":0,"min":0,"stddev":0},"timerhandler.timer":{"15m.rate":0.0011080303990206543,"1m.rate":0.015991117074135343,"5m.rate":0.0033057092356765017,"75%":60485,"95%":60485,"99%":60485,"99.9%":60485,"count":1,"max":60485,"mean":60485,"mean.rate":1.1334543719787356,"median":60485,"min":60485,"stddev":0}}
-
尝试多次访问所有端点,看看它们如何变化。
-
go.mod
文件可能会被更新,而go.sum
文件现在应该存在于顶层的配方目录中。 -
如果您复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
gometrics
将所有度量标准保存在注册表中。一旦设置好,您可以使用任何度量发射选项,比如counter
或timer
,它将把这个更新存储在注册表中。有多个导出器将度量标准导出到第三方工具。在我们的情况下,我们设置了一个以 JSON 格式发射所有度量标准的处理程序。
我们设置了三个处理程序——一个用于增加计数器,一个用于记录退出处理程序的时间,以及一个用于打印报告(同时还增加了一个额外的计数器)。GetOrRegister
函数对于以线程安全的方式原子地获取或创建度量发射器非常有用。或者,您可以提前注册所有内容。
第十二章:响应式编程和数据流
在本章中,我们将讨论 Go 中的响应式编程设计模式。响应式编程是一种专注于数据流和变化传播的编程概念。诸如 Kafka 之类的技术允许您快速生成或消费数据流。因此,这些技术彼此之间是自然契合的。在将 Kafka 连接到 Goflow配方中,我们将探讨将kafka
消息队列与goflow
结合起来,以展示使用这些技术的实际示例。本章还将探讨连接到 Kafka 并使用它来处理消息的各种方法。最后,本章将演示如何在 Go 中创建一个基本的graphql
服务器。
在本章中,我们将涵盖以下配方:
-
使用 Goflow 进行数据流编程
-
使用 Sarama 与 Kafka
-
使用异步生产者与 Kafka
-
将 Kafka 连接到 Goflow
-
使用 Go 编写 GraphQL 服务器
技术要求
为了继续本章中的所有配方,根据以下步骤配置您的环境:
-
在您的操作系统上下载并安装 Go 1.12.6 或更高版本,网址为
golang.org/doc/install.
-
打开一个终端或控制台应用程序,并创建并导航到一个名为
~/projects/go-programming-cookbook
的项目目录。所有代码都将从这个目录运行和修改。 -
将最新的代码克隆到
~/projects/go-programming-cookbook-original
,或者选择从该目录工作,而不是手动输入示例:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
使用 Goflow 进行数据流编程
github.com/trustmaster/goflow
包对于创建基于数据流的应用程序非常有用。它试图抽象概念,以便您可以编写组件并使用自定义网络将它们连接在一起。这个配方将重新创建第九章中讨论的应用程序,测试 Go 代码,但将使用goflow
包来实现。
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter12/goflow
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter12/goflow
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter12/goflow
-
从
~/projects/go-programming-cookbook-original/chapter12/goflow
复制测试,或者使用这个作为练习来编写一些您自己的代码! -
创建一个名为
components.go
的文件,内容如下:
package goflow
import (
"encoding/base64"
"fmt"
)
// Encoder base64 encodes all input
type Encoder struct {
Val <-chan string
Res chan<- string
}
// Process does the encoding then pushes the result onto Res
func (e *Encoder) Process() {
for val := range e.Val {
encoded := base64.StdEncoding.EncodeToString([]byte(val))
e.Res <- fmt.Sprintf("%s => %s", val, encoded)
}
}
// Printer is a component for printing to stdout
type Printer struct {
Line <-chan string
}
// Process Prints the current line received
func (p *Printer) Process() {
for line := range p.Line {
fmt.Println(line)
}
}
- 创建一个名为
network.go
的文件,内容如下:
package goflow
import (
"github.com/trustmaster/goflow"
)
// NewEncodingApp wires together the components
func NewEncodingApp() *goflow.Graph {
e := goflow.NewGraph()
// define component types
e.Add("encoder", new(Encoder))
e.Add("printer", new(Printer))
// connect the components using channels
e.Connect("encoder", "Res", "printer", "Line")
// map the in channel to Val, which is
// tied to OnVal function
e.MapInPort("In", "encoder", "Val")
return e
}
-
创建一个名为
example
的新目录,并导航到该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/chapter12/goflow"
flow "github.com/trustmaster/goflow"
)
func main() {
net := goflow.NewEncodingApp()
in := make(chan string)
net.SetInPort("In", in)
wait := flow.Run(net)
for i := 0; i < 20; i++ {
in <- fmt.Sprint("Message", i)
}
close(in)
<-wait
}
-
运行
go run main.go
。 -
您也可以运行以下命令:
$ go build $ ./example
现在您应该看到以下输出:
$ go run main.go
Message6 => TWVzc2FnZTY=
Message5 => TWVzc2FnZTU=
Message1 => TWVzc2FnZTE=
Message0 => TWVzc2FnZTA=
Message4 => TWVzc2FnZTQ=
Message8 => TWVzc2FnZTg=
Message2 => TWVzc2FnZTI=
Message3 => TWVzc2FnZTM=
Message7 => TWVzc2FnZTc=
Message10 => TWVzc2FnZTEw
Message9 => TWVzc2FnZTk=
Message12 => TWVzc2FnZTEy
Message11 => TWVzc2FnZTEx
Message14 => TWVzc2FnZTE0
Message13 => TWVzc2FnZTEz
Message16 => TWVzc2FnZTE2
Message15 => TWVzc2FnZTE1
Message18 => TWVzc2FnZTE4
Message17 => TWVzc2FnZTE3
Message19 => TWVzc2FnZTE5
-
go.mod
文件可能会更新,顶级配方目录中现在应该存在go.sum
文件。 -
如果您已经复制或编写了自己的测试,请返回上一级目录并运行
go test
命令。确保所有测试都通过。
它是如何工作的...
github.com/trustmaster/goflow
包的工作方式是定义一个网络/图,注册一些组件,然后将它们连接在一起。这可能会感觉有点容易出错,因为组件是用字符串描述的,但通常在运行时会在应用程序设置和功能正确运行之前就会出现错误。
在这个配方中,我们设置了两个组件,一个是对传入的字符串进行 Base64 编码,另一个是打印传递给它的任何内容。我们将它连接到在main.go
中初始化的输入通道,任何传递到该通道的内容都将通过我们的管道流动。
这种方法的重点很大程度上在于忽略正在进行的内部工作。我们把一切都当作连接的黑匣子,并让goflow
来处理剩下的事情。您可以看到,在这个配方中,完成这个任务流水线的代码是多么简洁,而且我们有更少的旋钮来控制工作人员的数量,等等。
使用 Sarama 与 Kafka
Kafka 是一个流行的分布式消息队列,具有许多用于构建分布式系统的高级功能。本配方将展示如何使用同步生产者向 Kafka 主题写入,并如何使用分区消费者消费相同的主题。本配方不会探讨 Kafka 的不同配置,因为这是一个超出本书范围的更广泛的主题,但我建议从kafka.apache.org/intro
开始。
做好准备
根据以下步骤配置您的环境:
-
参考本章开头的技术要求部分。
-
按照
www.tutorialspoint.com/apache_kafka/apache_kafka_installation_steps.htm
中提到的步骤安装 Kafka。 -
或者,您也可以访问
github.com/spotify/docker-kafka
。
如何做...
这些步骤涵盖了编写和运行您的应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter12/synckafka
的新目录,并导航到该目录。 -
运行此命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter12/synckafka
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter12/synckafka
-
从
~/projects/go-programming-cookbook-original/chapter12/synckafka
复制测试,或者使用这个作为练习来编写一些您自己的代码! -
确保 Kafka 在
localhost:9092
上运行正常。 -
在名为
consumer
的目录中创建一个名为main.go
的文件,其中包含以下内容:
package main
import (
"log"
sarama "github.com/Shopify/sarama"
)
func main() {
consumer, err :=
sarama.NewConsumer([]string{"localhost:9092"}, nil)
if err != nil {
panic(err)
}
defer consumer.Close()
partitionConsumer, err :=
consumer.ConsumePartition("example", 0,
sarama.OffsetNewest)
if err != nil {
panic(err)
}
defer partitionConsumer.Close()
for {
msg := <-partitionConsumer.Messages()
log.Printf("Consumed message: \"%s\" at offset: %d\n",
msg.Value, msg.Offset)
}
}
- 在名为
producer
的目录中创建一个名为main.go
的文件,其中包含以下内容:
package main
import (
"fmt"
"log"
sarama "github.com/Shopify/sarama"
)
func sendMessage(producer sarama.SyncProducer, value string) {
msg := &sarama.ProducerMessage{Topic: "example", Value:
sarama.StringEncoder(value)}
partition, offset, err := producer.SendMessage(msg)
if err != nil {
log.Printf("FAILED to send message: %s\n", err)
return
}
log.Printf("> message sent to partition %d at offset %d\n",
partition, offset)
}
func main() {
producer, err :=
sarama.NewSyncProducer([]string{"localhost:9092"}, nil)
if err != nil {
panic(err)
}
defer producer.Close()
for i := 0; i < 10; i++ {
sendMessage(producer, fmt.Sprintf("Message %d", i))
}
}
-
导航到上一级目录。
-
运行
go run ./consumer
。 -
在与同一目录的另一个终端中运行
go run ./producer
。 -
在生产者终端中,您应该看到以下内容:
$ go run ./producer
2017/05/07 11:50:38 > message sent to partition 0 at offset 0
2017/05/07 11:50:38 > message sent to partition 0 at offset 1
2017/05/07 11:50:38 > message sent to partition 0 at offset 2
2017/05/07 11:50:38 > message sent to partition 0 at offset 3
2017/05/07 11:50:38 > message sent to partition 0 at offset 4
2017/05/07 11:50:38 > message sent to partition 0 at offset 5
2017/05/07 11:50:38 > message sent to partition 0 at offset 6
2017/05/07 11:50:38 > message sent to partition 0 at offset 7
2017/05/07 11:50:38 > message sent to partition 0 at offset 8
2017/05/07 11:50:38 > message sent to partition 0 at offset 9
在消费者终端中,您应该看到以下内容:
$ go run ./consumer
2017/05/07 11:50:38 Consumed message: "Message 0" at offset: 0
2017/05/07 11:50:38 Consumed message: "Message 1" at offset: 1
2017/05/07 11:50:38 Consumed message: "Message 2" at offset: 2
2017/05/07 11:50:38 Consumed message: "Message 3" at offset: 3
2017/05/07 11:50:38 Consumed message: "Message 4" at offset: 4
2017/05/07 11:50:38 Consumed message: "Message 5" at offset: 5
2017/05/07 11:50:38 Consumed message: "Message 6" at offset: 6
2017/05/07 11:50:38 Consumed message: "Message 7" at offset: 7
2017/05/07 11:50:38 Consumed message: "Message 8" at offset: 8
2017/05/07 11:50:38 Consumed message: "Message 9" at offset: 9
-
go.mod
文件可能已更新,顶级配方目录中现在应该存在go.sum
文件。 -
如果您已经复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
如何运作...
该配方演示了通过 Kafka 传递简单消息。更复杂的方法应该使用诸如json
、gob
、protobuf
或其他的序列化格式。生产者可以通过sendMessage
同步地向 Kafka 发送消息。这在 Kafka 集群宕机的情况下处理得不好,并且可能导致这些情况下的进程挂起。这对于诸如 Web 处理程序之类的应用程序来说很重要,因为它可能导致超时并且对 Kafka 集群有硬性依赖。
假设消息队列正确,我们的消费者将观察 Kafka 流并对结果进行处理。本章中的先前配方可能利用此流来进行一些额外的处理。
使用 Kafka 的异步生产者
在继续下一个任务之前,等待 Kafka 生产者完成通常是没有意义的。在这种情况下,您可以使用异步生产者。这些生产者在通道上接收 Sarama 消息,并具有返回成功/错误通道的方法,可以单独检查。
在本配方中,我们将创建一个 Go 例程,用于处理成功和失败的消息,同时允许处理程序排队发送消息,而不管结果如何。
做好准备
参考Sarama 使用 Kafka配方中的做好准备部分。
如何做...
这些步骤涵盖了编写和运行您的应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter12/asynckafka
的新目录,并导航到该目录。 -
运行此命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter12/asynckafka
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter12/asynckafka
-
从
~/projects/go-programming-cookbook-original/chapter12/asynckafka
复制测试,或者使用这个作为练习来编写一些您自己的代码! -
确保 Kafka 在
localhost:9092
上运行正常。 -
从上一个配方中复制 consumer 目录。
-
创建一个名为
producer
的目录并导航到该目录。 -
创建一个名为
producer.go
的文件,其中包含以下内容:
package main
import (
"log"
sarama "github.com/Shopify/sarama"
)
// Process response grabs results and errors from a producer
// asynchronously
func ProcessResponse(producer sarama.AsyncProducer) {
for {
select {
case result := <-producer.Successes():
log.Printf("> message: \"%s\" sent to partition
%d at offset %d\n", result.Value,
result.Partition, result.Offset)
case err := <-producer.Errors():
log.Println("Failed to produce message", err)
}
}
}
- 创建一个名为
handler.go
的文件,其中包含以下内容:
package main
import (
"net/http"
sarama "github.com/Shopify/sarama"
)
// KafkaController allows us to attach a producer
// to our handlers
type KafkaController struct {
producer sarama.AsyncProducer
}
// Handler grabs a message from a GET parama and
// send it to the kafka queue asynchronously
func (c *KafkaController) Handler(w http.ResponseWriter, r
*http.Request) {
if err := r.ParseForm(); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
msg := r.FormValue("msg")
if msg == "" {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("msg must be set"))
return
}
c.producer.Input() <- &sarama.ProducerMessage{Topic:
"example", Key: nil, Value:
sarama.StringEncoder(msg)}
w.WriteHeader(http.StatusOK)
}
- 创建一个名为
main.go
的文件,其中包含以下内容:
package main
import (
"fmt"
"net/http"
sarama "github.com/Shopify/sarama"
)
func main() {
config := sarama.NewConfig()
config.Producer.Return.Successes = true
config.Producer.Return.Errors = true
producer, err :=
sarama.NewAsyncProducer([]string{"localhost:9092"}, config)
if err != nil {
panic(err)
}
defer producer.AsyncClose()
go ProcessResponse(producer)
c := KafkaController{producer}
http.HandleFunc("/", c.Handler)
fmt.Println("Listening on port :3333")
panic(http.ListenAndServe(":3333", nil))
}
-
返回到上一级目录。
-
运行
go run ./consumer
。 -
在与同一目录的另一个终端中运行
go run ./producer
。 -
在第三个终端中,运行以下命令:
$ curl "http://localhost:3333/?msg=this"
$ curl "http://localhost:3333/?msg=is"
$ curl "http://localhost:3333/?msg=an"
$ curl "http://localhost:3333/?msg=example"
在生产者终端中,您应该看到以下内容:
$ go run ./producer
Listening on port :3333
2017/05/07 13:52:54 > message: "this" sent to partition 0 at offset 0
2017/05/07 13:53:25 > message: "is" sent to partition 0 at offset 1
2017/05/07 13:53:27 > message: "an" sent to partition 0 at offset 2
2017/05/07 13:53:29 > message: "example" sent to partition 0 at offset 3
- 在消费者终端中,您应该看到这个:
$ go run ./consumer
2017/05/07 13:52:54 Consumed message: "this" at offset: 0
2017/05/07 13:53:25 Consumed message: "is" at offset: 1
2017/05/07 13:53:27 Consumed message: "an" at offset: 2
2017/05/07 13:53:29 Consumed message: "example" at offset: 3
-
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶级食谱目录中。 -
如果您已经复制或编写了自己的测试,请返回到上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
我们在本章中的修改都是针对生产者的。这一次,我们创建了一个单独的 Go 例程来处理成功和错误。如果这些问题没有得到处理,您的应用程序将陷入死锁。接下来,我们将我们的生产者附加到一个处理程序上,并在收到消息时通过对处理程序的GET
调用发出消息。
处理程序在发送消息后将立即返回成功,而不管其响应如何。如果这是不可接受的,应该改用同步方法。在我们的情况下,我们可以接受稍后分别处理成功和错误。
最后,我们用几条不同的消息curl
我们的端点,您可以看到它们从处理程序流向我们在上一节中编写的 Kafka 消费者最终打印的地方。
将 Kafka 连接到 Goflow
这个食谱将把 Kafka 消费者与 Goflow 管道结合起来。当我们的消费者从 Kafka 接收消息时,它将对它们运行strings.ToUpper()
,然后打印结果。这些自然配对,因为 Goflow 旨在操作传入流,这正是 Kafka 提供给我们的。
准备就绪
参考使用 Sarama 与 Kafka食谱的准备就绪*部分。
如何做...
这些步骤涵盖了编写和运行应用程序的过程:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter12/kafkaflow
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter12/kafkaflow
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter12/kafkaflow
-
从
~/projects/go-programming-cookbook-original/chapter12/kafkaflow
复制测试,或者将其用作编写一些自己代码的练习! -
确保 Kafka 在
localhost:9092
上运行。 -
创建一个名为
components.go
的文件,其中包含以下内容:
package kafkaflow
import (
"fmt"
"strings"
flow "github.com/trustmaster/goflow"
)
// Upper upper cases the incoming
// stream
type Upper struct {
Val <-chan string
Res chan<- string
}
// Process loops over the input values and writes the upper
// case string version of them to Res
func (e *Upper) Process() {
for val := range e.Val {
e.Res <- strings.ToUpper(val)
}
}
// Printer is a component for printing to stdout
type Printer struct {
flow.Component
Line <-chan string
}
// Process Prints the current line received
func (p *Printer) Process() {
for line := range p.Line {
fmt.Println(line)
}
}
- 创建一个名为
network.go
的文件,其中包含以下内容:
package kafkaflow
import "github.com/trustmaster/goflow"
// NewUpperApp wires together the components
func NewUpperApp() *goflow.Graph {
u := goflow.NewGraph()
u.Add("upper", new(Upper))
u.Add("printer", new(Printer))
u.Connect("upper", "Res", "printer", "Line")
u.MapInPort("In", "upper", "Val")
return u
}
- 在名为
consumer
的目录中创建一个名为main.go
的文件,其中包含以下内容:
package main
import (
"github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter12/kafkaflow"
sarama "github.com/Shopify/sarama"
flow "github.com/trustmaster/goflow"
)
func main() {
consumer, err := sarama.NewConsumer([]string{"localhost:9092"}, nil)
if err != nil {
panic(err)
}
defer consumer.Close()
partitionConsumer, err := consumer.ConsumePartition("example", 0, sarama.OffsetNewest)
if err != nil {
panic(err)
}
defer partitionConsumer.Close()
net := kafkaflow.NewUpperApp()
in := make(chan string)
net.SetInPort("In", in)
wait := flow.Run(net)
defer func() {
close(in)
<-wait
}()
for {
msg := <-partitionConsumer.Messages()
in <- string(msg.Value)
}
}
-
从使用 Sarama 与 Kafka食谱中复制
producer
目录。 -
运行
go run ./consumer
。 -
在与同一目录的另一个终端中运行
go run ./producer
。 -
在生产者终端中,您现在应该看到以下内容:
$ go run ./producer
2017/05/07 18:24:12 > message "Message 0" sent to partition 0 at offset 0
2017/05/07 18:24:12 > message "Message 1" sent to partition 0 at offset 1
2017/05/07 18:24:12 > message "Message 2" sent to partition 0 at offset 2
2017/05/07 18:24:12 > message "Message 3" sent to partition 0 at offset 3
2017/05/07 18:24:12 > message "Message 4" sent to partition 0 at offset 4
2017/05/07 18:24:12 > message "Message 5" sent to partition 0 at offset 5
2017/05/07 18:24:12 > message "Message 6" sent to partition 0 at offset 6
2017/05/07 18:24:12 > message "Message 7" sent to partition 0 at offset 7
2017/05/07 18:24:12 > message "Message 8" sent to partition 0 at offset 8
2017/05/07 18:24:12 > message "Message 9" sent to partition 0 at offset 9
在消费者终端中,您应该看到以下内容:
$ go run ./consumer
MESSAGE 0
MESSAGE 1
MESSAGE 2
MESSAGE 3
MESSAGE 4
MESSAGE 5
MESSAGE 6
MESSAGE 7
MESSAGE 8
MESSAGE 9
-
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶级食谱目录中。 -
如果您已经复制或编写了自己的测试,请返回到上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
这个食谱结合了本章中以前食谱的想法。与以前的食谱一样,我们设置了 Kafka 消费者和生产者。这个食谱使用了使用 Sarama 与 Kafka食谱中的同步生产者,但也可以使用异步生产者。一旦收到消息,我们就像在数据流编程的 Goflow食谱中一样,在输入通道上排队。我们修改了这个食谱中的组件,将我们的传入字符串转换为大写,而不是 Base64 编码。我们重用打印组件,结果网络配置类似。
最终结果是,通过 Kafka 消费者接收的所有消息都被传输到我们基于流的工作流中进行操作。这使我们能够将我们的工作流组件进行模块化和可重用,并且我们可以在不同的配置中多次使用相同的组件。同样,我们将从任何写入 Kafka 的生产者接收流量,因此我们可以将生产者多路复用成单个数据流。
在 Go 中编写 GraphQL 服务器
GraphQL 是由 Facebook 创建的 REST 的替代品(graphql.org/
)。这项技术允许服务器实现和发布模式,然后客户端可以请求他们需要的信息,而不是理解和利用各种 API 端点。
对于这个示例,我们将创建一个代表一副扑克牌的Graphql
模式。我们将公开一个名为 card 的资源,可以按花色和值进行过滤。或者,如果未指定参数,此模式可以返回牌组中的所有牌。
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter12/graphql
的新目录,并导航到该目录。 -
运行此命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter12/graphql
应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter12/graphql
-
从
~/projects/go-programming-cookbook-original/chapter12/graphql
复制测试,或者将其用作练习来编写自己的代码! -
创建并导航到
cards
目录。 -
创建一个名为
card.go
的文件,其中包含以下内容:
package cards
// Card represents a standard playing
// card
type Card struct {
Value string
Suit string
}
var cards []Card
func init() {
cards = []Card{
{"A", "Spades"}, {"2", "Spades"}, {"3", "Spades"},
{"4", "Spades"}, {"5", "Spades"}, {"6", "Spades"},
{"7", "Spades"}, {"8", "Spades"}, {"9", "Spades"},
{"10", "Spades"}, {"J", "Spades"}, {"Q", "Spades"},
{"K", "Spades"},
{"A", "Hearts"}, {"2", "Hearts"}, {"3", "Hearts"},
{"4", "Hearts"}, {"5", "Hearts"}, {"6", "Hearts"},
{"7", "Hearts"}, {"8", "Hearts"}, {"9", "Hearts"},
{"10", "Hearts"}, {"J", "Hearts"}, {"Q", "Hearts"},
{"K", "Hearts"},
{"A", "Clubs"}, {"2", "Clubs"}, {"3", "Clubs"},
{"4", "Clubs"}, {"5", "Clubs"}, {"6", "Clubs"},
{"7", "Clubs"}, {"8", "Clubs"}, {"9", "Clubs"},
{"10", "Clubs"}, {"J", "Clubs"}, {"Q", "Clubs"},
{"K", "Clubs"},
{"A", "Diamonds"}, {"2", "Diamonds"}, {"3",
"Diamonds"},
{"4", "Diamonds"}, {"5", "Diamonds"}, {"6",
"Diamonds"},
{"7", "Diamonds"}, {"8", "Diamonds"}, {"9",
"Diamonds"},
{"10", "Diamonds"}, {"J", "Diamonds"}, {"Q",
"Diamonds"},
{"K", "Diamonds"},
}
}
- 创建一个名为
type.go
的文件,其中包含以下内容:
package cards
import "github.com/graphql-go/graphql"
// CardType returns our card graphql object
func CardType() *graphql.Object {
cardType := graphql.NewObject(graphql.ObjectConfig{
Name: "Card",
Description: "A Playing Card",
Fields: graphql.Fields{
"value": &graphql.Field{
Type: graphql.String,
Description: "Ace through King",
Resolve: func(p graphql.ResolveParams)
(interface{}, error) {
if card, ok := p.Source.(Card); ok {
return card.Value, nil
}
return nil, nil
},
},
"suit": &graphql.Field{
Type: graphql.String,
Description: "Hearts, Diamonds, Clubs, Spades",
Resolve: func(p graphql.ResolveParams)
(interface{}, error) {
if card, ok := p.Source.(Card); ok {
return card.Suit, nil
}
return nil, nil
},
},
},
})
return cardType
}
- 创建一个名为
resolve.go
的文件,其中包含以下内容:
package cards
import (
"strings"
"github.com/graphql-go/graphql"
)
// Resolve handles filtering cards
// by suit and value
func Resolve(p graphql.ResolveParams) (interface{}, error) {
finalCards := []Card{}
suit, suitOK := p.Args["suit"].(string)
suit = strings.ToLower(suit)
value, valueOK := p.Args["value"].(string)
value = strings.ToLower(value)
for _, card := range cards {
if suitOK && suit != strings.ToLower(card.Suit) {
continue
}
if valueOK && value != strings.ToLower(card.Value) {
continue
}
finalCards = append(finalCards, card)
}
return finalCards, nil
}
- 创建一个名为
schema.go
的文件,其中包含以下内容:
package cards
import "github.com/graphql-go/graphql"
// Setup prepares and returns our card
// schema
func Setup() (graphql.Schema, error) {
cardType := CardType()
// Schema
fields := graphql.Fields{
"cards": &graphql.Field{
Type: graphql.NewList(cardType),
Args: graphql.FieldConfigArgument{
"suit": &graphql.ArgumentConfig{
Description: "Filter cards by card suit
(hearts, clubs, diamonds, spades)",
Type: graphql.String,
},
"value": &graphql.ArgumentConfig{
Description: "Filter cards by card
value (A-K)",
Type: graphql.String,
},
},
Resolve: Resolve,
},
}
rootQuery := graphql.ObjectConfig{Name: "RootQuery",
Fields: fields}
schemaConfig := graphql.SchemaConfig{Query:
graphql.NewObject(rootQuery)}
schema, err := graphql.NewSchema(schemaConfig)
return schema, err
}
-
导航回
graphql
目录。 -
创建一个名为
example
的新目录并导航到该目录。 -
创建一个名为
main.go
的文件,其中包含以下内容:
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter12/graphql/cards"
"github.com/graphql-go/graphql"
)
func main() {
// grab our schema
schema, err := cards.Setup()
if err != nil {
panic(err)
}
// Query
query := `
{
cards(value: "A"){
value
suit
}
}`
params := graphql.Params{Schema: schema, RequestString:
query}
r := graphql.Do(params)
if len(r.Errors) > 0 {
log.Fatalf("failed to execute graphql operation,
errors: %+v", r.Errors)
}
rJSON, err := json.MarshalIndent(r, "", " ")
if err != nil {
panic(err)
}
fmt.Printf("%s \n", rJSON)
}
-
运行
go run main.go
。 -
您还可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
{
"data": {
"cards": [
{
"suit": "Spades",
"value": "A"
},
{
"suit": "Hearts",
"value": "A"
},
{
"suit": "Clubs",
"value": "A"
},
{
"suit": "Diamonds",
"value": "A"
}
]
}
}
- 测试一些额外的查询,例如以下内容:
-
cards(suit: "Spades")
-
cards(value: "3", suit:"Diamonds")
-
go.mod
文件可能已更新,并且go.sum
文件现在应该存在于顶级示例目录中。 -
如果您已经复制或编写了自己的测试,请返回上一个目录并运行
go test
。确保所有测试都通过。
工作原理...
cards.go
文件定义了一个card
对象,并在名为cards
的全局变量中初始化了基本牌组。这种状态也可以保存在长期存储中,例如数据库中。然后,我们在types.go
中定义了CardType
,它允许graphql
将卡对象解析为响应。接下来,我们进入resolve.go
,在那里我们定义了如何按值和类型过滤卡片。这个Resolve
函数将被最终的模式使用,该模式在schema.go
中定义。
例如,您可以修改此示例中的Resolve
函数,以从数据库中检索数据。最后,我们加载模式并对其运行查询。这是一个小修改,将我们的模式挂载到 REST 端点,但为了简洁起见,此示例只运行一个硬编码查询。有关GraphQL
查询的更多信息,请访问graphql.org/learn/queries/
。
第十三章:无服务器编程
本章将重点介绍无服务器架构以及如何在 Go 语言中使用它们。无服务器架构是指开发人员不管理后端服务器的架构。这包括 Amazon Lambda、Google App Engine 和 Firebase 等服务。这些服务允许您快速部署应用程序并在网络上存储数据。
本章中的所有示例都涉及到按使用计费的第三方服务;确保在使用完毕后进行清理。否则,可以将这些示例视为在这些平台上启动更大型应用程序的起步器。
在本章中,我们将涵盖以下内容:
-
使用 Apex 在 Lambda 上进行 Go 编程
-
Apex 无服务器日志和指标
-
使用 Go 的 Google App Engine
-
使用
firebase.google.com/go
与 Firebase 一起工作
使用 Apex 在 Lambda 上进行 Go 编程
Apex 是一个用于构建、部署和管理 AWS Lambda 函数的工具。它曾经提供了一个用于在代码中管理 Lambda 函数的 Go shim
,但现在可以使用原生的 AWS 库(github.com/aws/aws-lambda-go
)来完成这个任务。本教程将探讨如何创建 Go Lambda 函数并使用 Apex 部署它们。
准备工作
根据以下步骤配置您的环境:
-
从
golang.org/doc/install
下载并安装 Go 1.12.6 或更高版本到您的操作系统上。 -
从
apex.run/#installation
安装 Apex。 -
打开终端或控制台应用程序,并创建并导航到一个项目目录,例如
~/projects/go-programming-cookbook
。本教程中涵盖的所有代码都将在此目录中运行和修改。 -
将最新的代码克隆到
~/projects/go-programming-cookbook-original
。在这里,您可以选择从该目录中工作,而不是手动输入示例:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
如何做...
这些步骤涵盖了编写和运行您的应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter13/lambda
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter13/lambda
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter13/lambda
-
创建一个 Amazon 账户和一个可以编辑 Lambda 函数的 IAM 角色,可以从
aws.amazon.com/lambda/
完成。 -
创建一个名为
~/.aws/credentials
的文件,内容如下,将您在 Amazon 控制台中设置的凭据复制进去:
[default]
aws_access_key_id = xxxxxxxx
aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxxxx
- 创建一个环境变量来保存您想要的区域:
export AWS_REGION=us-west-2
- 运行
apex init
命令并按照屏幕上的说明进行操作:
$ apex init
Enter the name of your project. It should be machine-friendly, as this is used to prefix your functions in Lambda.
Project name: go-cookbook
Enter an optional description of your project.
Project description: Demonstrating Apex with the Go Cookbook
[+] creating IAM go-cookbook_lambda_function role
[+] creating IAM go-cookbook_lambda_logs policy
[+] attaching policy to lambda_function role.
[+] creating ./project.json
[+] creating ./functions
Setup complete, deploy those functions!
$ apex deploy
-
删除
lambda/functions/hello
目录。 -
创建一个新的
lambda/functions/greeter1/main.go
文件,内容如下:
package main
import (
"context"
"fmt"
"github.com/aws/aws-lambda-go/lambda"
)
// Message is the input to the function and
// includes a Name
type Message struct {
Name string `json:"name"`
}
// Response is sent back and contains a greeting
// string
type Response struct {
Greeting string `json:"greeting"`
}
// HandleRequest will be called when the lambda function is invoked
// it takes a Message and returns a Response that contains a greeting
func HandleRequest(ctx context.Context, m Message) (Response, error) {
return Response{Greeting: fmt.Sprintf("Hello, %s", m.Name)}, nil
}
func main() {
lambda.Start(HandleRequest)
}
- 创建一个新的
lambda/functions/greeter/main.go
文件,内容如下:
package main
import (
"context"
"fmt"
"github.com/aws/aws-lambda-go/lambda"
)
// Message is the input to the function and
// includes a FirstName and LastName
type Message struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
// Response is sent back and contains a greeting
// string
type Response struct {
Greeting string `json:"greeting"`
}
// HandleRequest will be called when the lambda function is invoked
// it takes a Message and returns a Response that contains a greeting
// this greeting contains the first and last name specified
func HandleRequest(ctx context.Context, m Message) (Response, error) {
return Response{Greeting: fmt.Sprintf("Hello, %s %s", m.FirstName, m.LastName)}, nil
}
func main() {
lambda.Start(HandleRequest)
}
- 部署它们:
$ apex deploy
• creating function env= function=greeter2
• creating function env= function=greeter1
• created alias current env= function=greeter2 version=4
• function created env= function=greeter2 name=go-cookbook_greeter2 version=1
• created alias current env= function=greeter1 version=5
• function created env= function=greeter1 name=go-cookbook_greeter1 version=1
- 调用新部署的函数:
$ echo '{"name": "Reader"}' | apex invoke greeter1 {"greeting":"Hello, Reader"}
$ echo '{"first_name": "Go", "last_name": "Coders"}' | apex invoke greeter2 {"greeting":"Hello, Go Coders"}
- 查看日志:
$ apex logs greeter2
apex logs greeter2
/aws/lambda/go-cookbook_greeter2 START RequestId: 7c0f9129-3830-11e7-8755-75aeb52a51b9 Version: 1
/aws/lambda/go-cookbook_greeter2 END RequestId: 7c0f9129-3830-11e7-8755-75aeb52a51b9
/aws/lambda/go-cookbook_greeter2 REPORT RequestId: 7c0f9129-3830-11e7-8755-75aeb52a51b9 Duration: 93.84 ms Billed Duration: 100 ms
Memory Size: 128 MB Max Memory Used: 19 MB
- 清理已部署的服务:
$ apex delete
The following will be deleted:
- greeter1 - greeter2
Are you sure? (yes/no) yes
• deleting env= function=greeter
• function deleted env= function=greeter
它是如何工作的...
AWS Lambda 使得无需维护服务器即可按需运行函数变得容易。Apex 提供了部署、版本控制和测试函数的功能,使您可以将它们发送到 Lambda。
Go 库(github.com/aws/aws-lambda-go
)在 Lambda 中提供了原生的 Go 编译,并允许我们将 Go 代码部署为 Lambda 函数。这是通过定义一个处理程序、处理传入的请求有效负载并返回响应来实现的。目前,您定义的函数必须遵循这些规则:
-
处理程序必须是一个函数。
-
处理程序可能需要零到两个参数。
-
如果有两个参数,则第一个参数必须满足
context.Context
接口。 -
处理程序可能返回零到两个参数。
-
如果有两个返回值,则第二个参数必须是一个错误。
-
如果只有一个返回值,它必须是一个错误。
在这个配方中,我们定义了两个问候函数,一个接受全名,另一个将名字分成名和姓。如果我们修改了一个函数greeter
,而不是创建两个,Apex 将部署新版本,并在所有先前的示例中调用v2
而不是v1
。也可以使用apex rollback greeter
进行回滚。
Apex 无服务器日志和指标
在使用 Lambda 等无服务器函数时,拥有可移植的结构化日志非常有价值。此外,您还可以将处理日志的早期配方与此配方结合使用。我们在第四章Go 中的错误处理中涵盖的配方同样相关。因为我们使用 Apex 来管理我们的 Lambda 函数,所以我们选择使用 Apex 记录器进行此配方。我们还将依赖 Apex 提供的指标,以及 AWS 控制台。早期的配方探讨了更复杂的日志记录和指标示例,这些仍然适用——Apex 记录器可以轻松配置为使用例如 Amazon Kinesis 或 Elasticsearch 来聚合日志。
准备工作
参考本章中Go 编程在 Apex 上的 Lambda配方的准备工作部分。
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter13/logging
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter13/logging
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter13/logging
-
创建一个可以编辑 Lambda 函数的 Amazon 帐户和 IAM 角色,可以在
aws.amazon.com/lambda/
上完成。 -
创建一个
~/.aws/credentials
文件,其中包含以下内容,将您在 Amazon 控制台中设置的凭据复制过来:
[default]
aws_access_key_id = xxxxxxxx
aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxxxx
- 创建一个环境变量来保存您想要的区域:
export AWS_REGION=us-west-2
- 运行
apex init
命令并按照屏幕上的说明进行操作:
$ apex init
Enter the name of your project. It should be machine-friendly, as this is used to prefix your functions in Lambda.
Project name: logging
Enter an optional description of your project.
Project description: An example of apex logging and metrics
[+] creating IAM logging_lambda_function role
[+] creating IAM logging_lambda_logs policy
[+] attaching policy to lambda_function role.
[+] creating ./project.json
[+] creating ./functions
Setup complete, deploy those functions!
$ apex deploy
-
删除
lambda/functions/hello
目录。 -
创建一个新的
lambda/functions/secret/main.go
文件,其中包含以下内容:
package main
import (
"context"
"os"
"github.com/apex/log"
"github.com/apex/log/handlers/text"
"github.com/aws/aws-lambda-go/lambda"
)
// Input takes in a secret
type Input struct {
Secret string `json:"secret"`
}
// HandleRequest will be called when the Lambda function is invoked
// it takes an input and checks if it matches our super secret value
func HandleRequest(ctx context.Context, input Input) (string, error) {
log.SetHandler(text.New(os.Stderr))
log.WithField("secret", input.Secret).Info("secret guessed")
if input.Secret == "klaatu barada nikto" {
return "secret guessed!", nil
}
return "try again", nil
}
func main() {
lambda.Start(HandleRequest)
}
- 将其部署到指定的区域:
$ apex deploy
• creating function env= function=secret
• created alias current env= function=secret version=1
• function created env= function=secret name=logging_secret version=1
- 要调用它,请运行以下命令:
$ echo '{"secret": "open sesame"}' | apex invoke secret
"try again"
$ echo '{"secret": "klaatu barada nikto"}' | apex invoke secret
"secret guessed!"
- 检查日志:
$ apex logs secret
/aws/lambda/logging_secret START RequestId: cfa6f655-3834-11e7-b99d-89998a7f39dd Version: 1
/aws/lambda/logging_secret INFO[0000] secret guessed secret=open sesame
/aws/lambda/logging_secret END RequestId: cfa6f655-3834-11e7-b99d-89998a7f39dd
/aws/lambda/logging_secret REPORT RequestId: cfa6f655-3834-11e7-b99d-89998a7f39dd Duration: 52.23 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 19 MB
/aws/lambda/logging_secret START RequestId: d74ea688-3834-11e7-aa4e-d592c1fbc35f Version: 1
/aws/lambda/logging_secret INFO[0012] secret guessed secret=klaatu barada nikto
/aws/lambda/logging_secret END RequestId: d74ea688-3834-11e7-aa4e-d592c1fbc35f
/aws/lambda/logging_secret REPORT RequestId: d74ea688-3834-11e7-aa4e-d592c1fbc35f Duration: 7.43 ms Billed Duration: 100 ms
Memory Size: 128 MB Max Memory Used: 19 MB
- 检查您的指标:
$ apex metrics secret
secret
total cost: $0.00
invocations: 0 ($0.00)
duration: 0s ($0.00)
throttles: 0
errors: 0
memory: 128
- 清理已部署的服务:
$ apex delete
Are you sure? (yes/no) yes
• deleting env= function=secret
• function deleted env= function=secret
它是如何工作的...
在这个配方中,我们创建了一个名为 secret 的新 Lambda 函数,它将根据您是否猜对了秘密短语来做出响应。该函数解析传入的 JSON 请求,使用Stderr
进行一些日志记录,并返回一个响应。
使用函数几次后,我们可以看到我们的日志可以使用apex logs
命令查看。此命令可以在单个 Lambda 函数或所有受管理的函数上运行。如果您正在链接 Apex 命令并希望观看许多服务的日志,这将非常有用。
此外,我们还向您展示了如何使用apex metrics
命令收集有关应用程序的一般指标,包括成本和调用。您还可以在 Lambda 部分的 AWS 控制台中直接查看大量此信息。与其他配方一样,我们在最后尽力清理。
使用 Go 的 Google App Engine
App Engine 是谷歌的一个服务,可以快速部署 Web 应用程序。这些应用程序可以访问云存储和各种其他谷歌 API。总体思路是 App Engine 将根据负载轻松扩展,并简化与托管应用相关的任何操作管理。这个配方将展示如何创建并可选部署一个基本的 App Engine 应用程序。这个配方不会深入讨论设置谷歌云帐户、设置计费或清理实例的具体细节。作为最低要求,此配方需要访问 Google Cloud Datastore (cloud.google.com/datastore/docs/concepts/overview
)。
准备工作
根据这些步骤配置您的环境:
-
从
golang.org/doc/install
下载并安装 Go 1.11.1 或更高版本到您的操作系统。 -
从
cloud.google.com/appengine/docs/flexible/go/quickstart
下载 Google Cloud SDK。 -
创建一个允许您执行数据存储访问并记录应用程序名称的应用程序。对于这个配方,我们将使用
go-cookbook
。 -
安装
gcloud components install app-engine-go
Go app engine 组件。 -
打开终端或控制台应用程序,并创建并导航到一个项目目录,例如
~/projects/go-programming-cookbook
。本配方中涵盖的所有代码都将从此目录运行和修改。 -
将最新的代码克隆到
~/projects/go-programming-cookbook-original
。在这里,您可以选择从该目录中工作,而不是手动输入示例:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter13/appengine
的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter13/appengine
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter13/appengine
- 创建一个名为
app.yml
的文件,其中包含以下内容,将go-cookbook
替换为您在准备就绪部分创建的应用程序名称:
runtime: go112
manual_scaling:
instances: 1
#[START env_variables]
env_variables:
GCLOUD_DATASET_ID: go-cookbook
#[END env_variables]
- 创建一个名为
message.go
的文件,其中包含以下内容:
package main
import (
"context"
"time"
"cloud.google.com/go/datastore"
)
// Message is the object we store
type Message struct {
Timestamp time.Time
Message string
}
func (c *Controller) storeMessage(ctx context.Context, message
string) error {
m := &amp;Message{
Timestamp: time.Now(),
Message: message,
}
k := datastore.IncompleteKey("Message", nil)
_, err := c.store.Put(ctx, k, m)
return err
}
func (c *Controller) queryMessages(ctx context.Context, limit
int) ([]*Message, error) {
q := datastore.NewQuery("Message").
Order("-Timestamp").
Limit(limit)
messages := make([]*Message, 0)
_, err := c.store.GetAll(ctx, q, &amp;messages)
return messages, err
}
- 创建一个名为
controller.go
的文件,其中包含以下内容:
package main
import (
"context"
"fmt"
"log"
"net/http"
"cloud.google.com/go/datastore"
)
// Controller holds our storage and other
// state
type Controller struct {
store *datastore.Client
}
func (c *Controller) handle(w http.ResponseWriter, r
*http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "invalid method",
http.StatusMethodNotAllowed)
return
}
ctx := context.Background()
// store the new message
r.ParseForm()
if message := r.FormValue("message"); message != "" {
if err := c.storeMessage(ctx, message); err != nil {
log.Printf("could not store message: %v", err)
http.Error(w, "could not store
message",
http.StatusInternalServerError)
return
}
}
// get the current messages and display them
fmt.Fprintln(w, "Messages:")
messages, err := c.queryMessages(ctx, 10)
if err != nil {
log.Printf("could not get messages: %v", err)
http.Error(w, "could not get messages",
http.StatusInternalServerError)
return
}
for _, message := range messages {
fmt.Fprintln(w, message.Message)
}
}
- 创建一个名为
main.go
的文件,其中包含以下内容:
package main
import (
"log"
"net/http"
"os"
"cloud.google.com/go/datastore"
"golang.org/x/net/context"
"google.golang.org/appengine"
)
func main() {
ctx := context.Background()
log.SetOutput(os.Stderr)
// Set this in app.yaml when running in production.
projectID := os.Getenv("GCLOUD_DATASET_ID")
datastoreClient, err := datastore.NewClient(ctx, projectID)
if err != nil {
log.Fatal(err)
}
c := Controller{datastoreClient}
http.HandleFunc("/", c.handle)
port := os.Getenv("PORT")
if port == "" {
port = "8080"
log.Printf("Defaulting to port %s", port)
}
log.Printf("Listening on port %s", port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}
-
运行
gcloud config set project go-cookbook
命令,其中go-cookbook
是您在准备就绪部分创建的项目。 -
运行
gcloud auth application-default login
命令,并按照说明操作。 -
运行
export PORT=8080
命令。 -
运行
export GCLOUD_DATASET_ID=go-cookbook
命令,其中go-cookbook
是您在准备就绪部分创建的项目。 -
运行
go build
命令。 -
运行
./appengine
命令。 -
尝试几条消息(
?message=other
)。 -
可选择使用
gcloud app deploy
将应用程序部署到您的实例。 -
使用
gcloud app browse
导航到部署的应用程序。 -
可选择清理您的
appengine
实例和数据存储在以下 URL:
-
go.mod
文件可能会更新,go.sum
文件现在应该存在于顶级配方目录中。 -
如果您复制或编写了自己的测试,请运行
go test
命令。确保所有测试都通过。
它是如何工作的...
一旦云 SDK 配置为指向您的应用程序并已经经过身份验证,GCloud 工具允许快速部署和配置,使本地应用程序能够访问 Google 服务。
在验证和设置端口之后,我们在localhost
上运行应用程序,然后可以开始使用代码。该应用程序定义了一个可以从数据存储中存储和检索的消息对象。这演示了您可能如何隔离这种代码。您还可以使用存储/数据库接口,如前几章所示。
接下来,我们设置一个处理程序,尝试将消息插入数据存储,然后检索所有消息,在浏览器中显示它们。这创建了类似基本留言簿的东西。您可能会注意到消息并不总是立即出现。如果您在没有消息参数的情况下导航或发送另一条消息,它应该在重新加载时出现。
最后,请确保在不再使用它们时清理实例。
使用 firebase.google.com/go 使用 Firebase 进行工作
Firebase 是另一个谷歌云服务,它创建了一个可扩展、易于管理的数据库,可以支持身份验证,并且特别适用于移动应用程序。在这个示例中,我们将使用最新的 Firestore 作为我们的数据库后端。Firebase 服务提供的功能远远超出了本示例涵盖的范围,但我们只会关注存储和检索数据。我们还将研究如何为您的应用程序设置身份验证,并使用我们自己的自定义客户端封装 Firebase 客户端。
准备工作
根据以下步骤配置您的环境:
-
从
golang.org/doc/install
下载并安装 Go 1.11.1 或更高版本到您的操作系统。 -
在
console.firebase.google.com/
创建一个 Firebase 帐户、项目和数据库。
此示例以测试模式运行,默认情况下不安全。
-
通过访问
console.firebase.google.com/project/go-cookbook/settings/serviceaccounts/adminsdk
生成服务管理员令牌。在这里,go-cookbook
将替换为您的项目名称。 -
将下载的令牌移动到
/tmp/service_account.json
。 -
打开终端或控制台应用程序,并创建并导航到一个项目目录,例如
~/projects/go-programming-cookbook
。本示例中涵盖的所有代码都将从该目录运行和修改。 -
将最新的代码克隆到
~/projects/go-programming-cookbook-original
。在这里,您可以选择从该目录工作,而不是手动输入示例:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter13/firebase
的新目录,并进入该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter13/firebase
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter13/firebase
- 创建一个名为
client.go
的文件,内容如下:
package firebase
import (
"context"
"cloud.google.com/go/firestore"
"github.com/pkg/errors"
)
// Client Interface for mocking
type Client interface {
Get(ctx context.Context, key string) (interface{}, error)
Set(ctx context.Context, key string, value interface{}) error
Close() error
}
// firestore.Client implements Close()
// we create Get and Set
type firebaseClient struct {
*firestore.Client
collection string
}
func (f *firebaseClient) Get(ctx context.Context, key string) (interface{}, error) {
data, err := f.Collection(f.collection).Doc(key).Get(ctx)
if err != nil {
return nil, errors.Wrap(err, "get failed")
}
return data.Data(), nil
}
func (f *firebaseClient) Set(ctx context.Context, key string, value interface{}) error {
set := make(map[string]interface{})
set[key] = value
_, err := f.Collection(f.collection).Doc(key).Set(ctx, set)
return errors.Wrap(err, "set failed")
}
- 创建一个名为
auth.go
的文件,内容如下:
package firebase
import (
"context"
firebase "firebase.google.com/go"
"github.com/pkg/errors"
"google.golang.org/api/option"
)
// Authenticate grabs oauth scopes using a generated
// service_account.json file from
// https://console.firebase.google.com/project/go-cookbook/settings/serviceaccounts/adminsdk
func Authenticate(ctx context.Context, collection string) (Client, error) {
opt := option.WithCredentialsFile("/tmp/service_account.json")
app, err := firebase.NewApp(ctx, nil, opt)
if err != nil {
return nil, errors.Wrap(err, "error initializing app")
}
client, err := app.Firestore(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to intialize filestore")
}
return &amp;firebaseClient{Client: client, collection: collection}, nil
}
-
创建一个名为
example
的新目录并进入该目录。 -
创建一个名为
main.go
的文件,内容如下:
package main
import (
"context"
"fmt"
"log"
"github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter13/firebase"
)
func main() {
ctx := context.Background()
c, err := firebase.Authenticate(ctx, "collection")
if err != nil {
log.Fatalf("error initializing client: %v", err)
}
defer c.Close()
if err := c.Set(ctx, "key", []string{"val1", "val2"}); err != nil {
log.Fatalf(err.Error())
}
res, err := c.Get(ctx, "key")
if err != nil {
log.Fatalf(err.Error())
}
fmt.Println(res)
if err := c.Set(ctx, "key2", []string{"val3", "val4"}); err != nil {
log.Fatalf(err.Error())
}
res, err = c.Get(ctx, "key2")
if err != nil {
log.Fatalf(err.Error())
}
fmt.Println(res)
}
-
运行
go run main.go
。 -
您也可以运行
go build ./example
。您应该会看到以下输出:
$ go run main.go
[val1 val2]
[val3 val4]
-
go.mod
文件可能已更新,顶级示例目录中现在应该存在go.sum
文件。 -
如果您复制或编写了自己的测试,返回上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
Firebase 提供了方便的功能,让您可以使用凭据文件登录。登录后,我们可以存储任何类型的结构化、类似地图的对象。在这种情况下,我们存储map[string]interface{}
。这些数据可以被多个客户端访问,包括 Web 和移动设备。
客户端代码将所有操作封装在一个接口中,以便进行测试。这是编写客户端代码时常见的模式,也用于其他示例中。在我们的情况下,我们创建了一个Get
和Set
函数,用于按键存储和检索值。我们还公开了Close()
,以便使用客户端的代码可以延迟close()
并在最后清理我们的连接。
第十四章:性能改进、技巧和诀窍
在本章中,我们将专注于优化应用程序和发现瓶颈。这些都是一些可立即被现有应用程序使用的技巧。如果您或您的组织需要完全可重现的构建,许多这些食谱是必需的。当您想要对应用程序的性能进行基准测试时,它们也是有用的。最后一个食谱侧重于提高 HTTP 的速度;然而,重要的是要记住网络世界变化迅速,重要的是要及时了解最佳实践。例如,如果您需要 HTTP/2,自 Go 1.6 版本以来,可以使用内置的 Go net/http
包。
在这一章中,我们将涵盖以下食谱:
-
使用 pprof 工具
-
基准测试和发现瓶颈
-
内存分配和堆管理
-
使用 fasthttprouter 和 fasthttp
技术要求
为了继续本章中的所有食谱,请根据以下步骤配置您的环境:
-
在您的操作系统上从
golang.org/doc/install
下载并安装 Go 1.12.6 或更高版本。 -
打开一个终端或控制台应用程序,并创建并导航到一个项目目录,例如
~/projects/go-programming-cookbook
。所有代码将从该目录运行和修改。 -
将最新的代码克隆到
~/projects/go-programming-cookbook-original
,并选择从该目录工作,而不是手动输入示例:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
- 可选地,从
www.graphviz.org/Home.php
安装 Graphviz。
使用 pprof 工具
pprof
工具允许 Go 应用程序收集和导出运行时分析数据。它还提供了用于从 Web 界面访问工具的 Webhook。本食谱将创建一个基本应用程序,验证bcrypt
哈希密码与明文密码,然后对应用程序进行分析。
您可能希望在第十一章 分布式系统中涵盖pprof
工具,以及其他指标和监控食谱。但它实际上被放在了本章,因为它将用于分析和改进程序,就像基准测试可以使用一样。因此,本食谱将主要关注于使用pprof
来分析和改进应用程序的内存使用情况。
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter14/pprof
的新目录,并导航到该目录。 -
运行此命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter14/pprof
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter14/pprof
-
从
~/projects/go-programming-cookbook-original/chapter14/pprof
复制测试,或者使用这个作为练习来编写一些您自己的代码! -
创建一个名为
crypto
的目录并导航到该目录。 -
创建一个名为
handler.go
的文件,内容如下:
package crypto
import (
"net/http"
"golang.org/x/crypto/bcrypt"
)
// GuessHandler checks if ?message=password
func GuessHandler(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil{
// if we can't parse the form
// we'll assume it is malformed
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("error reading guess"))
return
}
msg := r.FormValue("message")
// "password"
real :=
[]byte("$2a$10$2ovnPWuIjMx2S0HvCxP/mutzdsGhyt8rq/
JqnJg/6OyC3B0APMGlK")
if err := bcrypt.CompareHashAndPassword(real, []byte(msg));
err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("try again"))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("you got it"))
return
}
-
导航到上一级目录。
-
创建一个名为
example
的新目录并导航到该目录。 -
创建一个
main.go
文件,内容如下:
package main
import (
"fmt"
"log"
"net/http"
_ "net/http/pprof"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter14/pprof/crypto"
)
func main() {
http.HandleFunc("/guess", crypto.GuessHandler)
fmt.Println("server started at localhost:8080")
log.Panic(http.ListenAndServe("localhost:8080", nil))
}
-
运行
go run main.go
。 -
您还可以运行以下命令:
$ go build $ ./example
现在您应该看到以下输出:
$ go run main.go
server started at localhost:8080
- 在一个单独的终端中,运行以下命令:
$ go tool pprof http://localhost:8080/debug/pprof/profile
-
这将启动一个 30 秒的计时器。
-
在
pprof
运行时运行几个curl
命令:
$ curl "http://localhost:8080/guess?message=test"
try again
$curl "http://localhost:8080/guess?message=password"
you got it
.
.
.
.
$curl "http://localhost:8080/guess?message=password"
you got it
-
返回到
pprof
命令并等待其完成。 -
从
pprof
提示符中运行top10
命令:
(pprof) top 10
930ms of 930ms total ( 100%)
Showing top 10 nodes out of 15 (cum >= 930ms)
flat flat% sum% cum cum%
870ms 93.55% 93.55% 870ms 93.55%
golang.org/x/crypto/blowfish.encryptBlock
30ms 3.23% 96.77% 900ms 96.77%
golang.org/x/crypto/blowfish.ExpandKey
30ms 3.23% 100% 30ms 3.23% runtime.memclrNoHeapPointers
0 0% 100% 930ms 100% github.com/agtorre/go-
cookbook/chapter13/pprof/crypto.GuessHandler
0 0% 100% 930ms 100%
golang.org/x/crypto/bcrypt.CompareHashAndPassword
0 0% 100% 30ms 3.23% golang.org/x/crypto/bcrypt.base64Encode
0 0% 100% 930ms 100% golang.org/x/crypto/bcrypt.bcrypt
0 0% 100% 900ms 96.77%
golang.org/x/crypto/bcrypt.expensiveBlowfishSetup
0 0% 100% 930ms 100% net/http.(*ServeMux).ServeHTTP
0 0% 100% 930ms 100% net/http.(*conn).serve
- 如果您安装了 Graphviz 或支持的浏览器,请从
pprof
提示符中运行web
命令。您应该会看到类似这样的东西,右侧有一长串红色框:
-
go.mod
文件可能会更新,go.sum
文件现在应该存在于顶级食谱目录中。 -
如果您已经复制或编写了自己的测试,请返回到上一级目录并运行
go test
。确保所有测试都通过。
它是如何工作的...
pprof
工具提供了关于应用程序的许多运行时信息。使用net/pprof
包通常是最简单的配置方式,只需要在端口上进行监听并导入即可。
在我们的案例中,我们编写了一个处理程序,使用了一个非常计算密集的应用程序(bcrypt
),以便演示在使用pprof
进行分析时它们是如何出现的。这将快速地分离出在应用程序中创建瓶颈的代码块。
我们选择收集一个通用概要,导致pprof
在 30 秒内轮询我们的应用程序端点。然后我们对端点生成流量,以帮助产生结果。当您尝试检查单个处理程序或代码分支时,这可能会有所帮助。
最后,我们查看了在 CPU 利用率方面排名前 10 的函数。还可以使用pprof http://localhost:8080/debug/pprof/heap
命令查看内存/堆管理。pprof
控制台中的web
命令可用于查看 CPU/内存概要的可视化,并有助于突出更活跃的代码。
基准测试和查找瓶颈
使用基准测试来确定代码中的慢部分是另一种方法。基准测试可用于测试函数的平均性能,并且还可以并行运行基准测试。这在比较函数或对特定代码进行微优化时非常有用,特别是要查看在并发使用时函数实现的性能如何。在本示例中,我们将创建两个结构,两者都实现了原子计数器。第一个将使用sync
包,另一个将使用sync/atomic
。然后我们将对这两种解决方案进行基准测试。
如何操作...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter14/bench
的新目录,并导航到该目录。 -
运行此命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter14/bench
您应该会看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter14/bench
- 从
~/projects/go-programming-cookbook-original/chapter14/bench
复制测试,或者将其作为练习编写一些自己的代码!
请注意,复制的测试还包括本示例中稍后编写的基准测试。
- 创建一个名为
lock.go
的文件,内容如下:
package bench
import "sync"
// Counter uses a sync.RWMutex to safely
// modify a value
type Counter struct {
value int64
mu *sync.RWMutex
}
// Add increments the counter
func (c *Counter) Add(amount int64) {
c.mu.Lock()
c.value += amount
c.mu.Unlock()
}
// Read returns the current counter amount
func (c *Counter) Read() int64 {
c.mu.RLock()
defer c.mu.RUnlock()
return c.value
}
- 创建一个名为
atomic.go
的文件,内容如下:
package bench
import "sync/atomic"
// AtomicCounter implements an atmoic lock
// using the atomic package
type AtomicCounter struct {
value int64
}
// Add increments the counter
func (c *AtomicCounter) Add(amount int64) {
atomic.AddInt64(&c.value, amount)
}
// Read returns the current counter amount
func (c *AtomicCounter) Read() int64 {
var result int64
result = atomic.LoadInt64(&c.value)
return result
}
- 创建一个名为
lock_test.go
的文件,内容如下:
package bench
import "testing"
func BenchmarkCounterAdd(b *testing.B) {
c := Counter{0, &sync.RWMutex{}}
for n := 0; n < b.N; n++ {
c.Add(1)
}
}
func BenchmarkCounterRead(b *testing.B) {
c := Counter{0, &sync.RWMutex{}}
for n := 0; n < b.N; n++ {
c.Read()
}
}
func BenchmarkCounterAddRead(b *testing.B) {
c := Counter{0, &sync.RWMutex{}}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
c.Add(1)
c.Read()
}
})
}
- 创建一个名为
atomic_test.go
的文件,内容如下:
package bench
import "testing"
func BenchmarkAtomicCounterAdd(b *testing.B) {
c := AtomicCounter{0}
for n := 0; n < b.N; n++ {
c.Add(1)
}
}
func BenchmarkAtomicCounterRead(b *testing.B) {
c := AtomicCounter{0}
for n := 0; n < b.N; n++ {
c.Read()
}
}
func BenchmarkAtomicCounterAddRead(b *testing.B) {
c := AtomicCounter{0}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
c.Add(1)
c.Read()
}
})
}
- 运行
go test -bench .
命令,您将看到以下输出:
$ go test -bench .
BenchmarkAtomicCounterAdd-4 200000000 8.38 ns/op
BenchmarkAtomicCounterRead-4 1000000000 2.09 ns/op
BenchmarkAtomicCounterAddRead-4 50000000 24.5 ns/op
BenchmarkCounterAdd-4 50000000 34.8 ns/op
BenchmarkCounterRead-4 20000000 66.0 ns/op
BenchmarkCounterAddRead-4 10000000 146 ns/op
PASS
ok github.com/PacktPublishing/Go-Programming-Cookbook-Second-
Edition/chapter14/bench 10.919s
- 如果您已经复制或编写了自己的测试,请返回上一级目录并运行
go test
。确保所有测试都通过。
工作原理...
本示例是比较代码的关键路径的一个示例。例如,有时您的应用程序必须经常执行某些功能,也许是每次调用。在这种情况下,我们编写了一个原子计数器,可以从多个 go 例程中添加或读取值。
第一个解决方案使用RWMutex
和Lock
或RLock
对象进行写入和读取。第二个使用atomic
包,它提供了相同的功能。我们使函数的签名相同,以便可以在稍作修改的情况下重用基准测试,并且两者都可以满足相同的atomic
整数接口。
最后,我们为添加值和读取值编写了标准基准测试。然后,我们编写了一个并行基准测试,调用添加和读取函数。并行基准测试将创建大量的锁争用,因此我们预计会出现减速。也许出乎意料的是,atomic
包明显优于RWMutex
。
内存分配和堆管理
一些应用程序可以从优化中受益很多。例如,考虑路由器,我们将在以后的示例中进行讨论。幸运的是,工具基准测试套件提供了收集许多内存分配以及内存分配大小的标志。调整某些关键代码路径以最小化这两个属性可能会有所帮助。
这个教程将展示编写一个将字符串用空格粘合在一起的函数的两种方法,类似于strings.Join("a", "b", "c")
。一种方法将使用连接,而另一种方法将使用strings
包。然后我们将比较这两种方法之间的性能和内存分配。
如何做...
这些步骤涵盖了编写和运行您的应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter14/tuning
的新目录,并导航到该目录。 -
运行此命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter14/tuning
应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter14/tuning
- 从
~/projects/go-programming-cookbook-original/chapter14/tuning
复制测试,或者将其作为练习编写一些您自己的代码!
请注意,复制的测试还包括稍后在本教程中编写的基准测试。
- 创建一个名为
concat.go
的文件,其中包含以下内容:
package tuning
func concat(vals ...string) string {
finalVal := ""
for i := 0; i < len(vals); i++ {
finalVal += vals[i]
if i != len(vals)-1 {
finalVal += " "
}
}
return finalVal
}
- 创建一个名为
join.go
的文件,其中包含以下内容:
package tuning
import "strings"
func join(vals ...string) string {
c := strings.Join(vals, " ")
return c
}
- 创建一个名为
concat_test.go
的文件,其中包含以下内容:
package tuning
import "testing"
func Benchmark_concat(b *testing.B) {
b.Run("one", func(b *testing.B) {
one := []string{"1"}
for i := 0; i < b.N; i++ {
concat(one...)
}
})
b.Run("five", func(b *testing.B) {
five := []string{"1", "2", "3", "4", "5"}
for i := 0; i < b.N; i++ {
concat(five...)
}
})
b.Run("ten", func(b *testing.B) {
ten := []string{"1", "2", "3", "4", "5",
"6", "7", "8", "9", "10"}
for i := 0; i < b.N; i++ {
concat(ten...)
}
})
}
- 创建一个名为
join_test.go
的文件,其中包含以下内容:
package tuning
import "testing"
func Benchmark_join(b *testing.B) {
b.Run("one", func(b *testing.B) {
one := []string{"1"}
for i := 0; i < b.N; i++ {
join(one...)
}
})
b.Run("five", func(b *testing.B) {
five := []string{"1", "2", "3", "4", "5"}
for i := 0; i < b.N; i++ {
join(five...)
}
})
b.Run("ten", func(b *testing.B) {
ten := []string{"1", "2", "3", "4", "5",
"6", "7", "8", "9", "10"}
for i := 0; i < b.N; i++ {
join(ten...)
}
})
}
- 运行
GOMAXPROCS=1 go test -bench=. -benchmem -benchtime=1s
命令,您将看到以下输出:
$ GOMAXPROCS=1 go test -bench=. -benchmem -benchtime=1s
Benchmark_concat/one 100000000 13.6 ns/op 0 B/op 0 allocs/op
Benchmark_concat/five 5000000 386 ns/op 48 B/op 8 allocs/op
Benchmark_concat/ten 2000000 992 ns/op 256 B/op 18 allocs/op
Benchmark_join/one 200000000 6.30 ns/op 0 B/op 0 allocs/op
Benchmark_join/five 10000000 124 ns/op 32 B/op 2 allocs/op
Benchmark_join/ten 10000000 183 ns/op 64 B/op 2 allocs/op
PASS
ok github.com/PacktPublishing/Go-Programming-Cookbook-Second-
Edition/chapter14/tuning 12.003s
- 如果您已经复制或编写了自己的测试,请运行
go test
。确保所有测试都通过。
工作原理...
基准测试有助于调整应用程序并进行某些微优化,例如内存分配。在对带有输入的应用程序进行分配基准测试时,重要的是要尝试各种输入大小,以确定它是否会影响分配。我们编写了两个函数,concat
和join
。两者都将variadic
字符串参数与空格连接在一起,因此参数(a,b,c)将返回字符串a b c。
concat
方法仅通过字符串连接实现这一点。我们创建一个字符串,并在列表中和for
循环中添加字符串和空格。我们在最后一个循环中省略添加空格。join
函数使用内部的Strings.Join
函数来更有效地完成这个任务。与您自己的函数相比,对标准库进行基准测试有助于更好地理解性能、简单性和功能性之间的权衡。
我们使用子基准测试来测试所有参数,这也与表驱动基准测试非常搭配。我们可以看到concat
方法在单个长度输入的情况下比join
方法产生了更多的分配。一个很好的练习是尝试使用可变长度的输入字符串以及一些参数来进行测试。
使用 fasthttprouter 和 fasthttp
尽管 Go 标准库提供了运行 HTTP 服务器所需的一切,但有时您需要进一步优化诸如路由和请求时间等内容。本教程将探讨一个加速请求处理的库,称为fasthttp
(github.com/valyala/fasthttp
),以及一个显著加速路由性能的路由器,称为fasthttprouter
(github.com/buaazp/fasthttprouter
)。尽管fasthttp
很快,但重要的是要注意它不支持 HTTP/2(github.com/valyala/fasthttp/issues/45
)。
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter14/fastweb
的新目录,并导航到该目录。 -
运行此命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter14/fastweb
应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter14/fastweb
-
从
~/projects/go-programming-cookbook-original/chapter14/fastweb
复制测试,或者将其作为练习编写一些您自己的代码! -
创建一个名为
items.go
的文件,其中包含以下内容:
package main
import (
"sync"
)
var items []string
var mu *sync.RWMutex
func init() {
mu = &sync.RWMutex{}
}
// AddItem adds an item to our list
// in a thread-safe way
func AddItem(item string) {
mu.Lock()
items = append(items, item)
mu.Unlock()
}
// ReadItems returns our list of items
// in a thread-safe way
func ReadItems() []string {
mu.RLock()
defer mu.RUnlock()
return items
}
- 创建一个名为
handlers.go
的文件,其中包含以下内容:
package main
import (
"encoding/json"
"github.com/valyala/fasthttp"
)
// GetItems will return our items object
func GetItems(ctx *fasthttp.RequestCtx) {
enc := json.NewEncoder(ctx)
items := ReadItems()
enc.Encode(&items)
ctx.SetStatusCode(fasthttp.StatusOK)
}
// AddItems modifies our array
func AddItems(ctx *fasthttp.RequestCtx) {
item, ok := ctx.UserValue("item").(string)
if !ok {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
return
}
AddItem(item)
ctx.SetStatusCode(fasthttp.StatusOK)
}
- 创建一个名为
main.go
的文件,其中包含以下内容:
package main
import (
"fmt"
"log"
"github.com/buaazp/fasthttprouter"
"github.com/valyala/fasthttp"
)
func main() {
router := fasthttprouter.New()
router.GET("/item", GetItems)
router.POST("/item/:item", AddItems)
fmt.Println("server starting on localhost:8080")
log.Fatal(fasthttp.ListenAndServe("localhost:8080",
router.Handler))
}
-
运行
go build
命令。 -
运行
./fastweb
命令:
$ ./fastweb
server starting on localhost:8080
- 从单独的终端,使用一些
curl
命令进行测试:
$ curl "http://localhost:8080/item/hi" -X POST
$ curl "http://localhost:8080/item/how" -X POST
$ curl "http://localhost:8080/item/are" -X POST
$ curl "http://localhost:8080/item/you" -X POST
$ curl "http://localhost:8080/item" -X GET
["hi","how", "are", "you"]
-
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶级配方目录中。 -
如果您已经复制或编写了自己的测试,请运行
go test
。确保所有测试都通过。
它是如何工作的...
fasthttp
和fasthttprouter
包可以大大加快 Web 请求的生命周期。这两个包在热代码路径上进行了大量优化,但不幸的是,需要重写处理程序以使用新的上下文对象,而不是传统的请求和响应写入器。
有许多框架采用了类似的路由方法,有些直接集成了fasthttp
。这些项目在它们的README
文件中保持最新的信息。
我们的配方实现了一个简单的list
对象,我们可以通过一个端点进行附加,然后由另一个端点返回。这个配方的主要目的是演示如何处理参数,设置一个现在明确定义支持的方法的路由器,而不是通用的Handle
和HandleFunc
,并展示它们与标准处理程序有多么相似,但又有许多其他好处。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
2022-05-04 Impatient JavaScript 中文版校对活动期待大家的参与
2022-05-04 ApacheCN 翻译/校对活动进度公告 2022.5.4
2022-05-04 非安全系列教程 NPM、PYPI、DockerHub 备份
2022-05-04 UIUC CS241 系统编程中文讲义校对活动 | ApacheCN