Go-秘籍-全-

Go 秘籍(全)

原文:zh.annas-archive.org/md5/469ed73cb7319377ae1a92346d2fa65c

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

感谢您选择这本书!希望它将成为开发者快速查找 Go 开发模式的便捷参考。本书旨在成为其他资源的伴侣和参考,希望阅读一次后仍能长期有用。本书中的每个配方都包括工作、简单且经过测试的代码,可以作为参考或您自己应用的基石。本书涵盖了从基础到高级主题的广泛内容。

本书涵盖的内容

第一章,I/O 和文件系统,涵盖了常见的 Go I/O 接口并探讨了与文件系统一起工作。这包括临时文件、模板和 CSV 文件。

第二章,命令行工具,探讨了通过命令行接收用户输入并探索处理常见数据类型(如 TOML、YAML 和 JSON)。

第三章,数据转换和组合,演示了在 Go 接口和数据类型之间进行类型转换的方法。它还展示了编码策略和一些 Go 的功能设计模式。

第四章,Go 中的错误处理,展示了在 Go 中处理错误的策略。它探讨了如何传递错误、处理它们以及记录它们。

第五章,关于数据库和存储的一切,处理了访问数据存储系统(如 MySQL)的各种存储库。它还演示了使用接口将库与应用程序逻辑解耦的方法。

第六章,Web 客户端和 API,实现了 Go HTTP 客户端接口、REST 客户端、OAuth2 客户端、装饰和扩展客户端以及 gRPC。

第七章,Go 应用中的微服务,探讨了 Web 处理器、向处理器传递状态、用户输入验证和中间件。

第八章,测试,专注于模拟、测试覆盖率、模糊测试、行为测试以及有用的测试工具。

第九章,并行和并发,提供了关于通道和异步操作、原子值、Go 上下文对象以及通道状态管理的参考。

第十章,分布式系统,实现了服务发现、Docker 容器化、指标和监控以及编排。它主要涉及 Go 应用的部署和产业化。

第十一章,响应式编程和数据流,探讨了响应式和数据流应用程序、Kafka 和分布式消息队列以及 GraphQL 服务器。

第十二章,无服务器编程涉及在不维护服务器的情况下部署 Go 应用程序。这包括使用 Google App Engine、Firebase、Lambda 以及在这些无服务器环境中的登录。

第十三章,性能改进、小贴士和技巧是最后一章,涉及基准测试、识别瓶颈、优化以及提高 Go 应用程序的 HTTP 性能。

您需要为本书准备以下内容

要使用本书,您需要以下内容:

  • Unix 编程环境

  • Go 1.x 系列的最新版本

  • 互联网连接

  • 允许安装每章中描述的附加包

本书面向对象

本书面向网页开发者、程序员和企业开发者。假设读者具备 Go 语言的基本知识。虽然不需要有后端应用程序开发的经验,但这可能有助于理解某些菜谱背后的动机。

本书是针对已经熟练但需要快速提醒、示例或参考的 Go 开发者的一本好参考书。有了开源仓库,应该能够快速将这些示例与团队分享。

章节

在本书中,您会发现几个频繁出现的标题(准备就绪、如何操作…、工作原理…、还有更多…、以及另请参阅)。为了清楚地说明如何完成菜谱,我们使用以下这些部分:

准备就绪

本节告诉您在菜谱中可以期待什么,并描述如何设置任何软件或任何为菜谱所需的初步设置。

如何操作…

本节包含遵循菜谱所需的步骤。

工作原理…

本节通常包含对前节发生事件的详细解释。

还有更多…

本节包含有关菜谱的附加信息,以便使读者对菜谱有更深入的了解。

另请参阅

本节提供了对其他有用信息的菜谱链接。

惯例

在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“Copy()函数在接口之间进行复制,并将它们视为流。”

代码块设置如下:

package main

import "github.com/agtorre/go-cookbook/chapter1/tempfiles"

func main() {
        if err := tempfiles.WorkWithTemp(); err != nil {
                panic(err)
        }
}

任何命令行输入或输出都如下所示:

$ go run main.go 
/var/folders/kd/ygq5l_0d1xq1lzk_c7htft900000gn/T
/tmp764135258/tmp588787953

新术语重要词汇以粗体显示。

警告或重要注意事项以如下框中的形式出现。

小贴士和技巧看起来像这样。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。要发送一般反馈,请简单地发送电子邮件至 feedback@packtpub.com,并在邮件主题中提及书籍的标题。如果您在某个主题领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在你已经是 Packt 书籍的骄傲拥有者,我们有许多事情可以帮助你从购买中获得最大收益。

下载示例代码

您可以从您的账户中下载此书的示例代码文件www.packtpub.com。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。您可以通过以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的“支持”标签上。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入书籍名称。

  5. 选择您想要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买此书籍的来源。

  7. 点击“代码下载”。

你也可以通过点击 Packt 出版网站书籍网页上的“代码文件”按钮来下载代码文件。您可以通过在搜索框中输入书籍名称来访问此页面。请注意,您需要登录您的 Packt 账户。一旦文件下载完成,请确保您使用最新版本的以下软件解压或提取文件夹:

  • WinRAR / 7-Zip for Windows

  • Zipeg / iZip / UnRarX for Mac

  • 7-Zip / PeaZip for Linux

该书的代码包也托管在 GitHub 上github.com/PacktPublishing/Go-Cookbook。我们还有来自我们丰富的书籍和视频目录的其他代码包可供选择,可在github.com/PacktPublishing/找到。查看它们吧!

勘误

尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。

盗版

在互联网上侵犯版权材料是一个跨所有媒体的持续问题。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现我们作品的任何非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。请通过发送链接到疑似盗版材料的方式与我们联系,邮箱地址为copyright@packtpub.com。我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。

问题

如果您对本书的任何方面有问题,您可以通过发送邮件到questions@packtpub.com与我们联系,我们将尽力解决问题。

第一章:I/O 和文件系统

在本章中,将涵盖以下食谱:

  • 使用常见的 I/O 接口

  • 使用 bytes 和 strings 包

  • 使用目录和文件工作

  • 使用 CSV 格式工作

  • 使用临时文件工作

  • 使用 text/template 和 HTML/templates 工作与文本

简介

Go 为基本和复杂的 I/O 提供了出色的支持。本章中的食谱将探索常见的 Go 接口以处理 I/O,并展示如何使用它们。Go 标准库经常使用这些接口,这些接口将在本书的食谱中广泛使用。

您将学习如何在内存和流的形式中处理数据。您将看到处理文件和目录的示例,以及处理 CSV 格式的示例。临时文件食谱讨论了一种无需处理名称冲突等开销即可处理文件的方法。最后,我们将探索 Go 标准模板,包括纯文本和 HTML。

这些食谱应该为使用接口表示和修改数据奠定基础,并帮助您以抽象和灵活的方式思考数据。

使用常见的 I/O 接口

Go 提供了标准库中使用的多个 I/O 接口。尽可能使用这些接口,而不是直接传递结构体或其他类型,这是一个最佳实践。在本食谱中,我们将探索两个强大的接口:io.Readerio.Writer 接口。这些接口在标准库中广泛使用,了解如何使用它们将使您成为更好的 Go 开发者。

ReaderWriter 接口看起来像这样:

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)

本书剩余部分将使用这些接口。

准备工作

根据以下步骤配置您的环境:

  1. 在您的操作系统上下载并安装 Go,并在 golang.org/doc/install 配置 GOPATH 环境变量。

  2. 打开终端/控制台应用程序,导航到您的 GOPATH/src 目录,并创建一个项目目录,例如 $GOPATH/src/github.com/yourusername/customrepo

所有代码都将从这个目录运行和修改。

  1. 可选地,使用以下命令安装最新测试版本的代码:
 go get github.com/agtorre/go-cookbook/

如何做到...

这些步骤涵盖了编写和运行您的应用程序:

  1. 在您的终端/控制台应用程序中,创建一个名为 chapter1/interfaces 的新目录。

  2. 导航到该目录。

github.com/agtorre/go-cookbook/tree/master/chapter1/interfaces 复制测试,或者将其作为练习编写一些您自己的代码。

  1. 创建一个名为 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
        }

  1. 创建一个名为 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("testn"))
                    w.Close()
                }()

                if _, err := io.Copy(os.Stdout, r); err != nil {
                    return err
                }
                return nil
        }

  1. 创建一个名为 example 的新目录。

  2. 导航到 example

  3. 创建一个 main.go 文件,并包含以下内容,并确保你修改导入的接口以使用步骤 2 中设置的路径:

        package main

        import (
             "bytes"
             "fmt"

             "github.com/agtorre/go-cookbook/chapter1/interfaces"
        )

        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)
                }
        }

  1. 运行 go run main.go

  2. 你也可以运行这些:

 go build      ./example

你应该看到以下输出:

 $ go run main.go
 stdout on Copy = exampleexample
 out bytes buffer = exampleexample
 stdout on PipeExample = test

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test,并确保所有测试通过。

它是如何工作的...

Copy() 函数在接口之间进行复制,并将它们视为流。将数据视为流有许多实际用途,尤其是在处理网络流量或文件系统时。Copy() 函数还创建了一个多写入器,它结合了两个写入流,并使用 ReadSeeker 对它们进行两次写入。如果你使用 Reader 接口而不是看到 exampleexample,那么即使复制到 MultiWriter 接口两次,你也只会看到 example。还有一个缓冲写入的示例,如果你的流不适合内存,你可能需要使用它。

PipeReaderPipeWriter 结构体实现了 io.Readerio.Writer 接口。它们是连接的,创建了一个内存管道。管道的主要目的是从流中读取,同时从同一流向不同的源写入。本质上,它将两个流合并成一个管道。

Go 接口是一个干净的抽象,用于封装执行常见操作的数据。这在进行 I/O 操作时变得明显,因此 io 包是学习接口组合的绝佳资源。pipe 包通常未被充分利用,但在连接输入和输出流时提供了极大的灵活性,并且具有线程安全性。

使用 bytesstrings

bytesstring 包提供了一些有用的辅助函数,用于处理字符串和字节类型之间的转换。它们允许创建与许多通用 I/O 接口一起工作的缓冲区。

准备工作

请参考 准备工作 部分的步骤,在 使用通用 I/O 接口 菜谱

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 在你的终端/控制台应用程序中,创建一个名为 chapter1/bytestrings 的新目录。

  2. 导航到该目录。

  3. github.com/agtorre/go-cookbook/tree/master/chapter1/bytesstrings 复制测试,或者将其用作练习来编写你自己的代码!

  4. 创建一个名为 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 intial 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
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 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)
        }

  1. 创建一个名为 example 的新目录。

  2. 导航到 example

  3. 创建一个 main.go 文件,并包含以下内容,并确保你修改导入的接口以使用步骤 2 中设置的路径:

        package main

        import "github.com/agtorre/go-cookbook/chapter1/bytestrings"

        func main() {
                err := bytestrings.WorkWithBuffer()
                if err != nil {
                        panic(err)
                }

                // each of these print to stdout
                bytestrings.SearchString()
                bytestrings.ModifyString()
                bytestrings.StringReader() 
        }

  1. 运行 go run main.go

  2. 你也可以运行这些:

 go build ./example

你应该看到以下输出:

 $ go run main.go
 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

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test,并确保所有测试通过。

它是如何工作的...

当与数据一起工作时,字节库提供了一些方便的函数。例如,一个缓冲区在处理流处理库或方法时比字节数组更灵活。一旦创建了缓冲区,它就可以用来满足 io.Reader 接口,这样您就可以利用 ioutil 函数来操作数据。对于流式应用程序,您可能希望使用缓冲区和扫描器。bufio 包在这些情况下非常有用。有时,使用数组或切片对于较小的数据集或当您的机器上有大量内存时更为合适。

Go 提供了在基本类型之间进行接口转换的很多灵活性--在字符串和字节之间进行转换相对简单。当与字符串一起工作时,strings 包提供了一些方便的函数来处理、搜索和操作字符串。在某些情况下,一个好的正则表达式可能是合适的,但大多数时候,stringsstrconv 包就足够了。strings 包允许您使字符串看起来像标题,将其分割成数组,或删除空白。它还提供了一个自己的 Reader 接口,可以用作 bytes 包读取器类型的替代。

与目录和文件一起工作

在您在平台之间切换时(例如 Windows 和 Linux),与目录和文件一起工作可能会很困难。Go 提供了跨平台支持,在 osioutils 包中处理文件和目录。我们已经看到了 ioutils 的例子,但现在我们将探讨如何以另一种方式使用它们!

准备就绪

参考在 准备就绪 部分的步骤,在 使用通用 I/O 接口 菜谱中。

如何操作...

这些步骤涵盖了编写和运行您的应用程序:

  1. 在您的终端/控制台应用程序中,创建一个名为 chapter1/filedirs 的新目录。

  2. 导航到该目录。

  3. github.com/agtorre/go-cookbook/tree/master/chapter1/filedirs 复制测试,或者将其作为练习编写一些自己的代码!

  4. 创建一个名为 dirs.go 的文件,内容如下:

        package filedirs

        import (
                "errors"
                "io"
                "os"
        )

        // Operate manipulates files and directories
        func Operate() error {
                // this 0777 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
        }

  1. 创建一个名为 bytes.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, 0); 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
        }

  1. 创建一个名为 example 的新目录。

  2. 导航到 example

  3. 创建一个包含以下内容的 main.go 文件,并确保您修改 filedirs 包导入以使用步骤 2 中设置的路径:

        package main

        import "github.com/agtorre/go-cookbook/chapter1/filedirs"

        func main() {
                if err := filedirs.Operate(); err != nil {
                        panic(err)
                }

                if err := filedirs.CapitalizerExample(); err != nil {
                        panic(err)
                }
        }

  1. 运行 go run main.go

  2. 您还可以运行以下命令:

 go build ./example

您应该看到以下输出:

 $ go run main.go 
 hello

  1. 如果您复制或编写了自己的测试,请向上移动一个目录并运行 go test,并确保所有测试都通过。

它是如何工作的...

如果你熟悉 Unix 中的文件,Go 的 os 库应该非常熟悉。你可以基本上执行所有常见的操作--获取文件属性、收集具有不同权限的文件、创建和修改目录和文件。我们执行了一系列对目录和文件的操纵,并在之后进行了清理。

与文件对象一起工作非常类似于内存流。文件还直接提供了一些便利函数,例如 ChownStatTruncate。最简单的方法是利用文件。在所有之前的菜谱中,我们必须小心清理我们的程序。

在构建后端应用程序时,与文件一起工作是件非常常见的事情。文件可用于配置、密钥、临时存储等。Go 使用 os 包封装 OS 系统调用,并允许相同的函数在 Windows 或 Unix 上运行。

一旦你的文件被打开并存储在 File 结构体中,它就可以很容易地传递到之前讨论的许多接口中。所有之前处理缓冲区和内存数据流的示例都可以直接替换为文件对象。这可能对将所有日志同时写入 stderr 和文件的情况很有用。

使用 CSV 格式

CSV 是一种常见的格式来操作数据。例如,导入或导出 CSV 文件到 Excel 是很常见的。Go 的 CSV 包在数据接口上操作,因此将数据写入缓冲区、stdout、文件或套接字变得很容易。本节中的示例将展示一些将数据输入和输出到 CSV 格式的常见方法。

准备工作

参考在 使用通用 I/O 接口 菜单中的 准备工作 部分的步骤*。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 在你的终端/控制台应用程序中,创建一个名为 chapter1/csvformat 的新目录。

  2. 导航到该目录。

  3. github.com/agtorre/go-cookbook/tree/master/chapter1/csvformat 复制测试,或者将其作为练习编写一些你自己的代码!

  4. 创建一个名为 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 wanna 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
        }

        // 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
        }

  1. 创建一个名为 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()
        }

        // 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
        }

  1. 创建一个名为 example 的新目录。

  2. 导航到 example

  3. 创建一个名为 main.go 的文件,并确保你将 csvformat 导入修改为步骤 2 中设置的路径:

        package main

        import (
                "fmt"

                "github.com/agtorre/go-cookbook/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())
        }

  1. 运行 go run main.go

  2. 你还可以运行以下命令:

 go build
 ./example

你应该看到以下输出:

 $ go run main.go 
 []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

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test,并确保所有测试都通过。

工作原理...

为了探索读取 CSV 格式,我们首先将我们的数据表示为一个结构体。在 Go 中,将数据格式化为结构体非常有用,因为它使得诸如序列化和编码等操作相对简单。我们的读取示例使用电影作为数据类型。该函数接受一个 io.Reader 接口作为输入,该接口包含我们的 CSV 数据。这可能是一个文件或一个缓冲区。然后我们使用这些数据来创建和填充一个 Movie 结构体,包括将年份转换为整数。我们还向 CSV 解析器添加了选项,使用 ; 作为分隔符,- 作为注释行。

接下来,我们探索相同的概念,但方向相反。小说用标题和作者来表示。我们初始化一个小说数组,然后将特定的小说以 CSV 格式写入一个 io.Writer 接口。同样,这可以是一个文件、stdout 或一个缓冲区。

CSV 包是为什么你想要在 Go 中将数据流视为实现常见接口的绝佳例子。通过简单的单行调整,我们可以轻松更改数据源和目标,并且可以轻松地操作 CSV 数据,而无需使用过多的内存或时间。例如,可以一次从数据流中读取一条记录,并一次以修改后的格式写入另一个单独的流中。这样做不会产生显著的内存或处理器使用。

在以后探索数据管道和工作者池时,你会看到这些想法如何结合,以及如何并行处理这些流。

使用临时文件

我们已经创建并使用文件处理了多个示例。我们也必须手动处理清理、名称冲突等问题。临时文件和目录是处理这些情况更快、更简单的方法。

准备就绪

参考在 使用通用 I/O 接口 菜单中的 准备就绪 部分的步骤*。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 在你的终端/控制台应用程序中,创建一个名为 chapter1/tempfiles 的新目录。

  2. 导航到这个目录。

  3. github.com/agtorre/go-cookbook/tree/master/chapter1/tempfiles 复制测试,或者使用这个练习来编写你自己的代码!

  4. 创建一个名为 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
        }

  1. 创建一个名为 example 的新目录。

  2. 导航到 example

  3. 创建一个 main.go 文件,内容如下,并确保你修改导入的 tempfiles 以使用步骤 2 中设置的路径:

        package main

        import "github.com/agtorre/go-cookbook/chapter1/tempfiles"

        func main() {
                if err := tempfiles.WorkWithTemp(); err != nil {
                        panic(err)
                }
        }

  1. 运行 go run main.go

  2. 你也可以运行以下命令:

 go build ./example

你应该看到(使用不同的路径)以下输出:

 $ go run main.go 
 /var/folders/kd/ygq5l_0d1xq1lzk_c7htft900000gn/T
        /tmp764135258/tmp588787953

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test,并确保所有测试通过。

它是如何工作的...

可以使用 ioutil 包创建和利用临时文件和目录。虽然你仍然必须自己删除文件,但 RemoveAll 是一种约定,它只需一行额外的代码就会为你完成这项工作。

在编写测试时,强烈建议使用临时文件。对于构建工件等其他事情也很有用。Go 的 ioutil 包默认会尝试尊重操作系统偏好,但如果需要,它允许你回退到其他目录。

使用 text/template 和 HTML/templates 进行工作

Go 为模板提供了丰富的支持。嵌套模板、导入函数、表示变量、遍历数据等操作都非常简单。如果你需要比 CSV 写入器更复杂的功能,模板可能是一个很好的解决方案。

模板的另一个应用是用于网站。当我们想要将服务器端数据渲染到客户端时,模板非常适合。最初,Go 模板可能看起来有些令人困惑。本章将探讨与模板一起工作、收集目录中的模板以及处理 HTML 模板。

准备就绪

参考在 使用常见 I/O 接口 菜单中的 准备就绪 部分的步骤*。

如何实现...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建一个名为 chapter1/templates 的新目录。

  2. 导航到这个目录。

  3. github.com/agtorre/go-cookbook/tree/master/chapter1/templates 复制测试,或者将其作为练习编写一些自己的测试!

  4. 创建一个名为 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}}
`        

        // 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
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 example 的新目录。

  2. 导航到 example

  3. 创建一个名为 main.go 的文件,并确保你修改了导入的临时文件以使用步骤 2 中设置的路径:

        package main

        import "github.com/agtorre/go-cookbook/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)
                }
        }

  1. 运行 go run main.go

  2. 你也可以运行这些:

 go build ./example

你应该看到以下输出(路径可能不同):

 $ go run main.go 

 This template demonstrates printing a "variable".

 If condition is set, we'll print this

 Next we'll iterate over an array of strings:

 0: item1

 1: item2

 2: item3

 We can also easily import other functions like strings.Split
 then immediately used the array created as a result:

 0: another_item1

 1: another_item2

 2: another_item3

 Blocks are a way to embed templates into one another

 I'm defined in a second template!

 Template 1! Var1!!
 Template 2! Var2!!
 Template 3! Var3!!
 <h1>Hello! &lt;script&gt;alert('Can you see 
         me?')&lt;/script&gt;</h1>
 example x3Cexample@example.comx3E
 example &lt;example@example.com&gt;
 example+%3Cexample%40example.com%3E

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test,并确保所有测试都通过。

工作原理...

Go 有两个模板包--text/templatehtml/template。这两个包共享功能并提供了各种函数。通常,使用 html/template 来渲染网站,对于其他所有内容则使用 text/html。模板是纯文本,但可以在花括号块中使用变量和函数。

模板包还提供了方便的方法来处理文件。示例在临时目录中创建了一些模板,然后使用一行代码读取它们。

html/template 包是 text/template 包的包装器。所有的模板示例都直接使用 html/template 包,无需修改,只需更改导入语句。HTML 模板提供了额外的上下文感知安全性。这防止了诸如 JavaScript 注入等问题。

模板包提供了您期望的现代模板库所具备的功能。组合模板、添加应用程序逻辑以及确保将结果输出到 HTML 和 JavaScript 时的安全性都很简单。

第二章:命令行工具

在本章中,将涵盖以下配方:

  • 使用命令行标志

  • 使用命令行参数

  • 读取和设置环境变量

  • 使用 TOML、YAML 和 JSON 进行配置

  • 使用 Unix 管道进行工作

  • 捕获和处理信号

  • ANSI 着色应用程序

简介

命令行应用程序是处理用户输入和输出的最简单方法之一。本章将专注于基于命令行的交互,如命令行参数、配置和环境变量。它将以一个用于在 Unix 和 Bash for Windows 中着色文本输出的库结束。

通过本章中的配方,你应该能够处理预期的和意外的用户输入。信号配方是用户可能向你的应用程序发送意外信号的例子,而管道配方是相对于标志或命令行参数获取用户输入的一个很好的替代方案。

ANSI 颜色配方可能会提供一些清理用户输出的示例。例如,在日志记录中,根据文本的目的对文本进行着色有时可以使大量文本变得更加清晰。

使用命令行标志

flag 包使得向 Go 应用程序添加命令行标志参数变得简单。它有一些缺点--你往往需要重复大量代码来添加标志的缩写版本,并且它们按字母顺序排列在帮助提示中。有一些第三方库试图解决这些缺点,但本章将专注于标准库版本,而不是那些库。

准备工作

根据以下步骤配置你的环境:

  1. golang.org/doc/install 下载并安装 Go 到你的操作系统上,并配置你的 GOPATH 环境变量:

  2. 打开终端/控制台应用程序,导航到你的 GOPATH/src 并创建一个项目目录,例如,$GOPATH/src/github.com/yourusername/customrepo

所有代码都将从这个目录运行和修改。

  1. 可选地,使用 go get github.com/agtorre/go-cookbook/ 命令安装代码的最新测试版本。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序创建并导航到 chapter2/flags 目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter2/flags 复制测试,或者将其作为练习编写一些你自己的代码!

  3. 创建一个名为 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
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 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())
        }

  1. 在命令行上运行以下命令:
 go build ./flags -h

  1. 尝试这些以及其他一些参数,你应该会看到以下输出:
 $ 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

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test,并确保所有测试通过。

它是如何工作的...

这个配方试图展示 flag 包的大部分常见用法。它展示了自定义变量类型、各种内置变量、缩写标志以及将所有标志写入一个公共结构体。这是第一个需要主函数的配方,因为 flag 的主要用法(flag.Parse())应该从主函数中调用。因此,正常的示例目录被省略了。

此应用程序的示例用法表明,你可以自动获得 -h 来获取包含的标志列表。还有一些其他需要注意的事情,比如没有参数调用的布尔标志,以及标志的顺序并不重要。

flag 包是一种快速为命令行应用程序结构化输入并提供灵活指定预先用户输入的方法,例如设置日志级别或应用程序的详细程度。在命令行参数配方中,我们将探索标志集并在它们之间使用参数进行切换。

使用命令行参数

上一配方中的标志是一种命令行参数。本章将通过构建一个支持嵌套子命令的命令来扩展这些参数的其他用途。这将演示标志集并使用传递给应用程序的位置参数。

与上一个配方一样,这个配方也需要一个主函数来运行。有许多第三方包可以处理复杂的嵌套参数和标志,但我们将研究如何仅使用标准库来完成这项工作。

准备工作

参考在 使用命令行标志 配方中的 准备工作 部分的步骤。

如何做到这一点...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建一个名为 chapter2/cmdargs 的新目录并导航到该目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter2/cmdargs 复制测试,或者将其作为练习来编写你自己的代码!

  3. 创建一个名为 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)
        }

  1. 创建一个名为 main.go 的文件,内容如下:
        package main

        import (
            "fmt"
            "os"
            "strings"
        )

        func main() {
            c := MenuConf{}
            menu := c.SetupMenu()
            menu.Parse(os.Args[1:])

         // 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.Parse(os.Args[3:])
                 }
                 c.Greet(os.Args[2])
             default:
                 fmt.Println("Invalid command")
                 menu.Usage()
                 return
             }
          } else {
             menu.Usage()
             return
          }
        }

  1. 运行 go build

  2. 运行以下命令并尝试一些其他参数组合:

 $./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!

  1. 如果你复制或编写了自己的测试,向上移动一个目录并运行 go test,确保所有测试通过。

它是如何工作的...

标志集可以用来设置独立的预期参数列表、用法字符串等。开发者需要对多个参数进行验证,正确解析命令的正确参数子集,并定义用法字符串。这可能会出错,并且需要大量迭代才能完全正确。

flag包使解析参数变得容易,并包括获取标志数量、参数等便利方法。本菜谱演示了使用包括包级配置、必需的位置参数、多级命令使用以及如何将这些内容拆分为多个文件或包(如果需要)的基本方法来构建复杂的命令行应用程序。

读取和设置环境变量

环境变量是将状态传递给应用程序的另一种方式,除了从文件中读取数据或通过命令行显式传递之外。本菜谱将探讨一些非常基本的环境变量的获取和设置,然后使用高度有用的第三方库github.com/kelseyhightower/envconfig

我们将构建一个可以通过 JSON 或通过环境变量读取配置的应用程序。下一个菜谱将进一步探讨其他格式,包括 TOML 和 YAML。

准备就绪

根据以下步骤配置你的环境:

  1. 请参考准备就绪部分中使用命令行标志菜谱的步骤。

  2. 运行go get github.com/kelseyhightower/envconfig/命令。

  3. 运行go get github.com/pkg/errors/命令。

如何做...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建一个名为chapter2/envvar的新目录,并导航到该目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter2/envvar复制测试,或者将其作为练习编写一些自己的代码!

  3. 创建一个名为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
        }

  1. 创建一个名为example的新目录。

  2. 导航到example

  3. 创建一个名为main.go的文件,内容如下,并确保你修改了envvar导入以使用步骤 1 中设置的路径:

        package main

        import (
            "bytes"
            "fmt"
            "io/ioutil"
            "os"

            "github.com/agtorre/go-cookbook/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)
        }

  1. 运行go run main.go

  2. 你也可以运行以下命令:

 go build ./example

  1. 你应该看到以下输出:
 $ 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"}

  1. 如果你复制或编写了自己的测试,请向上移动一个目录,并运行go test,确保所有测试都通过。

它是如何工作的...

使用os包读取和写入环境变量相当简单。本菜谱使用的第三方库envconfig是一种巧妙的方式来捕获环境变量,并使用结构体标签指定某些要求。

LoadConfig函数是一种灵活的方式,可以从各种来源拉取配置信息,而无需大量开销或过多的额外依赖。将主要配置转换为 JSON 以外的格式或始终使用环境变量都很简单。

此外,请注意错误的使用。在本菜谱的代码中,我们包装了错误,这样我们就可以在不丢失原始错误信息的情况下注释错误。关于这一点,在第四章,Go 中的错误处理中将有更多细节。

使用 TOML、YAML 和 JSON 进行配置

Go 使用第三方库支持许多配置格式。其中三种最受欢迎的数据格式是 TOML、YAML 和 JSON。Go 可以直接支持 JSON,而其他格式则提供了如何对这些格式进行序列化/反序列化或编码/解码数据的线索。这些格式在配置之外还有许多好处,但本章将主要关注将 Go 结构体转换为配置结构体的形式。这个配方将探索使用这些格式的基本输入和输出。

这些格式还提供了一个接口,通过该接口 Go 和用其他语言编写的应用程序可以共享相同的配置。还有许多处理这些格式并简化它们使用的工具。

准备工作

根据以下步骤配置你的环境:

  1. 请参考 Using command-line flags 配方中的 Getting ready 部分的步骤.

  2. 运行 go get github.com/BurntSushi/toml 命令。

  3. 运行 go get github.com/go-yaml/yaml 命令。

如何做到...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建一个名为 chapter2/confformat 的新目录,并导航到该目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter2/confformat 复制测试,或者将其作为练习编写一些你自己的代码!

  3. 创建一个名为 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)
        }

  1. 创建一个名为 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)
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 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
            }

  1. 创建一个名为 example 的新目录。

  2. 导航到 example

  3. 创建一个 main.go 文件,内容如下,并确保你修改 confformat 导入以使用步骤 1 中设置的路径:

        package main

        import "github.com/agtorre/go-cookbook/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)
            }
        }

  1. 运行 go run main.go

  2. 你还可以运行以下命令:

 go build ./example

  1. 你应该看到以下内容:
 $ 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]

  1. 如果你复制或自己编写了测试,请向上移动一个目录并运行 go test。确保所有测试通过。

它是如何工作的...

这个配方给出了使用 TOML、YAML 和 JSON 解析器的示例,既可以向 go 结构体写入原始数据,也可以从其中读取数据并将其转换为相应的格式。就像在 第一章 的配方中一样,我们在 I/O 和文件系统 中看到,快速在 []bytestringbytes.Buffer 和其他 I/O 接口之间切换是多么常见。

encoding/json 包在提供编码、序列化和其他用于处理 JSON 格式的方法方面是最全面的。我们通过 ToFormat 函数将这些抽象出来,因此将多个方法附加到单个结构体上以使用这些类型,这将变得非常简单,该结构体可以快速转换为或从这些类型之一。

本节还使用了结构体标签及其用法。上一章也使用了这些,它们是 Go 中向包和库提供有关如何处理结构体中包含的数据提示的常见方式。

使用 Unix 管道

Unix 管道在将一个程序输出传递给另一个程序的输入时很有用。例如,看看这个:

$ echo "test case" | wc -l
 1

在 Go 应用程序中,管道的左侧可以使用 os.Stdin 读取,并像文件描述符一样工作。为了演示这一点,本配方将从一个管道的左侧获取输入,并返回单词及其出现次数的列表。这些单词将在空白处进行标记化。

准备就绪

请参考 准备就绪 部分的步骤,在 使用命令行标志 配方中.

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建一个名为 chapter2/pipes 的新目录,并导航到该目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter2/pipes 复制测试,或者将此作为练习编写一些你自己的代码!

  3. 创建一个名为 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)
            }
        }

  1. 运行 echo "some string" | go run pipes.go

  2. 你也可以运行这些:

 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

  1. 如果你复制或编写了自己的测试,请进入上一级目录并运行 go test。确保所有测试都通过。

作用原理...

在 Go 中处理管道相当简单,特别是如果你熟悉处理文件。例如,你可以使用 第一章 中的管道配方,I/O 和文件系统,来创建一个 tee 应用程序 (en.wikipedia.org/wiki/Tee_(command)),其中所有通过管道输入的内容都会立即写入 stdout 和文件。

此配方使用扫描器来对 os.Stdin 文件对象的 io.Reader 接口进行标记化。你可以看到在完成所有读取后,你必须检查错误。

捕获和处理信号

信号是用户或操作系统终止运行中的应用程序的有用方式。有时,以比默认行为更优雅的方式处理这些信号是有意义的。Go 提供了一种捕获和处理信号的方法。在本配方中,我们将通过使用 Go 协程的信号处理来探索信号的处理。

准备就绪

请参考 准备就绪 部分的步骤,在 使用命令行标志 配方中。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建一个名为 chapter2/signals 的新目录,并导航到该目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter2/signals 复制测试,或者将此作为练习编写一些你自己的代码!

  3. 创建一个名为 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 blogs until someone writes to done
            <-done
            fmt.Println("Done!")

        }

  1. 运行以下命令:
 go build ./signals

  1. 尝试运行并按下 Ctrl + C,你应该会看到以下内容:
 $./signals
 Press ctrl-c to terminate...
 ^C
 sig received: interrupt
 handling a SIGINT now!
 Done!

  1. 再次尝试运行它,并在另一个终端中确定 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!

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

这个配方使用了通道,这在第九章中进行了更广泛的介绍,并行和并发。这是因为信号。Notify 函数需要一个通道来发送信号通知。kill 命令是测试向应用程序传递信号的好方法。我们通过信号注册我们关心的信号类型。Notify 函数。然后,我们在 Go 协程中设置一个函数来处理传递给该函数的通道上的任何活动。一旦我们收到信号,我们可以按自己的意愿处理它。我们可以终止应用程序,响应消息,并为不同的信号有不同的行为。

我们还使用一个 done 通道来阻塞应用程序,直到接收到信号,否则程序将立即终止。这对于长时间运行的应用程序,如 Web 应用程序来说是不必要的。创建适当的信号处理例程来执行清理工作非常有用,尤其是在有大量 Go 协程持有大量状态的应用程序中。一个优雅关闭的实用示例可能是允许当前处理程序完成它们的 HTTP 请求,而不会在途中终止它们。

一个 ANSI 着色应用程序

在你想要着色的文本部分之前和之后,由各种代码处理 ANSI 终端应用程序的着色。本章将探讨一个基本的着色机制,用于着色文本为红色或普通。对于完整的应用程序,请查看github.com/agtorre/gocolorize,它支持更多颜色和文本类型,并且还实现了fmt.Formatter接口,以便于打印。

准备工作

请参考 准备工作 部分的步骤,在 使用命令行标志 配方中.

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建并导航到 chapter2/ansicolor 目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter2/ansicolor复制测试,或者将其作为练习编写一些自己的代码!

  3. 创建一个名为 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)
        }

  1. 创建一个名为 example 的新目录。

  2. 导航到 example

  3. 创建一个包含以下内容的 main.go 文件,并确保你修改 ansicolor 导入以使用步骤 1 中设置的路径:

        package main

        import (
            "fmt"

            "github.com/agtorre/go-cookbook/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())
        }

  1. 运行 go run main.go

  2. 你还可以运行以下命令:

 go build ./example

  1. 如果您的终端支持 ANSI 着色格式,您应该看到以下带有颜色的输出:
 $ go run main.go
 I'm red!
 Now I'm green!
 Back to normal...

  1. 如果您复制或编写了自己的测试,请向上移动一个目录并运行go test。确保所有测试都通过。

它是如何工作的...

此应用程序使用结构体来维护彩色文本的状态。在这种情况下,它存储文本的颜色和文本的值。当您调用String()方法时,将渲染最终的字符串,该方法将返回彩色文本或纯文本,具体取决于结构体中存储的值。默认情况下,文本将是纯文本。

第三章:数据转换和组合

在本章中,将介绍以下食谱:

  • 转换数据类型和接口转换

  • 使用 math 和 math/big 操作数值数据类型

  • 货币转换和 float64 考虑

  • 使用指针和 SQL NullTypes 进行编码和解码

  • 编码和解码 Go 数据

  • Go 中的结构体标签和基本反射

  • 通过闭包实现集合

简介

理解 Go 的类型系统是 Go 开发所有级别的关键步骤。本章将展示在数据类型之间转换、处理非常大的数字、处理货币、编码和解码的类型(包括 base64 和 gob)以及使用闭包创建自定义集合的示例。

转换数据类型和接口转换

Go 在数据之间的转换通常非常灵活。一个类型可以继承另一个类型,如下所示:

type A int

然后,我们可以始终将类型转换回我们继承的类型,如下所示:

var a A = 1
fmt.Println(int(a))

还有一些方便的函数用于在数字之间进行转换(使用类型转换),在字符串和其他类型之间使用 fmt.Sprintstrconv 进行转换,以及使用反射在接口和类型之间进行转换。这个食谱将探索一些将在整本书中使用的这些基本转换。

准备工作

根据以下步骤配置你的环境:

  1. golang.org/doc/install 下载并安装 Go 到你的操作系统上,并配置你的 GOPATH 环境变量。

  2. 打开终端/控制台应用程序,导航到你的 GOPATH/src 并创建一个项目目录,例如 $GOPATH/src/github.com/yourusername/customrepo

所有代码都将从这个目录运行和修改。

  1. 可选地,使用 go get github.com/agtorre/go-cookbook/ 命令安装代码的最新测试版本。

如何做到这一点...

这些步骤涵盖了编写和运行你的应用程序:

  1. 在你的终端/控制台应用程序中,创建并导航到 chapter3/dataconv 目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter3/dataconv 复制测试或使用此作为练习编写一些你自己的代码。

  3. 创建一个名为 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)
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 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")
            }
        }

  1. 创建一个名为 example 的新目录。

  2. 导航到 example

  3. 创建一个名为 main.go 的文件,内容如下。确保将 dataconv 导入修改为使用步骤 2 中设置的路径:

        package main

        import "github.com/agtorre/go-cookbook/chapter3/dataconv"

        func main() {
            dataconv.ShowConv()
            if err := dataconv.Strconv(); err != nil {
                panic(err)
            }
            dataconv.Interfaces()
        }

  1. 运行 go run main.go

  2. 你也可以运行:

 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

  1. 如果你复制或编写了自己的测试,向上导航一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

本食谱通过使用新的类型包装它们、使用 strconv 包以及使用接口反射来演示类型之间的转换。这些方法允许 Go 开发者快速在多种抽象的 Go 类型之间进行转换。这些方法在编译过程中都会揭示错误,但反射可能更加复杂。如果你错误地反射到一个不受支持的类型,你将导致程序崩溃。根据类型进行切换是一种通用方法,本食谱中也进行了演示。

对于像 math 这样的包,转换变得很重要,因为它们仅使用 float64 进行操作。

使用 math 和 math/big 进行数值数据类型的操作

mathmath/big 包专注于向 Go 语言暴露更复杂的数学运算,例如 PowSqrtCosmath 包本身主要在 float64 上操作,除非函数有其他说明。math/big 包用于表示超过 64 位值的大数。本食谱将展示 math 包的一些基本用法,并演示 math/big 在斐波那契数列中的应用。

准备就绪

参考在 转换数据类型和接口转换 食谱的 准备就绪 部分中给出的步骤。

如何做...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建并导航到 chapter3/math 目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter3/math 复制测试或将其作为练习编写一些自己的代码。

  3. 创建一个名为 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)
        }

  1. 创建一个名为 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 nil
            }

            // 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]
        }

  1. 创建一个名为 example 的新目录。

  2. 导航到 example

  3. 创建一个名为 main.go 的文件,内容如下;确保将 math 导入修改为步骤 2 中设置的路径:

        package main

        import (
            "fmt"

            "github.com/agtorre/go-cookbook/chapter3/math"
        )

        func main() {
            math.Examples()

            for i := 0; i < 10; i++ {
                fmt.Printf("%v ", math.Fib(i))
            }
            fmt.Println()
        }

  1. 运行 go run main.go

  2. 你也可以运行:

 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

  1. 如果你复制或编写了自己的测试,向上移动一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

math 包使得在 Go 中执行复杂的数学运算成为可能。本食谱应与该包结合使用,以执行复杂的浮点运算和根据需要转换类型。值得注意的是,即使使用 float64,某些浮点数仍然可能存在舍入误差,以下食谱展示了处理这些误差的一些技术。

math/big 部分展示了递归的斐波那契数列。如果你修改 main.go 以超过 10 的循环,如果你使用 int64 而不是 big.Int,你将很快溢出。此包还具有将大类型转换为其他类型的一些辅助方法。

货币转换和 float64 考虑事项

处理货币总是一个棘手的过程。可能会诱使你将金钱表示为 float64,但在进行计算时,这可能会导致一些相当棘手(且错误)的舍入误差。因此,最好将金钱视为分,并以 Int64 存储它。

当从表单、命令行或其他来源收集用户输入时,金钱通常以美元形式表示。因此,最好将其视为字符串,并直接将该字符串转换为便士,而不进行浮点数转换。这个食谱将展示如何将货币的字符串表示转换为 int64(便士)以及再次转换回来。

准备就绪

参考转换数据类型和接口转换食谱准备就绪 部分的步骤。

如何做...

这些步骤涵盖了编写和运行你的应用程序:

  1. 在你的终端/控制台应用程序中,创建并导航到 chapter3/currency 目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter3/currency 复制测试或将其作为练习编写一些自己的测试。

  3. 创建一个名为 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]
                if len(r) > 2 {
                    r = r[:2]
                }
            }

            // 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)
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 example 的新目录。

  2. 导航到 example

  3. 创建一个名为 main.go 的文件,并包含以下内容;请确保将 currency 导入修改为你在第 2 步中设置的路径:

        package main

        import (
            "fmt"

            "github.com/agtorre/go-cookbook/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)
        }

  1. 运行 go run main.go

  2. 你也可以运行以下操作:

 go build ./example

你应该看到以下输出:

 $ go run main.go
 User input converted to 1593 pennies
 Added 15 cents, new values is 16.08 dollars

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试都通过。

如何工作...

这个食谱使用了 strconvstrings 包来在美元字符串格式和 int64 的便士之间转换货币。它甚至不需要将值转换为 float64,除了作为验证。

strconv.ParseIntstrconv.FormatInt 函数在将 int64 和字符串相互转换时非常有用。我们还利用了 Go 字符串可以轻松地按需追加和切片的事实。

使用指针和 SQL NullTypes 进行编码和解码

当你在 Go 中编码或解码到对象时,未显式设置的类型将使用其默认值。例如,字符串将默认为空字符串 "",整数将默认为 0。通常情况下,这是可以的,除非 0 对于你的 API 或服务来说意味着消耗用户输入或返回输入。

此外,如果你使用如 json omitempty 这样的结构标签,即使它们是有效的,0 值也会被忽略。另一个例子是 SQL 返回的 Null。对于 Int,哪个值最能代表 Null?这个食谱将探讨一些 Go 开发者处理这个问题的方法。

准备就绪

参考转换数据类型和接口转换食谱准备就绪 部分的步骤。

如何做...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建并导航到 chapter3/nulls 目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter3/nulls 复制测试代码或使用此作为练习编写一些你自己的代码。

  3. 创建一个名为 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
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 example 的新目录。

  2. 导航到 example

  3. 创建一个名为 main.go 的文件,并包含以下内容;确保将 nulls 导入路径修改为你在第 2 步中设置的路径:

        package main

        import (
            "fmt"

            "github.com/agtorre/go-cookbook/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)
            }
        }

  1. 运行 go run main.go

  2. 你也可以运行以下命令:

 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"}

  1. 如果你复制或编写了自己的测试代码,请向上导航一个目录并运行 go test。确保所有测试都通过。

工作原理...

从值转换为指针是表达序列化和反序列化时的空值的一种快速方法。在设置这些值时可能有点不清楚,因为你不能直接将它们赋值给指针 -- *a := 1,但除此之外,这是一种灵活处理它的方法。

本食谱还演示了使用 sql.NullInt64 类型的替代方法。这通常与 SQL 一起使用,如果返回的值不是 Null,则有效,否则设置为 Null。我们添加了 MarshalJSONUnmarshallJSON 方法,以便此类型可以与 JSON 包交互,我们选择使用指针,以便 omitempty 能够按预期继续工作。

Go 数据的编码和解码

Go 除了 JSON、TOML 和 YAML 之外,还提供了一些替代编码类型。这些类型主要用于在 Go 进程之间传输数据,例如使用网络协议和 RPC,或者在某些字符格式受限制的情况下。

本食谱将探讨 gob 格式和 base64 的编码和解码。后续章节将探讨如 GRPC 这样的协议。

准备工作

参考在 转换数据类型和接口转换 食谱的 准备工作 部分中给出的步骤.

如何实现...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建并导航到 chapter3/encoding 目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter3/encoding 复制测试代码或使用此作为练习编写一些你自己的代码。

  3. 创建一个名为 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
        }

  1. 创建一个名为 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)

            // be sure to close
            if err := encoder.Close(); err != nil {
                return err
            }
            if _, err := encoder.Write([]byte("encoding some other 
            data")); 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
        }

  1. 创建一个名为 example 的新目录。

  2. 导航到 example

  3. 创建一个名为 main.go 的文件,并包含以下内容;确保将 encoding 导入路径修改为你在第 2 步中设置的路径:

        package main

        import (
            "github.com/agtorre/go-cookbook/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)
            }
        }

  1. 运行 go run main.go

  2. 你也可以运行以下命令:

 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}

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试通过。

它是如何工作的...

Gob 编码是一种考虑 Go 数据类型构建的流格式。在发送和编码许多连续项时效率最高。对于单个项,其他编码格式,如 JSON,可能更高效且更便携。尽管如此,gob 编码使得对大型复杂结构体的序列化和在单独进程中重建变得简单。尽管这里没有展示,gob 还可以对具有自定义 MarshalBinaryUnmarshalBinary 方法的自定义类型或未导出类型进行操作。

Base64 编码在通过 GET 请求通过 URL 进行通信或生成二进制数据的字符串表示编码时很有用。大多数语言都可以支持这种格式并在另一端解包数据。因此,在 JSON 格式不受支持的情况下,通常会将 JSON 负载编码。

Go 中的结构体标签和基本反射

反射是一个复杂的话题,无法在一个食谱中完全涵盖。然而,反射的一个实际应用是处理结构体标签。在本质上,结构体标签只是键值字符串。你查找键,然后处理值。正如你可以想象的那样,对于像 JSON 序列化和反序列化这样的操作,处理这些值有很多复杂性。

reflect 包旨在查询和理解接口对象。它有一些辅助方法来查看结构体的类型、值、结构体标签等。如果你需要像本章开头那样进行基本的接口转换之外的操作,你应该查看这个包。

准备工作

参考转换数据类型和接口转换食谱准备工作 部分的步骤。

如何做到...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建并导航到 chapter3/tags 目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter3/tags 复制测试或将其用作练习来编写你自己的代码。

  3. 创建一个名为 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
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 example 的新目录。

  2. 导航到 example

  3. 创建一个名为 main.go 的文件,并包含以下内容;确保将 tags 导入修改为你在步骤 2 中设置的路径:

        package main

        import (
            "fmt"

            "github.com/agtorre/go-cookbook/chapter3/tags"
        )

        func main() {

            if err := tags.EmptyStruct(); err != nil {
                panic(err)
            }

            fmt.Println()

            if err := tags.FullStruct(); err != nil {
                panic(err)
            }
        }

  1. 运行 go run main.go

  2. 你也可以运行这个:

 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}

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试通过。

它是如何工作的...

这个配方创建了一种字符串序列化格式,它接受一个结构体,并将所有字符串字段序列化为可解析的格式。这个配方不处理某些边缘情况;特别是,字符串不得包含 :; 字符。以下是其行为摘要:

  1. 如果字段是字符串,它将被序列化/反序列化。

  2. 如果字段不是字符串,它将被忽略。

  3. 如果字段的 struct 标签包含 serialize"key",则键将是返回的序列化/反序列化环境。

  4. 重复项不会被处理。

  5. 如果没有指定结构体标签,则使用字段名。

  6. 如果指定了 serialize -`,即使字段是字符串,该字段也会被忽略。

需要注意的其他事项是,反射在非导出值上并不完全起作用。

通过闭包实现集合

如果你一直在使用函数式或动态编程语言,你可能会觉得 for 循环和 if 语句会产生冗长的代码。用于处理列表的函数式结构,如 mapfilter,可能很有用,并使代码看起来更易读。然而,在 Go 中,这些类型不在标准库中,并且没有泛型或非常复杂的反射和空接口的使用,很难泛化。这个配方将为你提供一些使用 Go 闭包实现集合的基本示例。

准备工作

参考配方 Converting Data Types and Interface Casting 中的 Getting ready 部分给出的步骤.

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建并导航到 chapter3/collections 目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter3/collections 复制测试或将其用作练习编写一些你自己的代码。

  3. 创建一个名为 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
        }

  1. 创建一个名为 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
            }
        }

  1. 创建一个名为 example 的新目录。

  2. 导航到 example

  3. 创建一个名为 main.go 的文件,内容如下;请确保将 collections 导入修改为你在步骤 2 中设置的路径:

        package main

        import (
            "fmt"

            "github.com/agtorre/go-cookbook/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)
        }

  1. 运行 go run main.go

  2. 你也可以运行以下操作:

 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}}

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

Go 中的闭包非常强大。尽管我们的集合函数不是泛型的,但它们相对较小,并且可以很容易地应用于我们的 WorkWith 结构体的各种函数。你可能注意到,我们在任何地方都没有返回错误。这些函数的想法是它们是纯的。除了我们在每次调用后选择覆盖它之外,原始列表没有副作用。

如果你需要将修改层应用于列表或列表的列表结构,这种模式可以帮你节省很多困惑,并使测试变得非常直接。同时,也可以将映射和过滤器链式连接起来,以实现非常表达性的编码风格。

第四章:Go 中的错误处理

在本章中,将涵盖以下配方:

  • 处理错误和错误接口

  • 使用 pkg/errors 包和包装错误

  • 使用日志包并了解何时记录错误

  • 使用 apex 和 logrus 包进行结构化日志记录

  • 使用上下文包进行日志记录

  • 使用包级全局变量

  • 捕获长时间运行进程的恐慌

简介

错误处理对于最基本的 Go 程序也很重要。Go 中的错误实现了 Error 接口,必须在代码的每一层进行处理。Go 错误不像异常那样工作,未处理的错误可能导致巨大的问题。您应该努力在错误发生时处理和考虑错误。

本章还涵盖了日志记录,因为每当实际发生错误时,通常都会进行日志记录。我们还将研究包装错误,以便给定的错误具有适当的上下文,以便调用函数。

处理错误和错误接口

Error 接口是一个非常小且简单的接口:

type Error interface{
  Error() string
}

这个接口很优雅,因为它简单,任何东西都可以满足它。不幸的是,这也为需要根据接收到的错误执行某些操作的包造成了混淆。

在 Go 中创建错误有多种方式,这个配方将探讨创建基本错误、具有分配的值或类型的错误以及使用结构体创建的自定义错误。

准备工作

根据以下步骤配置您的环境:

  1. golang.org/doc/install 在您的操作系统上下载并安装 Go,并配置您的 GOPATH 环境变量。

  2. 打开终端/控制台应用程序。

  3. 导航到您的 GOPATH/src 并创建一个项目目录,例如,$GOPATH/src/github.com/yourusername/customrepo

所有代码都将从这个目录运行和修改。

  1. 可选地,使用 go get github.com/agtorre/go-cookbook/ 命令安装代码的最新测试版本。

如何做...

这些步骤涵盖了编写和运行您的应用程序:

  1. 从您的终端/控制台应用程序中创建 chapter4/basicerrors 目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter4/basicerrors 复制测试或将其用作练习编写一些自己的代码。

  3. 创建一个名为 basicerrors.go 的文件,内容如下:

        package basicerrors

        import (
            "errors"
            "fmt"
        )

        // ErrorTyped is a way to make a package level
        // error to check against. I.e. if err == TypedError
        var ErrorTyped = errors.New("this is a typed 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 = ErrorTyped
            fmt.Println("typed error: ", err)
        }

  1. 创建一个名为 custom.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)
        }

  1. 创建一个名为 example 的新目录并导航到它。

  2. 创建一个 main.go 文件,内容如下。确保您修改 basicerrors 导入以使用步骤 2 中设置的路径:

        package main

        import (
            "fmt"

            "github.com/agtorre/go-cookbook/chapter4/basicerrors"
        )

        func main() {
            basicerrors.BasicErrors()

            err := basicerrors.SomeFunc()
            fmt.Println("custom error: ", err)
        }

  1. 运行 go run main.go

  2. 您还可以运行:

 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

  1. 如果您复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

无论你使用errors.Newfmt.Errorf还是自定义错误,最重要的是你绝对不应该在你的代码中留下未处理的错误。这些定义错误的不同方法提供了很大的灵活性。例如,你可以在你的结构体中添加额外的函数来进一步调查错误,并在调用函数中将接口转换为你的错误类型以获得一些附加功能。

该接口本身非常简单,唯一的要求是你返回一个有效的字符串。对于一些具有一致错误处理但希望与其他应用程序良好协作的高级应用程序,将此与结构体连接起来可能很有用。

使用 pkg/errors 包和错误包装

位于github.com/pkg/errorserrors包是标准 Go errors包的直接替代品。此外,它提供了一些非常实用的功能,用于包装和处理错误。前面菜谱中提到的类型化和声明性错误是一个很好的例子——它们可以用来向错误添加额外信息,但按照标准方式包装它将改变其类型并破坏类型断言:

// this wont work if you wrapped it 
// in a standard way. i.e.
// fmt.Errorf("custom error: %s", err.Error())
if err == Package.ErrorNamed{
  //handle this error in a specific way
}

本菜谱将演示如何使用pkg/errors包在你的代码中对错误添加注释。

准备中

根据以下步骤配置你的环境:

  1. 参考本章中“处理错误和错误接口”菜谱的“准备就绪”部分。

  2. 运行go get github.com/pkg/errors/命令。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建名为chapter4/errwrap的目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter4/errwrap复制测试用例,或者将其作为练习编写你自己的代码。

  3. 创建一个名为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))

        }

  1. 创建一个名为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)
        }

  1. 创建一个名为example的新目录并导航到它。

  2. 创建一个main.go文件,并包含以下内容。确保你修改errwrap导入以使用步骤 2 中设置的路径:

        package main

        import (
            "fmt"

            "github.com/agtorre/go-cookbook/chapter4/errwrap"
        )

        func main() {
            errwrap.Wrap()
            fmt.Println()
            errwrap.Unwrap()
            fmt.Println()
            errwrap.StackTrace()
        }

  1. 运行go run main.go

  2. 你还可以运行以下命令:

 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/agtorre/go-cookbook/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

  1. 如果你复制或编写了自己的测试用例,请向上移动一个目录并运行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 包的使用和一些有用的选项,并展示何时可能需要记录日志。

准备工作

根据以下步骤配置你的环境:

  1. 参考本章中“处理错误和 Error 接口”食谱的“准备工作”部分。

  2. 运行 go get github.com/pkg/errors/ 命令。

如何做...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建 chapter4/log 目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter4/log 复制测试或将其作为练习编写一些自己的代码。

  3. 创建一个名为 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 Fataln to 
            log and crash", true)

            fmt.Println(buf.String())
        }

  1. 创建一个名为 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
            }
        }

  1. 创建一个名为 example 的新目录并导航到它。

  2. 创建一个 main.go 文件,内容如下。确保你修改 log 导入以使用步骤 2 中设置的路径:

        package main

        import (
            "fmt"

            "github.com/agtorre/go-cookbook/chapter4/log"
        )

        func main() {
            fmt.Println("basic logging and modification of logger:")
            log.Log()
            fmt.Println("logging 'handled' errors:")
            log.FinalDestination()
        }

  1. 运行 go run main.go

  2. 你也可以运行:

 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

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

你可以初始化一个日志记录器并使用 log.NewLogger() 将其传递,或者使用 log 包级别的日志记录器来记录消息。本食谱中的日志文件执行前者,而错误执行后者。它还展示了在错误达到最终目的地后何时记录日志可能是有意义的,否则你可能会为同一事件记录多次。

这种方法有几个问题。首先,你可能在一个中间函数中有额外的上下文,例如你想要记录的变量。其次,记录大量变量可能会变得混乱,难以阅读和理解。下一个菜谱将探讨提供变量记录灵活性的结构化日志记录,稍后的菜谱将探讨实现全局包级日志记录器。

使用 apex 和 logrus 包进行结构化日志记录

记录信息的主要原因是检查系统在事件发生或过去发生时所处的状态。当你有大量记录日志的微服务时,基本的日志消息很难整理。

如果你可以将日志转换为它们理解的数据格式,那么有各种各样的第三方包可以用来处理日志。这些包提供索引功能、可搜索性以及更多功能。sirupsen/logrusapex/log 包提供了一种进行结构化日志记录的方法,你可以记录多个字段,这些字段可以被重新格式化以适应这些第三方日志读取器。例如,将日志以 JSON 格式发射以便由各种服务解析是相当简单的。

准备工作

根据以下步骤配置你的环境:

  1. 参考菜谱 Handling errors and the Error interface 中的 Getting ready 部分。

  2. 运行 go get github.com/sirupsen/logrus 命令。

  3. 运行 go get github.com/apex/log 命令。

如何做到这一点...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建 chapter4/structured 目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter4/structured 复制测试或使用这个练习来编写你自己的代码。

  3. 创建一个名为 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!")
        }

  1. 创建一个名为 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")
        }

  1. 创建一个名为 example 的新目录并导航到它。

  2. 创建一个 main.go 文件,内容如下。确保你修改 structured 导入以使用步骤 2 中设置的路径:

        package main

        import (
            "fmt"

            "github.com/agtorre/go-cookbook/chapter4/structured"
        )

        func main() {
            fmt.Println("Logrus:")
            structured.Logrus()

            fmt.Println()
            fmt.Println("Apex:")
            structured.Apex()
        }

  1. 运行 go run main.go

  2. 你也可以运行:

 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

  1. 如果你复制或编写了自己的测试,请向上导航一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

sirupsen/logrusapex/log 包都是优秀的结构化日志记录器。它们都提供了钩子,用于向多个事件发射或向日志条目添加额外字段。例如,使用 logrus 钩子或 apex 自定义处理程序添加行号以及服务名称相对简单。钩子的另一个用途可能包括 traceID 以追踪跨越不同服务的一个请求。

虽然logrus将钩子和格式化器分开,但apex将它们合并。此外,apex还添加了一些便利函数,如WithError,用于添加error字段以及跟踪,这些都在配方中进行了演示。将logrus的钩子适配到apex处理程序也很简单。对于这两种解决方案,将转换为 JSON 格式而不是 ANSI 彩色文本只是一个简单的更改。

使用context包进行日志记录

这个配方将演示在各个函数之间传递日志字段的方法。Go 的pkg/context包是传递额外变量和取消操作在函数之间的一种极好方式。这个配方将探讨使用这种功能在函数之间分配变量以用于日志记录。

这种风格可以适应前面的配方中的logrusapex。我们将使用apex进行这个配方。

准备工作

根据以下步骤配置你的环境:

  1. 参考配方Handling errors and the Error interface中的Getting ready部分。

  2. 运行go get github.com/apex/log命令。

如何做...

这些步骤涵盖了编写和运行你的应用程序:

  1. 在你的终端/控制台应用程序中创建并导航到chapter4/context目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter4/context复制测试或使用这个练习来编写你自己的代码。

  3. 创建一个名为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
        }

  1. 创建一个名为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"})
        }

  1. 创建一个名为example的新目录并导航到它。

  2. 创建一个包含以下内容的main.go文件。确保你修改context导入以使用步骤 2 中设置的路径:

        package main

        import "github.com/agtorre/go-cookbook/chapter4/context"

        func main() {
            context.Initialize()
        }

  1. 运行go run main.go

  2. 你也可以运行以下命令:

 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

  1. 如果你复制或编写了自己的测试,请向上导航一个目录并运行go test。确保所有测试都通过。

它是如何工作的...

context包现在出现在各种包中,包括数据库和 HTTP 包。这个配方将允许你将日志字段附加到上下文中,并用于日志记录目的。想法是,不同的方法可以在上下文传递过程中附加更多字段,然后最终调用点可以执行日志记录和聚合变量。

这个配方模仿了在前面配方中找到的日志包中的WithFieldWithFields方法。这些方法修改上下文中存储的单个值,并提供使用上下文的其它好处:取消、超时和线程安全。

使用包级别的全局变量

在前面的例子中,apexlogrus包都使用了包级别的全局变量。有时,将你的库结构化以支持具有各种方法的 struct 和顶级函数是有用的,这样你就可以直接使用它们而无需传递它们。

这个配方还展示了使用 sync.Once 来确保全局日志器只初始化一次。它也可以通过 Set 方法绕过。这个配方只导出 WithFieldDebug,但可以想象导出附加到 log 对象上的所有方法。

准备工作

按照以下步骤配置你的环境:

  1. 参考本章中“处理错误和 Error 接口”配方中的“准备工作”部分。

  2. 运行 go get github.com/sirupsen/logrus 命令。

如何做...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中创建

    进入 chapter4/global 目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter4/global 复制测试或将其用作练习来编写一些你自己的代码。

  3. 创建一个名为 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 intially
        // 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...)
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 example 的新目录并导航到 example

  2. 创建一个包含以下内容的 main.go 文件。确保你修改 global 导入以使用步骤 2 中设置的路径:

        package main

        import "github.com/agtorre/go-cookbook/chapter4/global"

        func main() {
            if err := global.UseLog(); err != nil {
                panic(err)
            }
        }

  1. 运行 go run main.go.

  2. 你也可以运行:

 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"}

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

这些全局包级对象的常见模式是保持全局未导出,并通过方法仅公开所需的功能。通常,你还可以为希望获得日志对象的项目提供一个返回全局日志器副本的方法。

sync.Once 类型是一种新引入的结构。这个结构,结合 Do 方法,将在代码中只执行一次。我们在初始化代码中使用它,如果 Init 被多次调用,Init 函数将抛出错误。

虽然这个例子使用了日志,你也可以想象在数据库连接、数据流和其他许多用例中这可能是有用的。

捕获长时间运行进程的恐慌

在实现长时间运行的过程时,某些代码路径可能会导致恐慌。这通常与未初始化的映射和指针以及用户输入验证不良时的除以零问题有关。

在这些情况下,程序完全崩溃通常比恐慌本身要糟糕得多,因此捕获和处理恐慌可能是有帮助的。

准备工作

参考本章中“处理错误和 Error 接口”配方中的“准备工作”部分。

如何做...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中创建 chapter4/panic 目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter4/panic 复制测试或将其作为练习来编写你自己的代码。

  3. 创建一个名为 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()
        }

  1. 创建一个名为 example 的新目录,并导航到 example 目录。

  2. 创建一个 main.go 文件,并包含以下内容。确保你修改 panic 导入以使用步骤 2 中设置的路径:

        package main

        import (
            "fmt"

            "github.com/agtorre/go-cookbook/chapter4/panic"
        )

        func main() {
            fmt.Println("before panic")
            panic.Catcher()
            fmt.Println("after panic")
        }

  1. 运行 go run main.go

  2. 你也可以运行:

 go build ./example

你应该看到以下输出:

 $ go run main.go
 before panic
 panic occurred: runtime error: integer divide by zero
 after panic

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

这个示例是一个非常基础的如何捕获恐慌的示例。你可以想象,在更复杂的中间件中,你可以在运行许多嵌套函数之后延迟恢复并捕获它。在恢复过程中,你可以基本上做任何你想做的事情,尽管输出日志是很常见的。

在大多数 Web 应用程序中,当发生恐慌时,捕获恐慌并输出一个 http.InternalServerError 消息是很常见的。

第五章:所有关于数据库和存储

在本章中,将涵盖以下食谱:

  • MySQL 的 database/sql 包

  • 执行数据库事务接口

  • SQL 的连接池、速率限制和超时

  • 与 Redis 一起工作

  • 使用 MongoDB 和 mgo 的 NoSQL

  • 为数据可移植性创建存储接口

简介

Go 应用程序通常需要使用长期存储。这通常以关系型和非关系型数据库、键值存储等形式存在。当与这些存储应用程序一起工作时,将你的操作封装在接口中很有帮助。本章中的食谱将检查各种存储接口,考虑连接池等并行访问,并查看集成新库的一般技巧,这在使用新存储技术时通常是情况。

MySQL 的 database/sql 包

关系型数据库是一些最被理解和常见的数据库选项之一。MySQL 和 Postgres 是最受欢迎的开源关系型数据库。本食谱将演示 database/sql 包,这是一个提供多个关系型数据库钩子的包,并自动处理连接池、连接时长,并提供对多个基本数据库操作的访问。

本包的未来版本将包括对上下文和超时的支持。

准备工作

根据以下步骤配置你的环境:

  1. golang.org/doc/install 下载并安装 Go 到你的操作系统,并配置你的 GOPATH 环境变量。

  2. 打开终端/控制台应用程序,导航到你的 GOPATH/src 并创建一个项目目录,例如

    $GOPATH/src/github.com/你的用户名/customrepo

所有代码都将从这个目录运行和修改。

  1. 可选地,使用 go get github.com/agtorre/go-cookbook/ 命令安装代码的最新测试版本。

  2. 运行 go get github.com/go-sql-driver/mysql 命令。

  3. 使用 dev.mysql.com/doc/mysql-getting-started/en/ 安装和配置 MySQL。

  4. 运行 export MYSQLUSERNAME=<你的 MySQL 用户名> 命令。

  5. 运行 export MYSQLPASSWORD=<你的 MySQL 密码> 命令。

如何做到这一点...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序创建并导航到目录 chapter5/database

  2. github.com/agtorre/go-cookbook/tree/master/chapter5/database 复制测试或将其作为练习编写一些自己的代码。

  3. 创建一个名为 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
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 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) 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()
        }

  1. 创建一个名为 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
        }

  1. 创建并导航到 example 目录。

  2. 创建一个名为 main.go 的文件,内容如下;请确保将 database 导入修改为步骤 2 中设置的路径:

        package main

        import (
            "github.com/agtorre/go-cookbook/chapter5/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)
            }
        }

  1. 运行 go run main.go

  2. 您也可以运行以下命令:

 go build ./example

您应该看到以下输出:

 $ go run main.go
 Results:
 Name: Aaron
 Created: 2017-02-16 19:02:36 +0000 UTC

  1. 如果您复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

代码中的 _ "github.com/go-sql-driver/mysql" 行是如何将各种数据库连接器连接到 database/sql 包的。如果您要连接到 Postgres、SQLite 或其他实现 database/sql 接口的数据库,命令将类似。

连接后,该包设置了一个连接池,这在 Connection pooling, rate limiting, and timeouts for SQL 菜谱中有介绍,您可以直接在连接上执行 SQL,或者创建可以执行 commitrollback 命令的所有连接可以执行的事务对象。

mysql 包在与数据库通信时为 Go 时间对象提供了一些便利支持。此菜谱还从 MYSQLUSERNAMEMYSQLPASSWORD 环境变量中检索用户名和密码。

执行数据库事务接口

当与数据库等服务进行连接时,编写测试可能很困难。这是因为 Go 在运行时模拟或鸭子类型化事物很困难。虽然我建议在处理数据库时使用存储接口,但在这个接口内部模拟数据库事务接口仍然很有用。创建用于数据可移植性的存储接口 菜谱将涵盖存储接口;这个菜谱将专注于包装数据库连接和事务对象的接口。

为了展示此类接口的使用,我们将重写之前菜谱中的创建和查询文件,以使用我们的接口。最终输出将相同,但创建和查询操作都将在一个事务中执行。

准备工作

根据以下步骤配置您的环境:

  1. 参考菜谱 The database/sql package with MySQL 中的 Getting ready 部分的步骤。

  2. 运行 go get https://github.com/agtorre/go-cookbook/tree/master/chapter5/database 命令或使用 The database/sql package with MySQL 菜谱编写自己的命令。

如何操作...

这些步骤涵盖了编写和运行您的应用程序:

  1. 在您的终端/控制台应用程序中,创建并导航到 chapter5/dbinterface 目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter5/dbinterface 复制测试或将其作为练习编写一些自己的代码。

  3. 创建一个名为 transaction.go 的文件,内容如下:

        package database

        import _ "github.com/go-sql-driver/mysql" //we import supported
        libraries for database/sql
        // Exec grabs a new connection
        // creates tables, and later drops them
        // and issues some queries
        func Exec() error {
            db, err := Setup()
            if err != nil {
                return err
            }
            // 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

        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 query.go 的文件,内容如下:
        package dbinterface

        import (
            "fmt"

            "github.com/agtorre/go-cookbook/chapter5/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()
        }

  1. 创建一个名为 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
        }

  1. 导航到 example

  2. 创建一个名为 main.go 的文件,并包含以下内容;确保将 dbinterface 导入路径修改为你在第 2 步中设置的路径:

        package main

        import (
            "github.com/agtorre/go-cookbook/chapter5/database"
            "github.com/agtorre/go-cookbook/chapter5/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(db); err != nil {
             panic(err)
         }
         if err := tx.Commit(); err != nil {
             panic(err)
         }
        }

  1. 运行 go run main.go.

  2. 你也可以运行以下命令:

 go build ./example

你应该看到以下输出:

 $ go run main.go
 Results:
 Name: Aaron
 Created: 2017-02-16 20:00:00 +0000 UTC

  1. 如果你复制或编写了自己的测试,请向上导航一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

这个菜谱与上一个菜谱非常相似,但展示了同时使用事务和创建通用的数据库函数,这些函数可以与 sql.DB 连接和 sql.Transaction 对象一起使用。正如你将在第八章 Testing 中看到的,这些接口也很容易模拟。

SQL 的连接池、速率限制和超时

虽然database/sql包提供了对连接池、速率限制和超时的支持,但通常需要调整默认设置以更好地适应你的数据库配置。当你对微服务进行横向扩展且不希望保持过多的数据库活动连接时,这可能会变得很重要。

准备工作

根据以下步骤配置你的环境:

  1. 参考菜谱 The database/sql package with MySQL准备工作 部分的步骤。

  2. 运行 go get https://github.com/agtorre/go-cookbook/tree/master/chapter5/database 命令,或者使用 The database/sql package with MySQL 菜单编写自己的命令。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 在你的终端/控制台应用程序中,创建并导航到 chapter5/pools 目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter5/pools 复制测试,或者将此作为练习编写一些自己的代码。

  3. 创建一个名为 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
        }

  1. 创建一个名为 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, can := context.WithDeadline(ctx, time.Now())

            // call cancel after we complete
            defer can()

            // our transaction is context aware
            _, err = db.BeginTx(ctx, nil)
            return err
        }

  1. 导航到 example

  2. 创建一个名为 main.go 的文件,并包含以下内容;确保将 pools 导入路径修改为你在第 2 步中设置的路径:

        package main

        import "github.com/agtorre/go-cookbook/chapter5/pools"

        func main() {
            if err := pools.ExecWithTimeout(); err != nil {
                panic(err)
            }
        }

  1. 运行 go run main.go

  2. 你也可以运行以下命令:

 go build ./example

你应该看到以下输出:

 $ go run main.go
 panic: context deadline exceeded

 goroutine 1 [running]:
 main.main()
 /go/src/github.com/agtorre/go-  
      cookbook/chapter5/pools/example/main.go:7 +0x4e
 exit status 2

  1. 如果你复制或编写了自己的测试,请向上导航一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

能够控制连接池的深度非常有用。这将使我们不会过载数据库,但重要的是要考虑在超时上下文中的含义。如果你强制执行固定数量的连接和严格基于上下文的时间超时,就像我们在本菜谱中所做的那样,那么在尝试建立过多连接的过载应用程序中,你将会有请求频繁超时的情况。

这是因为连接将在等待连接变得可用时超时。database/sql 新增的上下文功能使得为整个请求(包括执行查询的步骤)设置共享超时变得更加简单。

与其他食谱一样,使用全局 config 对象传递给 Setup() 函数是有意义的,尽管这个食谱只是使用了环境变量。

使用 Redis

有时你可能需要持久化存储或第三方库和服务提供的附加功能。本食谱将探索 Redis 作为一种非关系型数据存储的形式,并展示一种如 Go 的语言如何与这些服务交互。

由于 Redis 支持使用简单接口进行键值存储,因此它非常适合用于会话存储或具有持续时间的数据。能够指定存储在 Redis 中的数据超时非常宝贵。本食谱将探索从配置到查询再到使用自定义排序的基本 Redis 使用方法。

准备工作

按照以下步骤配置你的环境:

  1. golang.org/doc/install 下载并安装 Go 到你的操作系统,并配置你的 GOPATH 环境变量。

  2. 打开一个终端/控制台应用程序。

  3. 导航到你的 GOPATH/src 并创建一个项目目录,例如 $GOPATH/src/github.com/yourusername/customrepo

所有代码都将从这个目录运行和修改。

  1. 可选地,使用 go get github.com/agtorre/go-cookbook/ 命令安装代码的最新测试版本。

  2. 运行 go get gopkg.in/redis.v5 命令。

  3. 使用 redis.io/topics/quickstart 安装和配置 Redis。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建并导航到 chapter5/redis 目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter5/redis 复制测试或将其作为练习编写一些自己的代码。

  3. 创建一个名为 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
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 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
            }

            if err := conn.LPush("list", 1).Err(); err != nil {
                return err
            }
            if err := conn.LPush("list", 3).Err(); err != nil {
                return err
            }
            if err := conn.LPush("list", 2).Err(); err != nil {
                return err
            }

            res, err := conn.Sort("list", redis.Sort{Order: 
            "ASC"}).Result()
            if err != nil {
                return err
            }
            fmt.Println(res)
            conn.Del("list")
            return nil
        }

  1. 导航到 example

  2. 创建一个名为 main.go 的文件,并包含以下内容;确保将 redis 导入修改为步骤 2 中设置的路径:

        package main

        import "github.com/agtorre/go-cookbook/chapter5/redis"

        func main() {
            if err := redis.Exec(); err != nil {
                panic(err)
            }

            if err := redis.Sort(); err != nil {
                panic(err)
            }
        }

  1. 运行 go run main.go

  2. 你也可以运行以下命令:

 go build ./example

你应该看到以下输出:

 $ go run main.go
 result = value
 [1 2 3]

  1. 如果你复制或编写了自己的测试,请向上导航一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

在 Go 中使用 Redis 与在 MySQL 中使用非常相似,尽管没有标准库,但很多相同的约定都遵循了,比如使用Scan()函数从 Redis 读取数据到 Go 类型。在这种情况下选择最佳库可能会很具挑战性,我建议定期调查可用的库,因为事情可能会迅速变化。

这个菜谱使用redis包来进行基本的设置和获取,执行更复杂的排序函数,以及基本配置。就像database/sql一样,你可以以写入超时、连接池大小等形式设置额外的配置。Redis 本身也提供了很多额外的功能,包括 Redis 集群支持、Zscore 和计数器对象、分布式锁等。

如前一个菜谱中建议的,我建议使用一个config对象,它存储你的 Redis 设置和配置细节,以便于设置和安全。

使用 MongoDB 和 mgo 的 NoSQL

你可能会首先认为 Go 更适合关系型数据库,因为 Go 有结构体,并且 Go 是一种静态类型语言。当使用类似mgo包的东西时,Go 可以几乎任意地存储和检索结构体对象。如果你对对象进行版本控制,你的模式可以适应,并且它可以提供一个非常灵活的开发环境。

一些库在隐藏或提升这些抽象方面做得更好。mgo包是一个很好的例子,它出色地完成了前者。这个菜谱将以类似 Redis 和 MySQL 的方式创建连接,但将存储和检索对象,甚至不需要定义具体的模式。

准备工作

根据以下步骤配置你的环境:

  1. golang.org/doc/install下载并安装 Go 到你的操作系统上,并配置你的GOPATH环境变量。

  2. 打开一个终端/控制台应用程序。

  3. 导航到你的GOPATH/src并创建一个项目目录,例如$GOPATH/src/github.com/yourusername/customrepo

所有代码都将从这个目录运行和修改。

  1. 可选地,使用go get github.com/agtorre/go-cookbook/命令安装代码的最新测试版本。

  2. 运行go get gopkg.in/mgo.v2命令。

  3. 要运行代码,你需要一个连接到 MongoDB 实例的工作数据库连接,本书将不会涉及这部分内容。

  4. 基本设置是docs.mongodb.com/getting-started/shell/

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建并导航到chapter5/mongodb目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter5/mongodb复制测试,或者将其作为练习编写一些你自己的代码。

  3. 创建一个名为config.go的文件,内容如下:

        package mongodb

        import mgo "gopkg.in/mgo.v2"

        // Setup initializes a redis client
        func Setup() (*mgo.Session, error) {
            session, err := mgo.Dial("localhost")
            if err != nil {
                return nil, err
            }
            return session, nil
        }

  1. 创建一个名为exec.go的文件,内容如下:
        package mongodb

        import (
            "fmt"

            "gopkg.in/mgo.v2/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() error {
            db, err := Setup()
            if err != nil {
                return err
            }

            conn := db.DB("gocookbook").C("example")

            // we can inserts many rows at once
            if err := conn.Insert(&State{"Washington", 7062000}, 
            &State{"Oregon", 3970000}); err != nil {
                return err
            }

            var s State
            if err := conn.Find(bson.M{"name": "Washington"}).One(&s); 
            err!= nil {
                return err
            }

            if err := conn.DropCollection(); err != nil {
                return err
            }

            fmt.Printf("State: %#vn", s)
            return nil
        }

  1. 导航到example

  2. 创建一个名为main.go的文件,内容如下;请确保将mongodb导入修改为使用你在步骤 2 中设置的路径:

        package main

        import "github.com/agtorre/go-cookbook/chapter5/mongodb"

        func main() {
            if err := mongodb.Exec(); err != nil {
                panic(err)
            }
        }

  1. 运行go run main.go

  2. 你也可以运行以下命令:

 go build ./example

你应该看到以下输出:

 $ go run main.go
 State: mongodb.State{Name:"Washington", Population:7062000}

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行go test。确保所有测试都通过。

它是如何工作的...

mgo包还提供了连接池,以及许多调整和配置与mongodb数据库连接的方法。此配方的示例相当基础,但它们说明了推理和查询基于文档的数据库是多么容易。该包实现了 BSON 数据类型,并且与它的序列化和反序列化非常类似于处理 JSON。

一致性保证和mongodb的最佳实践超出了本书的范围--但在 Go 语言中使用它是一种乐趣。

创建用于数据可移植性的存储接口

当与外部存储接口一起工作时,将操作抽象化在接口后面可能会有所帮助。这样做是为了方便模拟、在更改存储后端时的可移植性,以及关注点的隔离。这种方法的缺点可能在于,如果你需要在事务内部执行多个操作。在这种情况下,创建组合操作或允许通过上下文对象或额外的函数参数传递它是有意义的。

此配方将实现一个用于与 MongoDB 中的项目交互的非常简单的接口。这些项目将有一个名称和价格,我们将使用接口来持久化和检索这些对象。

准备工作

请参考使用 MongoDB 和 mgo 的 NoSQL配方中“准备工作”部分给出的步骤。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建并导航到chapter5/mongodb目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter5/mongodb复制测试或将其作为练习编写你自己的代码。

  3. 创建一个名为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
        }

  1. 创建一个名为mongoconfig.go的文件,内容如下:
        package storage

        import mgo "gopkg.in/mgo.v2"

        // MongoStorage implements our storage interface
        type MongoStorage struct {
            *mgo.Session
            DB string
            Collection string
        }

        // NewMongoStorage initializes a MongoStorage
        func NewMongoStorage(connection, db, collection string) 
        (*MongoStorage, error) {
            session, err := mgo.Dial("localhost")
            if err != nil {
                return nil, err
            }
            ms := MongoStorage{
                Session: session,
                DB: db,
                Collection: collection,
            }
            return &ms, nil
        }

  1. 创建一个名为mongointerface.go的文件,内容如下:
        package storage

        import (
            "context"

            "gopkg.in/mgo.v2/bson"
        )

        // GetByName queries mongodb for an item with
        // the correct name
        func (m *MongoStorage) GetByName(ctx context.Context, name 
        string) (*Item, error) {
            c := m.Session.DB(m.DB).C(m.Collection)
            var i Item
            if err := c.Find(bson.M{"name": name}).One(&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.Session.DB(m.DB).C(m.Collection)
            return c.Insert(i)
        }

  1. 创建一个名为exec.go的文件,内容如下:
        package storage

        import (
            "context"
            "fmt"
        )

        // Exec initializes storage, then performs operations
        // using the storage interface
        func Exec() error {
            m, err := NewMongoStorage("localhost", "gocookbook", 
            "items")
            if err != nil {
                return err
            }
            if err := PerformOperations(m); err != nil {
                return err
            }

            if err := 
            m.Session.DB(m.DB).C(m.Collection).DropCollection(); 
            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: %#vn", candles)
                return nil
        }

  1. 导航到example

  2. 创建一个名为main.go的文件,内容如下;请确保将storage导入修改为使用你在步骤 2 中设置的路径:

        package main

        import "github.com/agtorre/go-cookbook/chapter5/storage"

        func main() {
            if err := storage.Exec(); err != nil {
                panic(err)
            }
        }

  1. 运行go run main.go

  2. 你也可以运行以下命令:

 go build ./example

你应该看到以下输出:

 $ go run main.go
 Result: &storage.Item{Name:"candles", Price:100}

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行go test。确保所有测试都通过。

它是如何工作的...

展示此菜谱最重要的功能是 PerformOperation。此函数接受存储接口作为参数。这意味着我们可以动态地替换底层的存储,甚至无需修改此函数。例如,将存储连接到单独的 API 以消费和修改它将变得非常简单。

我们为这些接口添加了上下文,以增加额外的灵活性并允许接口处理超时。将您的应用程序逻辑与底层存储分离提供了各种好处,但选择合适的边界位置可能很困难,并且这会因应用程序而异。

第六章:Web 客户端和 API

在本章中,我们将涵盖以下食谱:

  • 初始化、存储和传递 http.Client 结构体

  • 编写 REST API 的客户端

  • 执行并行和异步客户端请求

  • 利用 OAuth2 客户端

  • 实现 OAuth2 令牌存储接口

  • 在客户端添加功能并进行函数组合

  • 理解 GRPC 客户端

简介

与 API 和编写 Web 客户端可能是一个棘手的话题。不同的 API 有不同的授权、认证和协议类型。我们将探索 http.Client 结构体对象,与 OAuth2 客户端和长期令牌存储一起工作,并以额外的 REST 接口结束 GRPC。

到本章结束时,你应该对如何与第三方或内部 API 进行接口交互以及一些常见操作的模式有所了解,例如对 API 的异步请求。

初始化、存储和传递 http.Client 结构体

Go 的 net/http 包提供了一个灵活的 http.Client 结构体,用于与 HTTP API 一起工作。这个结构体有独立的传输功能,相对简单就可以绕过请求,为每个客户端操作修改头信息,并处理任何 REST 操作。创建客户端是一个非常常见的操作,这个食谱将从创建 http.Client 对象的基本操作开始。

准备工作

根据以下步骤配置你的环境:

  1. golang.org/doc/install 下载并安装 Go 到你的操作系统上,并配置你的 GOPATH 环境变量。

  2. 打开一个终端/控制台应用程序。

  3. 导航到 GOPATH/src 并创建一个项目目录。例如,$GOPATH/src/github.com/yourusername/customrepo

所有代码都将从这个目录运行和修改。

  1. 可选地,使用 go get github.com/agtorre/go-cookbook/ 命令安装最新测试版本的代码。

如何做到这一点...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建 chapter6/client 目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter6/client 复制测试或将其作为练习编写一些你自己的代码。

  3. 创建一个名为 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
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 example 的新目录并导航到它。

  2. 创建一个名为 main.go 的文件,内容如下。确保你修改 client 导入以使用步骤 2 中设置的路径:

        package main

        import "github.com/agtorre/go-cookbook/chapter6/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)
            }
        }

  1. 运行 go run main.go

  2. 你也可以运行以下命令:

 go build ./example

你现在应该看到以下输出:

 $ go run main.go
 results of DefaultGetGolang: 200
 results of DoOps: 200
 results of client.DoOps 200
 results of DefaultGetGolang: 418

  1. 如果你复制或编写了自己的测试,请进入上一级目录并运行 go test。确保所有测试都通过。

它是如何工作的...

net/http 包公开了一个 DefaultClient 包变量,该变量用于内部操作,如 DoGETPOST 等。我们的 Setup() 函数返回一个客户端并将默认客户端设置为相同。在设置客户端时,大部分的修改都会发生在传输中,它只需要实现 RoundTripper 接口。

本示例提供了一个始终返回 418 状态码的无操作往返器的示例。你可以想象这可能在测试中很有用。它还展示了如何将客户端作为函数参数传递,将其用作结构参数,以及使用默认客户端处理请求。

为 REST API 编写客户端

为 REST API 编写客户端不仅可以帮助你更好地理解相关的 API,而且为你提供了所有未来使用该 API 的应用程序的有用工具。这将探讨客户端的结构化以及展示一些你可以立即利用的策略。

对于这个客户端,我们假设身份验证由基本身份验证处理,但也可以通过端点检索令牌等。为了简单起见,我们假设我们的 API 公开了一个端点 GetGoogle(),它返回从对 www.google.com 进行 GET 请求返回的状态码。

准备就绪

请参阅 准备就绪 部分,该部分在 初始化、存储和传递 http.Client 结构体 菜谱中。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建 chapter6/rest 目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter6/rest 复制测试或将其作为练习编写一些自己的代码。

  3. 创建一个名为 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
        }

  1. 创建一个名为 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)
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 example 的新目录并导航到它。

  2. 创建一个名为 main.go 的文件,内容如下。确保你修改 rest 导入以使用步骤 2 中设置的路径:

        package main

        import "github.com/agtorre/go-cookbook/chapter6/rest"

        func main() {
            if err := rest.Exec(); err != nil {
                panic(err)
            }
        }

  1. 运行 go run main.go

  2. 你还可以运行以下命令:

 go build ./example

你现在应该看到以下输出:

 $ go run main.go
 Result of GetGoogle: 200

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 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文件中完成。

准备工作

参考本章中初始化、存储和传递 http.Client 结构体配方中的准备工作部分。

如何操作...

这些步骤涵盖了编写和运行您的应用程序:

  1. 在您的终端/控制台应用程序中,创建名为chapter6/async的目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter6/async复制测试或将其作为练习编写一些自己的代码。

  3. 创建一个名为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
        }

  1. 创建一个名为exec.go的文件,内容如下:
        package async

        // FetchAll grabs a list of urls
        func FetchAll(urls []string, c *Client) {
            for _, url := range urls {
                go c.AsyncGet(url)
            }
        }

  1. 创建一个名为example的新目录并导航到它。

  2. 创建一个名为main.go的文件,内容如下。确保您修改client导入以使用步骤 2 中设置的路径:

        package main

        import (
            "fmt"
            "net/http"

            "github.com/agtorre/go-cookbook/chapter6/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)
                }
            }
        }

  1. 运行go run main.go

  2. 您还可以运行以下命令:

 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

  1. 如果您复制或编写了自己的测试,请向上移动一个目录并运行go test。确保所有测试都通过。

工作原理...

此配方创建了一个框架,用于使用单个客户端以扇出异步方式处理请求。它将尽可能快地检索您指定的尽可能多的 URL。在许多情况下,您可能还想进一步限制,例如使用工作池。也可能有在客户端外部处理这些异步 Go 协程的必要,特别是对于特定的存储或检索接口。

此配方还探讨了使用 case 语句在多个通道上进行切换。我们处理了锁定问题,因为我们知道我们将收到多少响应,并且只有在收到所有响应后才会完成。如果我们可以接受丢弃某些响应,另一个选项可能是超时。

使用 OAuth2 客户端

OAuth2 是与 API 通信的相对常见的协议。golang.org/x/oauth2包提供了一个相当灵活的客户端,用于与 OAuth2 一起工作。它有子包,指定了各种提供者的端点,例如 Facebook、Google 和 GitHub。

本教程将演示如何创建一个新的 GitHub OAuth2 客户端及其基本用法。

准备工作

根据以下步骤配置你的环境:

  1. 参考关于初始化、存储和传递 http.Client 结构的准备工作部分。

  2. 运行go get golang.org/x/oauth2命令。

  3. github.com/settings/applications/new配置 OAuth 客户端。

  4. 使用你的客户端 ID 和密钥设置环境变量:

    1. export GITHUB_CLIENT="your_client"

    2. export GITHUB_SECRET="your_secret"

  5. developer.github.com/v3/上复习 GitHub API 文档。

如何做到这一点...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建并导航到chapter6/client目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter6/oauthcli复制测试或将其作为练习编写一些自己的代码。

  3. 创建一个名为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)
        }

  1. 创建一个名为exec.go的文件,内容如下:
        package oauthcli

        import (
            "fmt"
            "net/http"
        )

        // GetUsers uses an initialized oauth2 client to get
        // information about a user
        func GetUsers(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
        }

  1. 创建一个名为example的新目录并进入它。

  2. 创建一个main.go文件,内容如下。确保修改client导入以使用你在步骤 2 中设置的路径:

        package main

        import (
            "context"

            "github.com/agtorre/go-cookbook/chapter6/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.GetUsers(client); err != nil {
                panic(err)
            }

        }

  1. 运行go run main.go

  2. 你也可以运行以下命令:

 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>}

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行go test。确保所有测试都通过。

它是如何工作的...

标准的 OAuth2 流程是基于重定向的,并以服务器将重定向到您指定的端点结束。然后,您的服务器负责抓取代码并将其交换为令牌。本教程通过允许我们使用类似https://localhosthttps://a-domain-you-own的 URL,手动复制/粘贴代码,然后按回车键来绕过这一要求。一旦令牌被交换,客户端将根据需要智能地刷新令牌。

重要的是要注意,我们不会以任何方式存储令牌。如果程序崩溃,它必须重新交换令牌。还需要注意的是,除非刷新令牌过期、丢失或损坏,否则我们只需要明确检索令牌一次。一旦客户端配置完成,它应该能够执行所有典型的 HTTP 操作,这些操作针对它授权的 API,并且具有适当的权限范围。

实现 OAuth2 令牌存储接口

在之前的配方中,我们检索了客户端的令牌并执行了 API 请求。这种方法的一个缺点是我们没有令牌的长期存储。例如,在一个 HTTP 服务器中,我们希望在请求之间保持令牌的一致存储。

此配方将探讨修改 OAuth2 客户端以在请求之间存储令牌并使用密钥随意检索它们。为了简化,这个密钥将是一个文件,但它也可以是数据库、Redis 等。

准备工作

参考配方 Making use of OAuth2 clients 中的 Getting ready 部分。

如何做到这一点...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建并导航到 chapter6/client 目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter6/oauthstore 复制测试或将其作为练习编写一些自己的代码。

  3. 创建一个名为 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))
        }

  1. 创建一个名为 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}
        }

  1. 创建一个名为 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)
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 example 的新目录,并导航到它。

  2. 创建一个名为 main.go 的文件,并包含以下内容。确保你将 oauthstore 导入修改为步骤 2 中设置的路径:

        package main

        import (
            "context"
            "io"
            "os"

            "github.com/agtorre/go-cookbook/chapter6/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)
        }

  1. 运行 go run main.go

  2. 你也可以运行以下命令:

 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>}

  1. 如果你复制或编写了自己的测试,向上导航一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

此配方负责将令牌的内容存储和检索到文件中。如果是首次运行,它必须执行整个代码交换,但后续运行将重用访问令牌,如果可用,它将使用刷新令牌进行刷新。

在此代码中目前没有区分用户/令牌的方法,但可以通过将 cookies 作为文件名或数据库中的一行作为键来实现。让我们看看这段代码是如何工作的:

  • config.go 文件包装了标准的 OAuth2 配置。对于涉及检索令牌的每个方法,我们首先检查本地存储中是否有有效的令牌。如果没有,我们使用标准配置检索一个,然后存储它。

  • tokensource.go 文件实现了我们的自定义 TokenSource 接口,它与 Config 配对。类似于 Config,我们首先尝试从文件中检索我们的令牌,如果没有,则使用新的令牌设置。

  • storage.go 文件是 ConfigTokenSource 使用的 storage 接口。它只定义了两个方法,我们还包含了一个辅助函数来引导 OAuth2 基于代码的流程,类似于我们在之前的配方中所做的,但如果存在包含有效令牌的文件,它将使用该文件。

  • filestorage.go文件实现了storage接口。当我们存储一个新的令牌时,我们首先截断文件并写入token结构的 JSON 表示。否则,我们解码文件并返回token

包装客户端以添加功能并进行函数组合

在 2015 年,Tomás Senart 做了一次关于使用接口包装http.Client结构体的精彩演讲,这使得你可以利用中间件和函数组合。你可以在github.com/gophercon/2015-talks了解更多信息。这个食谱借鉴了他的想法,并演示了如何将类似我们早期食谱“编写 REST API 客户端”的方式应用到http.Client结构的Transport接口上。

这个食谱将实现一个用于标准http.Client结构体的日志记录和基本身份验证中间件。它还包括一个装饰函数,当需要使用大量中间件时可以使用。

准备工作

参考本章中“准备就绪”部分的“初始化、存储和传递 http.Client 结构体”食谱。

如何实现...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建chapter6/decorator目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter6/decorator复制测试代码,或者将其作为练习来编写你自己的代码。

  3. 创建一个名为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
        }

  1. 创建一个名为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
        }

  1. 创建一个名为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
                })
            }
        }

  1. 创建一个名为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
        }

  1. 创建一个名为example的新目录并导航到它。

  2. 创建一个main.go文件,内容如下。确保将decorator导入修改为你在第二步中设置的路径:

        package main

        import "github.com/agtorre/go-cookbook/chapter6/decorator"

        func main() {
            if err := decorator.Exec(); err != nil {
                panic(err)
            }
        }

  1. 运行go run main.go

  2. 你还可以运行以下命令:

 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

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行go test。确保所有测试都通过。

它是如何工作的...

这个食谱利用了闭包作为一等公民和接口。允许这样做的主要技巧是有一个函数实现了接口。这允许我们用函数实现的接口来包装由结构体实现的接口。

middleware.go文件包含两个示例客户端中间件函数。这些可以扩展以包含额外的中间件,例如更复杂的身份验证、度量等。这个食谱也可以与之前的食谱结合,以生成可以由额外中间件扩展的 OAuth2 客户端。

Decorator 函数是一个便利函数,它允许以下操作:

Decorate(RoundTripper, Middleware1, Middleware2, etc)

vs

var t RoundTripper
t = Middleware1(t)
t = Middleware2(t)
etc

与包装客户端相比,这种方法的优势在于我们可以保持接口稀疏。如果你想有一个功能齐全的客户端,你还需要实现 GETPOSTPostForm 等方法。

理解 GRPC 客户端

GRPC 是一个使用协议缓冲区 (developers.google.com/protocol-buffers) 和 HTTP/2 (http2.github.io) 构建的高性能 RPC 框架。在 Go 中创建 GRPC 客户端与使用 Go HTTP 客户端有很多相似之处。为了演示基本的客户端使用,也最容易实现一个服务器。这个示例将创建一个 greeter 服务,它接受一个问候语和一个名字,并返回句子 <greeting> <name>!。此外,服务器可以指定是否要感叹号 ! 或不使用 .

这个示例不会探讨 GRPC 的某些细节,例如流式传输,但希望它能作为创建一个非常基本的客户端和服务器的一个介绍。

准备就绪

根据以下步骤配置你的环境:

  1. 参考本章中 初始化、存储和传递 http.Client 结构体 菜单的 准备就绪 部分。

  2. github.com/grpc/grpc/blob/master/INSTALL.md 安装 GRPC。

  3. 运行 go get github.com/golang/protobuf/proto 命令。

  4. 运行 go get github.com/golang/protobuf/protoc-gen-go 命令。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建并导航到 chapter6/grpc 目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter6/grpc 复制测试或将其作为练习编写一些自己的代码。

  3. 创建一个名为 greeter 的新目录并进入它。

  4. 创建一个名为 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;
        }

  1. 运行以下命令:
 protoc --go_out=plugins=grpc:. greeter.proto

  1. 向上移动一个目录。

  2. 创建一个名为 server 的新目录并进入它。

  3. 创建一个名为 server.go 的文件,内容如下。确保你修改 greeter 导入以使用第 3 步中设置的路径:

        package main

        import (
            "fmt"
            "net"

            "github.com/agtorre/go-cookbook/chapter6/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)
        }

  1. 创建一个名为 greeter.go 的文件,内容如下。确保你修改 greeter 导入以使用第 3 步中设置的路径:
        package main

        import (
            "fmt"

            "github.com/agtorre/go-cookbook/chapter6/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
        }

  1. 向上移动一个目录。

  2. 创建一个名为 client 的新目录并进入它。

  3. 创建一个名为 client.go 的文件,内容如下。确保你修改 greeter 导入以使用第 3 步中设置的路径:

        package main

        import (
            "context"
            "fmt"

            "github.com/agtorre/go-cookbook/chapter6/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)
        }

  1. 向上移动一个目录。

  2. 运行 go run server/server.go server/greeter.go,你将看到以下输出:

 $ go run server/server.go server/greeter.go
 Listening on port :4444

  1. 在另一个终端中,从 grpc 目录运行 go run client/client.go,你将看到以下输出:
 $ go run client/client.go
 response:"Hello Reader!" 
 response:"Goodbye Reader!"

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

GRPC 服务器被设置为监听端口 4444。一旦客户端连接,它就可以向服务器发送请求并接收响应。请求、响应的结构以及支持的方法由我们在第 4 步中创建的 .proto 文件决定。在实际应用中,当与 GRPC 服务器集成时,它们应该提供 .proto 文件,该文件可以用于自动生成客户端。

除了客户端之外,protoc 命令还会为服务器生成存根,所需做的只是填写实现细节。生成的 Go 代码还包含 JSON 标签,相同的结构体可以用于 JSON REST 服务。我们的代码设置了一个不安全的客户端。为了安全地处理 GRPC,你需要使用 SSL 证书。

第七章:Go 应用程序的微服务

本章将涵盖以下内容:

  • 与 Web 处理程序、请求和 ResponseWriter 一起工作

  • 使用结构体和闭包进行有状态处理程序

  • 验证 Go 结构体和用户输入

  • 渲染和内容协商

  • 实现和使用中间件

  • 构建 reverse proxy 应用程序

  • 将 GRPC 导出为 JSON API

简介

默认情况下,Go 是编写 Web 应用的绝佳选择。内置的 net/http 包与 html/template 等包结合使用,可以轻松地构建功能齐全的现代 Web 应用程序。它如此简单,以至于鼓励为甚至基本的长运行应用程序启动 Web 界面。尽管标准库功能齐全,但仍有许多第三方 Web 包,从路由到全栈框架,包括以下内容:

本章中的食谱将侧重于您在处理程序工作时可能遇到的基本任务,当导航响应和请求对象时,以及处理中间件等概念。

与 Web 处理程序、请求和 ResponseWriter 一起工作

Go 定义了 HandlerFuncsHandler 接口,具有以下签名:

// HandlerFunc implements the Handler interface
type HandlerFunc func(http.ResponseWriter, *http.Request)

type Handler interface {
    ServeHTTP(http.ResponseWriter, *http.Request)
}

默认情况下,net/http 包广泛使用这些类型。例如,可以将路由附加到 HandlerHandlerFunc 接口。本食谱将探讨创建 Handler 接口,监听本地端口,并在处理 http.Request 后对 http.ResponseWriter 接口执行一些操作。这应被视为 Go Web 应用程序和 RESTful API 的基础。

准备工作

根据以下步骤配置您的环境:

  1. golang.org/doc/install 在您的操作系统上下载和安装 Go,并配置您的 GOPATH 环境变量。

  2. 打开一个终端/控制台应用程序。

  3. 导航到您的 GOPATH/src 并创建一个项目目录,例如 $GOPATH/src/github.com/yourusername/customrepo

所有代码都将从这个目录运行和修改。

  1. 可选,使用 go get github.com/agtorre/go-cookbook/ 命令安装代码的最新测试版本。

  2. curl.haxx.se/download.html 安装 curl 命令。

如何操作...

这些步骤涵盖了编写和运行应用程序:

  1. 在您的终端/控制台应用程序中,创建并导航到 chapter7/handlers 目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter7/handlers 复制测试或将其作为练习来编写一些自己的代码。

  3. 创建一个名为 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)))
        }

  1. 创建一个名为 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)
                }
            }
            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)
            }
        }

  1. 创建一个名为 example 的新目录并导航到它。

  2. 创建一个名为 main.go 的文件,并包含以下内容;确保将 handlers 导入修改为你在第二步中设置的路径:

        package main

        import (
            "fmt"
            "net/http"

            "github.com/agtorre/go-cookbook/chapter7/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)
        }

  1. 运行 go run main.go.

  2. 你也可以运行以下命令:

 go build ./example

你应该看到以下输出:

 $ go run main.go
 Listening on port :3333

  1. 在另一个终端中,运行以下命令:
 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}

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

对于这个菜谱,我们设置了两个处理器。第一个处理器期望一个带有名为 nameGET 参数的 GET 请求。当我们使用 curl 访问它时,它返回纯文本字符串 Hello <name>!

第二个处理器期望一个带有 PostForm 请求的 POST 方法。如果你使用了一个标准的 HTML 表单而没有任何 AJAX 调用,你将得到这个结果。或者,我们也可以从请求体中解析 JSON。我建议你也尝试这个练习。最后,处理器发送一个 JSON 格式的响应并设置所有适当的头信息。

尽管所有这些都被明确写出,但有许多方法可以使代码更简洁,如下所示:

  • 使用 github.com/unrolled/render 来处理响应

  • 使用本章 与 Web 处理器、请求和 ResponseWriters 协同工作 菜单中提到的各种 Web 框架来解析路由参数,限制路由到特定的 HTTP 动词,处理优雅的关闭,等等

使用结构和闭包进行有状态的处理

由于 HTTP 处理器函数的签名稀疏,可能看起来很难向处理器添加状态。例如,有各种方法来包含数据库连接。实现这一目标的两种方法是通过闭包传递状态,这对于单个处理器的灵活性很有用,或者通过使用结构体。

这个菜谱将演示这两者。我们将使用一个结构控制器来存储存储接口,并创建两个由外部函数修改的单个处理器路由。

准备工作

参考本章 准备工作 部分的 与 Web 处理器、请求和 ResponseWriters 协同工作 菜单中的步骤。

如何做到这一点...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序,创建并导航到 chapter7/rest 目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter7/controllers 复制测试或将其作为练习来编写一些自己的。

  3. 创建一个名为 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"`
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 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)
            }

        }

  1. 创建一个名为 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)
                }
            }
        }

  1. 创建一个名为 example 的新目录,并导航到该目录。

  2. 创建一个名为 main.go 的文件,并包含以下内容;确保将 controllers 导入修改为步骤 2 中设置的路径:

        package main

        import (
            "fmt"
            "net/http"

            "github.com/agtorre/go-cookbook/chapter7/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)
        }

  1. 运行 go run main.go

  2. 你也可以运行:

 go build ./example

你应该看到以下输出:

 $ go run main.go
 Listening on port :3333

  1. 在另一个终端中,运行以下命令:
 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"}

  1. 如果你复制或编写了自己的测试,请向上导航一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

这些策略之所以有效,是因为 Go 允许方法满足诸如 http.HandlerFunc 这样的类型函数。通过使用结构体,我们可以在 main.go 中注入各种组件,这可能包括数据库连接、日志记录等。在这个配方中,我们插入了一个 Storage 接口。所有连接到控制器的处理程序都可以使用其方法和属性。

GetValue 方法没有 http.HandlerFunc 签名,而是返回它。这就是我们如何使用闭包来注入状态。在 main.go 中,我们定义了两个路由,一个将 UseDefault 设置为 false,另一个设置为 true。这可以在定义跨越多个路由的函数时使用,或者在使用结构体时,你的处理程序感觉过于繁琐。

验证 Go 结构体和用户输入

网页验证可能是一个难题。这个配方将探讨使用闭包来支持轻松模拟验证函数,并允许在初始化控制器结构体时(如前一个配方所述)进行灵活的验证类型。

我们将在结构体上执行此验证,但不探讨如何填充结构体。我们可以假设数据将通过解析 JSON 有效负载、显式地从表单输入填充或其他方法来填充。

准备工作

参考在 Working with web handlers, requests, and ResponseWriters 配方的 Getting ready 部分中给出的步骤。

如何做到这一点...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建并导航到 chapter7/validation 目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter7/validation 复制测试或将其作为练习编写一些自己的代码。

  3. 创建一个名为 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,
            }
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 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
                }
            }
        }

  1. 创建一个名为 example 的新目录,并导航到该目录。

  2. 创建一个名为 main.go 的文件,并包含以下内容;确保将 validation 导入修改为步骤 2 中设置的路径:

        package main

        import (
            "fmt"
            "net/http"

            "github.com/agtorre/go-cookbook/chapter7/validation"
        )

        func main() {
            c := validation.New()
            http.HandleFunc("/", c.Process)
            fmt.Println("Listening on port :3333")
            err := http.ListenAndServe(":3333", nil)
            panic(err)
        }

  1. 运行 go run main.go

  2. 你也可以运行:

 go build ./example

你应该看到以下输出:

 $ go run main.go
 Listening on port :3333

  1. 在另一个终端中运行以下命令:
 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}'  

你应该看到以下输出:

 $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>

  1. 如果你复制或编写了自己的测试用例,向上移动一个目录并运行 go test。确保所有测试通过。

它是如何工作的...

我们通过向我们的控制器结构体传递闭包来处理验证。对于控制器可能需要验证的任何输入,我们都需要这些闭包之一。这种方法的优势在于我们可以在运行时模拟和替换验证函数,从而使测试变得简单得多。此外,我们不受单一函数签名的限制,我们可以传递诸如数据库连接之类的信息给验证函数。

此配方还展示了返回一个名为 Verror 的类型化错误。此类型包含可以显示给用户的验证错误消息。这种方法的一个缺点是它不能同时处理多个验证消息。这可以通过修改 Verror 类型来实现,允许更多的状态,例如,通过包括一个映射,以便在从我们的 ValidatePayload 函数返回之前存储多个验证错误。

渲染和内容协商

Web 处理器可以返回各种内容类型,例如,它们可以返回 JSON、纯文本、图像等。在与 API 通信时,通常可以指定和接受内容类型,以明确你将传递数据的格式以及你希望接收的数据格式。

此配方将探讨使用 unrolled/render 和自定义函数来协商内容类型并相应地响应。

准备工作

根据以下步骤配置你的环境:

  1. 参考配方 Working with web handlers, requests, and ResponseWriters 中的 Getting ready 部分的步骤。

  2. 运行 go get github.com/unrolled/render 命令。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 在你的终端/控制台应用程序中,创建并导航到 chapter7/negotiate 目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter7/negotiate 复制测试用例,或者将此作为练习来编写你自己的代码。

  3. 创建一个名为 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(),
            }
        }

  1. 创建一个名为 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)
                }
        }

  1. 创建一个名为 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!"})
        }

  1. 创建一个名为 example 的新目录并导航到它。

  2. 创建一个名为 main.go 的文件,并包含以下内容;确保将协商导入修改为使用步骤 2 中设置的路径:

        package main

        import (
            "fmt"
            "net/http"

            "github.com/agtorre/go-cookbook/chapter7/negotiate"
        )

        func main() {
            http.HandleFunc("/", negotiate.Handler)
            fmt.Println("Listening on port :3333")
            err := http.ListenAndServe(":3333", nil)
            panic(err)
        }

  1. 运行 go run main.go

  2. 你也可以运行:

 go build ./example

你应该看到以下输出:

 $ go run main.go
 Listening on port :3333

  1. 在另一个终端中运行以下命令:
 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!"}

  1. 如果你复制或编写了自己的测试用例,向上移动一个目录并运行 go test。确保所有测试通过。

它是如何工作的...

github.com/unrolled/render 包为此菜谱做了大量工作。如果你需要处理 HTML 模板等,你可以输入大量的其他选项。此菜谱可用于在处理网络处理程序时自动协商,如在此处通过传递各种内容类型标题或直接操作结构体所示。

可以将类似的模式应用于接受标题,但请注意,这个标题通常包含多个值,并且你的代码必须考虑到这一点。

实现和使用中间件

Go 中的处理程序中间件是一个被广泛探讨的领域。有各种处理中间件的包。此菜谱将从头开始创建中间件并实现一个 ApplyMiddleware 函数来链接一系列中间件。

它还将探索在请求上下文对象中设置值并在稍后使用中间件检索它们。所有这些都将使用一个非常基本的处理程序来完成,以帮助演示如何将中间件逻辑与你的处理程序解耦。

准备工作

请参考 与网络处理程序、请求和 ResponseWriters 一起工作 菜谱中的 准备工作 部分给出的步骤。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建并导航到 chapter7/middleware 目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter7/middleware 复制测试或将其用作练习来编写一些你自己的测试。

  3. 创建一个名为 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))
                }
            }
        }

  1. 创建一个名为 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 ""
        }

  1. 创建一个名为 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"))
        }

  1. 创建一个名为 example 的新目录并导航到它。

  2. 创建一个名为 main.go 的文件,并包含以下内容;确保将 middleware 导入修改为你在第 2 步中设置的路径:

        package main

        import (
            "fmt"
            "log"
            "net/http"
            "os"

            "github.com/agtorre/go-cookbook/chapter7/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)
        }

  1. 运行 go run main.go

  2. 你也可以运行:

 go build ./example

你应该看到以下输出:

 $ go run main.go
 Listening on port :3333

  1. 在另一个终端中,多次运行以下 curl 命令:
 curl "http://localhost:3333

你将看到以下输出:

 $curl "http://localhost:3333
 success

 $curl "http://localhost:3333
 success

 $curl "http://localhost:3333
 success

  1. 在原始的 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

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 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 修改传出的响应。此外,还支持缓冲响应。

准备就绪

参考文档中“准备就绪”部分的步骤。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建并导航到 chapter7/proxy 目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter7/proxy 复制测试或将其作为练习编写一些自己的代码。

  3. 创建一个名为 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 implments 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)
        }

  1. 创建一个名为 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())
        }

  1. 创建一个名为 example 的新目录并导航到它。

  2. 创建一个名为 main.go 的文件,并包含以下内容;确保将 proxy 导入修改为使用你在第 2 步中设置的路径:

        package main

        import (
            "fmt"
            "net/http"

            "github.com/agtorre/go-cookbook/chapter7/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)
        }

  1. 运行 go run main.go

  2. 你也可以运行:

 go build ./example

你应该看到以下输出:

 $ go run main.go
 Listening on port :3333

  1. 使用浏览器导航到 localhost:3333/。你应该能看到 golang.org/ 网站被渲染出来!

  2. 如果你复制或编写了自己的测试,请进入上一级目录并运行 go test。确保所有测试都通过。

它是如何工作的...

Go 请求和响应对象在客户端和处理器之间大部分是可共享的。此代码通过满足 Handler 接口的 Proxy 结构体获取请求。main.go 文件使用 Handle 而不是其他地方使用的 HandleFunc。一旦请求可用,它就被修改为为请求添加 Proxy.BaseURL 前缀,然后客户端将其分派。最后,响应被复制回 ResponseWriter 接口。这包括所有头部、正文和状态。

我们还可以添加一些额外的功能,例如基本身份验证、令牌管理等等,如果需要的话。这对于令牌管理很有用,其中代理管理 JavaScript 或其他客户端应用程序的会话。

将 GRPC 作为 JSON API 导出

在第六章 理解 GRPC 客户端 的配方中,Web 客户端和 API,我们编写了一个基本的 GRPC 服务器和客户端。这个配方将在此基础上扩展,通过将常见的 RPC 函数放在一个包中,并将它们包装在 GRPC 服务器和标准网络处理器中。当你的 API 想要支持这两种类型的客户端,但又不想为常见功能重复代码时,这可能很有用。

准备就绪

根据以下步骤配置你的环境:

  1. 请参考 与网络处理器、请求和 ResponseWriters 一起工作 配方中的 准备就绪 部分的步骤。

  2. github.com/grpc/grpc/blob/master/INSTALL.md 安装 GRPC。

  3. 运行 go get github.com/golang/protobuf/proto 命令。

  4. 运行 go get github.com/golang/protobuf/protoc-gen-go 命令。

如何做...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建并导航到 chapter7/grpcjson 目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter7/grpcjson 复制测试,或者将其作为练习编写一些自己的代码。

  3. 创建一个名为 keyvalue 的新目录,并导航到它。

  4. 创建一个名为 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;
        }

  1. 运行以下命令:
 protoc --go_out=plugins=grpc:. keyvalue.proto

  1. 向上移动一个目录。

  2. 创建一个名为 internal 的新目录。

  3. 创建一个名为 internal/keyvalue.go 的文件,并包含以下内容:

        package internal

        import (
            "golang.org/x/net/context"
            "sync"

            "github.com/agtorre/go-cookbook/chapter7/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 map and controller
        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
        }

  1. 创建一个名为 grpc 的新目录。

  2. 创建一个名为 grpc/main.go 的文件,并包含以下内容:

        package main

        import (
            "fmt"
            "net"

            "github.com/agtorre/go-cookbook/chapter7/grpcjson/internal"
            "github.com/agtorre/go-cookbook/chapter7/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)
        }

  1. 创建一个名为 http 的新目录。

  2. 创建一个名为 http/set.go 的文件,并包含以下内容:

        package main

        import (
            "encoding/json"
            "net/http"

            "github.com/agtorre/go-cookbook/chapter7/grpcjson/internal"
            "github.com/agtorre/go-cookbook/chapter7/grpcjson/keyvalue"
            "github.com/apex/log"
        )

        // Controller holds an internal KeyValueObject
        type Controller struct {
            *internal.KeyValue
        }

        // SetHandler wraps or 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)
        }

  1. 创建一个名为 http/get.go 的文件,并包含以下内容:
        package main

        import (
            "encoding/json"
            "net/http"

            "google.golang.org/grpc"
            "google.golang.org/grpc/codes"

            "github.com/agtorre/go-cookbook/chapter7/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)
        }

  1. 创建一个名为 http/main.go 的文件,并包含以下内容:
        package main

        import (
            "fmt"
            "net/http"

            "github.com/agtorre/go-cookbook/chapter7/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)
        }

  1. 运行 go run http/*.go 命令。

你应该看到以下输出:

 $ go run http/*.go
 Listening on port :3333

  1. 在另一个终端中运行以下命令:
 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  
      'name=Reader;greeting=Goodbye' 
 <should return a 404>

 $curl "http://localhost:3333/get?key=test" -v 
      'name=Reader;greeting=Goodbye' 
 {"value":"123"}

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

虽然这个配方省略了客户端,但你可以在第六章的理解 gRPC 客户端配方中复制这些步骤,Web 客户端和 API,你应该会看到与我们使用 curl 看到的结果完全相同。httpgrpc目录都使用了相同的内部包。在这个包中,我们必须小心返回适当的 gRPC 错误代码,并将这些错误代码正确映射到我们的 HTTP 响应。在这种情况下,我们使用codes.NotFound,将其映射到http.StatusNotFound。如果你必须处理多个错误,使用switch语句可能比使用if…else语句更有意义。

你可能还会注意到,gRPC 签名通常非常一致。它们接收一个请求并返回一个可选的响应和一个错误。如果你的 gRPC 调用足够重复,你可以创建一个通用的处理程序适配器,而且这也似乎非常适合代码生成,你最终可能会在像github.com/goadesign/goa这样的包中看到类似的东西。

第八章:测试

在本章中,我们将介绍以下食谱:

  • 使用标准库进行模拟

  • 使用 Mockgen 包

  • 使用表格驱动测试来提高覆盖率

  • 使用第三方测试工具

  • 实践模糊测试

  • 使用 Go 进行行为测试

简介

本章将与前面的章节不同;这将专注于测试和测试方法。Go 提供了出色的测试支持,然而,对于来自更动态的语言(在这些语言中猴子补丁和模拟相对简单)的开发者来说,这可能很难理解。

Go 测试鼓励代码采用特定的结构,特别是测试和模拟接口非常直接且支持良好。某些类型的代码可能更难测试。例如,测试使用包级全局变量、未抽象为接口的地方以及具有非导出变量或方法的结构的代码可能很困难。本章将分享一些测试 Go 代码的食谱。

使用标准库进行模拟

在 Go 中,模拟通常意味着实现一个带有测试版本的接口,允许您从测试中控制运行时行为。它也可能指代模拟函数和方法,我们将在本食谱中探索另一个技巧。这个技巧使用了在play.golang.org/p/oLF1XnRX3C中定义的PatchRestore函数。

通常,将代码组合起来以便频繁使用接口,并将代码分成小块进行测试会更好。包含大量分支条件或深层嵌套逻辑的代码可能很难测试,并且测试最终可能更脆弱。这是因为开发者需要在测试中跟踪更多的模拟对象、补丁、返回值和状态。

准备工作

根据以下步骤配置您的环境:

  1. golang.org/doc/install下载并安装 Go 到您的操作系统上,并配置您的GOPATH环境变量。

  2. 打开一个终端/控制台应用程序。

  3. 导航到您的GOPATH/src并创建一个项目目录,例如,$GOPATH/src/github.com/yourusername/customrepo

所有代码都将从这个目录运行和修改。

  1. 可选地,使用go get github.com/agtorre/go-cookbook/命令安装代码的最新测试版本。

如何做到这一点...

这些步骤涵盖了编写和运行您的应用程序:

  1. 从您的终端/控制台应用程序中,创建chapter8/mocking目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter8/mocking复制测试或将其作为练习编写一些自己的代码。

  3. 创建一个名为mock.go的文件,内容如下:

        package mocking

        // DoStuffer is a simple interface
        type DoStuffer interface {
            DoStuff(input string) error
        }

  1. 创建一个名为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)
            }
        }

  1. 创建一个名为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
        }

  1. 创建一个名为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
        }

  1. 创建一个名为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)
                  }
                })
            }
        }

  1. 为剩余的函数填写测试,然后向上移动一个目录并运行go test。确保所有测试都通过:
 $go test
 PASS
 ok github.com/agtorre/go-cookbook/chapter8/mocking 0.006s

它是如何工作的...

这个食谱演示了如何模拟接口以及已声明为变量的函数。还有一些库可以直接在声明的函数上模拟这个补丁/恢复,但为了完成这个功能,它们绕过了 Go 的许多类型安全性。如果你需要从外部包中修补函数,你可以使用以下技巧:

// whatever package you wanna patch
import "github.com/package" 

// this is patchable using the method described in this recipe
var packageDoSomething = package.DoSomething

对于这个食谱,我们首先设置测试并使用表格驱动测试。关于这种技术的文献有很多,我建议进一步探索。一旦我们的测试设置完成,我们就为模拟函数选择输出。为了模拟我们的接口,我们的模拟对象定义了可以在运行时重写的闭包。我们应用补丁/恢复技术来更改全局函数,并在每次循环后恢复它。这要归功于t.Run,它为测试的每个循环设置一个新的函数。

使用 Mockgen 包

之前的例子使用了我们的自定义模拟对象。当你与许多接口一起工作时,编写这些对象可能会变得繁琐且容易出错。这是一个生成代码非常有意义的地方。幸运的是,有一个名为github.com/golang/mock/gomock的包,它提供了模拟对象的生成,并为我们提供了一个非常实用的库,可以与接口测试一起使用。

这个食谱将探索gomock的一些功能,并涵盖在哪里、何时以及如何与生成模拟对象一起工作的权衡。

准备就绪

按照以下步骤配置你的环境:

  1. 参考本章“使用标准库进行模拟”食谱中的“准备就绪”部分。

  2. 运行go get github.com/golang/mock/命令。

如何做...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建chapter8/mockgen目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter8/mockgen复制测试或将其作为练习来编写你自己的测试。

  3. 创建一个名为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)
        }

  1. 创建一个名为internal的目录。

  2. 运行mockgen -destination internal/mocks.go -package internal github.com/agtorre/go-cookbook/chapter8/mockgen GetSetter命令:

    • 确保你将包路径替换为你的本地版本。

    • 这将创建一个名为internal/mocks.go的文件。

  3. 创建一个名为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
        }

  1. 创建一个名为interface_test.go的文件,内容如下:
        package mockgen

        import (
            "errors"
            "testing"

            "github.com/agtorre/go-cookbook/chapter8/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)
            }
        }

  1. 创建一个名为exec_test.go的文件,内容如下:
        package mockgen

        import (
            "errors"
            "testing"

            "github.com/agtorre/go-cookbook/chapter8/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)
                    }
                })
             }
        }

  1. 为剩余的函数填写测试,然后向上移动一个目录并运行go test。确保所有测试都通过。

它是如何工作的...

生成的模拟对象允许测试指定预期的参数,函数将被调用的次数,以及返回的内容,并且允许我们设置额外的工件,例如,如果原始函数有类似的流程,我们可以直接写入通道。interface_test.go文件展示了在调用时使用模拟对象的示例。通常,测试将看起来更像exec_test.go,我们将在其中拦截实际代码执行的接口函数调用并在测试时改变它们的行为。

exec_test.go文件还展示了如何在表格驱动测试环境中使用模拟对象。Any()函数意味着模拟函数可以被调用零次或多次,这对于代码提前终止的情况非常有用。

在这个菜谱中演示的最后一个技巧是将模拟对象放入internal包中。当你需要模拟你自己的包外声明的函数时,这很有用。这允许这些方法在非_test.go文件中定义,但不会允许将它们导出给库的用户。通常,直接将模拟对象放入与当前编写的测试相同的包名的_test.go文件中会更简单。

使用表格驱动测试来提高覆盖率

这个菜谱将演示编写表格驱动测试、收集测试覆盖率并提高覆盖率的过程。它还将使用github.com/cweill/gotests包来生成测试。如果你已经下载了其他章节的测试代码,这些应该看起来非常熟悉。通过结合这个菜谱和前两个菜谱,你应该能够在所有情况下通过一些工作实现 100%的测试覆盖率。

准备工作

根据以下步骤配置你的环境:

  1. 参考本章“Mocking using the standard library”菜谱的“准备工作”部分。

  2. 运行go get github.com/cweill/gotests/命令。

如何做...

这些步骤涵盖了编写和运行你的应用程序:

  1. 在你的终端/控制台应用程序中,创建chapter8/coverage目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter8/coverage复制测试或将其用作练习编写一些你自己的代码。

  3. 创建一个名为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
        }

  1. 运行gotests -all -w命令。

  2. 这将生成一个名为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)
                    }
                })
            }
        }

  1. TODO部分填写以下内容:
        {"no condition", args{true}, true},

  1. 运行go test -cover命令,你将看到以下输出:
 go test -cover 
 PASS
 coverage: 66.7% of statements
 ok github.com/agtorre/go-cookbook/chapter8/coverage 0.007s

  1. TODO部分添加另一个条目:
        {"condition", args{false}, false},

  1. 运行go test -cover命令,你将看到以下输出:
 go test -cover 
 PASS
 coverage: 100.0% of statements
 ok github.com/agtorre/go-cookbook/chapter8/coverage 0.007s

  1. 运行以下命令:
 go test -coverprofile=cover.out 
 go tool cover -html=cover.out -o coverage.html

  1. 在浏览器中打开coverage.html文件以查看图形覆盖率报告。

它是如何工作的...

go test -cover命令是基本 Go 安装的一部分。它可以用来收集 Go 应用程序的覆盖率报告。此外,它还具有输出覆盖率指标和 HTML 覆盖率报告的能力。这个工具通常被其他工具包装,将在下一个配方中介绍。这些基于表格的测试风格在github.com/golang/go/wiki/TableDrivenTests中有介绍,是编写干净测试的极好方式,可以处理许多情况而无需编写大量额外的代码。

这个配方首先自动生成测试代码,然后根据需要填充测试用例,以帮助创建更多覆盖率。唯一特别棘手的时候是当你调用非变量函数或方法时。例如,让gob.Encode()返回错误以增加测试覆盖率可能很棘手。使用本章中使用标准库进行模拟配方中描述的方法,并使用var gobEncode = gob.Encode来允许修补,也可能显得有些奇怪。因此,很难主张 100%的测试覆盖率,相反,可以主张重点测试外部接口,即测试许多输入和输出的变体,在某些情况下,正如我们在本章的使用 Go 进行行为测试配方中所看到的,模糊测试可能变得有用。

使用第三方测试工具

有许多有助于 Go 测试的工具。这些工具可以更容易地了解代码覆盖率,每个函数级别的工具,用于断言以减少测试代码行数,以及测试运行器。这个配方将涵盖github.com/axw/gocovgithub.com/smartystreets/goconvey包,以展示一些这种功能。根据您的需求,还有许多其他值得注意的测试框架。github.com/smartystreets/goconvey包支持断言和测试运行器。在 Go 1.7 之前,它曾经是拥有标记子测试的最干净的方式。

准备就绪

按照以下步骤配置您的环境:

  1. 参考本章准备就绪部分中的使用标准库进行模拟

    本章的配方。

  2. 运行go get github.com/axw/gocov命令。

  3. 运行go get github.com/smartystreets/goconvey/命令。

如何做...

这些步骤涵盖了编写和运行您的应用程序:

  1. 从您的终端/控制台应用程序中,创建chapter8/tools目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter8/tools复制测试或将其用作练习来编写一些您自己的代码。

  3. 创建一个名为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
        }

  1. 创建一个名为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
        }

  1. 创建一个名为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)
                })
            }
        }

  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)
                })
            }
        }

  1. 运行gocov test | gocov report命令,您将看到以下输出:
 $ gocov test | gocov report
 ok github.com/agtorre/go-cookbook/chapter8/tools 0.006s 
      coverage: 100.0% of statements

 github.com/agtorre/go-cookbook/chapter8/tools/struct.go 
      c.example3 100.00% (5/5)
 github.com/agtorre/go-cookbook/chapter8/tools/funcs.go example 
      100.00% (2/2)
 github.com/agtorre/go-cookbook/chapter8/tools/funcs.go @12:16 
      100.00% (2/2)
 github.com/agtorre/go-cookbook/chapter8/tools ---------- 
      100.00% (9/9)

 Total Coverage: 100.00% (9/9)

  1. 运行goconvey命令,它将打开一个看起来像这样的浏览器:

  1. 确保所有测试都通过。

它是如何工作的...

本食谱演示了如何将goconvey命令连接到您的测试。Convey关键字基本上替换了t.Run并在goconvey网络 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 断言时,UI 中的成功会有一个勾选标记,并且还有额外的统计数据。它还可以将 if 检查的长度减少到一行,甚至可以创建自定义断言。

如果您打开goconvey网络界面并开启通知,每次保存代码时,测试将自动运行,并且您将收到关于覆盖率增加或减少以及构建失败的通知。

所有三种工具断言、测试运行器和网络 UI 都可以独立使用或一起使用。

当您努力提高测试覆盖率时,gocov工具非常有用。它可以快速识别缺乏覆盖率的函数,并帮助您深入了解覆盖率报告。此外,gocov可以通过使用github.com/matm/gocov-html包生成与 Go 代码一起分发的替代 HTML 报告。

实践模糊测试

本食谱将探讨模糊测试及其如何帮助验证函数。在第三章的“货币转换和 float64 考虑”食谱中,我们在“数据转换和组合”中创建了一个函数,该函数接受作为字符串的十进制美国货币并返回表示美分的 int64 版本。我们将修改该函数并通过模糊测试演示如何找到 panic。

准备就绪

根据以下步骤配置您的环境:

  1. 参考本章“使用标准库进行模拟”食谱中的“准备就绪”部分。

  2. 运行go get github.com/dvyukov/go-fuzz/go-fuzz命令。

  3. 运行go get github.com/dvyukov/go-fuzz/go-fuzz-build命令。

如何做到这一点...

这些步骤涵盖了编写和运行您的应用程序:

  1. 在您的终端/控制台应用程序中,创建chapter8/fuzz目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter8/fuzz 复制测试,或者将其作为练习编写您自己的代码。

  3. 创建一个名为dollars.go的文件,内容如下:

        package fuzz

        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
            val, err := strconv.ParseFloat(amount, 64)
            if err != nil {
                return 0, err
            }

            if val > 1000 && val < 1100 {
                panic("invalid range")
            }

            // 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]
                if len(r) > 2 {
                    r = r[:2]
                }
            }

            // 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)
        }

  1. 创建一个名为fuzz.go的文件,内容如下:
        package fuzz

        // Fuzz is the interface required to use gofuzz
        func Fuzz(data []byte) int {
            amount := string(data)

            _, err := ConvertStringDollarsToPennies(amount)
            if err != nil {
                return 0
            }
            return 1
        }

  1. 运行go-fuzz-build github.com/agtorre/go-cookbook/chapter8/fuzz命令或更改路径以匹配您的代码。

  2. 运行以下命令:

 mkdir -p output/corpus

 echo "0.01" > output/corpus/a
 echo "-0.01" > output/corpus/b
 echo "0.10" > output/corpus/c
 echo "1.00" > output/corpus/d
 echo "-1.00" > output/corpus/e
 echo "1.11" > output/corpus/f
 echo "1" > output/corpus/g
 echo "2" > output/corpus/h
 echo "999.99" > output/corpus/i

  1. 运行go-fuzz -bin=./fuzz-fuzz.zip -workdir=output命令,你将看到以下输出:
 go-fuzz -bin=./fuzz-fuzz.zip -workdir=output

 .
 .
 .
 2017/04/02 10:58:43 slaves: 4, corpus: 91 (11s ago), crashers: 
      1, restarts: 1/7064, execs: 204856 (13630/sec), cover: 453, 
      uptime: 15s
 2017/04/02 10:58:46 slaves: 4, corpus: 91 (14s ago), crashers: 
      1, restarts: 1/7244, execs: 253555 (14086/sec), cover: 453, 
      uptime: 18s

  1. 运行几次迭代后,通过按Ctrl + C退出。

  2. 填写剩余函数的测试,向上移动一个目录,并运行go test。确保所有测试都通过。

它是如何工作的...

github.com/dvyukov/go-fuzz包使用进化算法构建输入语料库以测试 Go 代码。在我们的例子中,我们故意引入了 panic,以演示找到崩溃时的行为。模糊测试是一种发现意外 panic 的实用方法,尤其是在处理数组边界或任意输入的编程中。

在模糊测试一个应用程序时,最困难的部分之一是编写一个合适的模糊函数。go-fuzz应用程序将根据此函数的响应进行适应。如果你的模糊函数返回1,则被视为成功的输入。如果返回-1,则该条目将不会包含在语料库中,如果返回0,则给予较低优先级。我们可以通过在步骤 4 中更改模糊函数以返回-1而不是0来找到被接受但可能未预期的有趣输入。例如,+1是这个函数的可能输入。

我们还通过向语料库建议一些项目来帮助我们的 fuzzer。这些项目来自我们的单元测试,代表已知的好值。这有助于 Go 模糊收敛到相关输入,例如,如果您的函数以整数范围作为输入,测试非整数输入可能需要很长时间。

使用 Go 进行行为测试

行为测试或集成测试是执行端到端黑盒测试的好方法。此类测试的一个流行框架是 cucumber (cucumber.io/),它使用 Gherkin 语言用英语描述测试步骤,然后在代码中实现这些步骤。Go 也有一个 cucumber 库(github.com/DATA-DOG/godog)。本食谱将探讨使用godog包编写行为测试。

准备工作

根据以下步骤配置您的环境:

  1. 参考文档的准备工作部分。

    本章的食谱。

  2. 运行go get github.com/DATA-DOG/godog命令。

  3. 运行go get github.com/DATA-DOG/godog/cmd/godog命令。

如何操作...

这些步骤涵盖了编写和运行您的应用程序:

  1. 从您的终端/控制台应用程序中,创建chapter8/bdd目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter8/bdd复制测试或将其用作练习编写一些自己的代码。

  3. 创建一个名为 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)))
        }

  1. 创建一个名为 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 |

  1. 运行 godog 命令,你将看到以下输出:
 $ godog
 .
 1 scenarios (1 undefined)
 4 steps (4 undefined)
 89.062µs
 .

  1. 这应该为你提供了一个实现我们在特性文件中编写的测试的框架;将这些内容复制到 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
        }

  1. 运行 godog 命令,你将看到以下输出:
 $ godog
 .
 1 scenarios (1 pending)
 4 steps (2 passed, 1 pending, 1 skipped)
 .

  1. 填写剩余的两个步骤:
        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)
        }

  1. 运行 godog 命令,你将看到以下输出:
 $ godog 
 .
 1 scenarios (1 passed)
 4 steps (4 passed)
 552.605µs
 .

它是如何工作的...

Cucumber 框架非常适合结对编程、端到端测试以及任何最好用书面指令进行沟通且对非技术人员可理解的测试类型。一旦某个步骤被实现,通常可以在需要的地方重用它。如果你想要测试服务之间的集成,可以编写使用实际 HTTP 客户端的测试,前提是你首先确保你的环境已经设置好以接收 HTTP 连接。

Datadog 对 BDD 的实现缺少一些你可能期望的功能,如果你使用过其他 Cucumber 框架,包括缺少示例、在函数间传递上下文以及许多其他关键字。然而,这是一个良好的开端,通过在这个菜谱中使用一些技巧,例如使用全局变量来跟踪状态(并确保在场景之间清理这些全局变量),可以构建一个相当健壮的测试集。Datadog 测试包还使用第三方测试运行器,因此无法与 gocovgo test -cover 等包一起使用。

第九章:并行和并发

本章将涵盖以下食谱:

  • 使用通道和 select 语句

  • 使用 sync.WaitGroup 执行异步操作

  • 使用原子操作和互斥锁

  • 使用上下文包

  • 执行通道的状态管理

  • 使用工作池设计模式

  • 使用工作者创建管道

简介

本章将介绍工作池、异步操作等待组和 context 包的使用。并行和并发是 Go 语言最宣传和推广的特性之一。本章将提供一些有用的模式来帮助你入门,并帮助你理解这些特性。

Go 提供了使并行应用程序成为可能的原语。Goroutines 允许任何函数成为异步和并发的。Channels 允许应用程序与 goroutines 建立通信。Go 中的一条著名说法是 不要通过共享内存来通信;相反,通过通信来共享内存,出自 blog.golang.org/share-memory-by-communicating

使用通道和 select 语句

Go channels 与 goroutines 结合,是异步通信的一等公民。当使用 select 语句时,Channels 变得特别强大。这些语句允许 goroutine 同时智能地处理多个通道的请求。

准备工作

根据以下步骤配置你的环境:

  1. golang.org/doc/install 下载并安装 Go 到你的操作系统上,并配置你的 GOPATH 环境变量。

  2. 打开终端/控制台应用程序。

  3. 导航到 GOPATH/src 并创建一个项目目录,例如 $GOPATH/src/github.com/yourusername/customrepo

所有代码都将从这个目录运行和修改。

  1. 可选地,使用 go get github.com/agtorre/go-cookbook/ 命令安装代码的最新测试版本。

如何做到...

这些步骤涵盖了编写和运行你的应用程序:

  1. 在你的终端/控制台应用程序中,创建 chapter9/channels 目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter9/channels 复制测试用例,或者使用这个练习来编写你自己的代码。

  3. 创建一个名为 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"
                }
            }
        }

  1. 创建一个名为 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")
                }
            }
        }

  1. 创建一个名为 example 的新目录并导航到它。

  2. 创建一个名为 main.go 的文件,内容如下,并确保将 channels 导入修改为步骤 2 中设置的路径:

        package main

        import (
            "context"
            "time"

            "github.com/agtorre/go-cookbook/chapter9/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(1 * time.Second)
        }

  1. 运行 go run main.go

  2. 你还可以运行以下命令:

 go build ./example

现在,你应该看到以下输出:

 $ go run main.go
 tick
 tock
 tick
 tick
 tock
 tick
 tick
 tock
 tick
 .
 .
 .
 sender done.
 printer done.

  1. 如果你复制或编写了自己的测试用例,请向上导航一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

这个配方演示了两种启动工作进程的方法,该进程可以读取或写入通道,也可能两者都做。终止条件是一个done通道,或者使用context包。使用上下文包配方将更详细地介绍上下文。

使用main包将独立的函数连接起来;多亏了这一点,只要通道不共享,就可以设置多个对。此外,还可以有多个 goroutine 监听同一个通道,我们将在使用工作池设计模式的配方中探讨这一点。

最后,由于 goroutines 的异步性质,建立清理和终止条件可能很棘手;例如,一个常见的错误是以下操作:

select{
    case <-time.Tick(200 * time.Millisecond):
    //this resets whenever any other 'lane' is chosen
}

通过在select语句中放置勾号,可以防止这种情况发生。在select语句中也没有简单的方法来优先处理流量。

使用 sync.WaitGroup 执行异步操作

有时,执行多个异步操作然后等待它们完成再继续是有用的。例如,如果操作需要从多个 API 中提取信息并汇总这些信息,那么异步地发出客户端请求可能会有所帮助。本章将探讨使用sync.WaitGroup来并行编排非依赖性任务。

准备工作

参考本章中使用通道和选择语句配方中的准备工作部分。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 在你的终端/控制台应用程序中创建

    chapter9/waitgroup目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter9/waitgroup复制测试或将其作为练习来编写一些自己的代码。

  3. 创建一个名为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, 
            ","))
        }

        // Valid can be used to determine if
        // we should return this
        func (c *CrawlError) Valid() bool {
            return len(c.Errors) != 0
        }

  1. 创建一个名为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()
            if cerr.Valid() {
                return resps, cerr
            }
            log.Printf("completed crawling in %s", time.Since(start))
            return resps, nil
        }

  1. 创建一个名为example的新目录并导航到它。

  2. 创建一个名为main.go的文件,内容如下。确保你修改waitgroup导入以使用你在步骤 2 中设置的路径:

        package main

        import (
            "fmt"

            "github.com/agtorre/go-cookbook/chapter9/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)
        }

  1. 运行go run main.go

  2. 你还可以运行以下命令:

 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]

  1. 如果你复制了自己的测试或编写了自己的测试,请向上移动一个目录并运行go test。确保所有测试都通过。

它是如何工作的...

这个配方展示了如何在等待工作时使用waitgroups作为同步机制。本质上,waitgroup.Wait()将等待其内部计数器达到0waitgroup.Add(int)方法将计数器增加输入的数量,而waitgroup.Done()将计数器减1。因此,在各个 goroutine 将waitgroup标记为Done()的同时,需要异步地Wait()

在这个示例中,我们在发送每个 HTTP 请求之前增加计数,然后调用一个 defer wg.Done()方法,这样我们就可以在 goroutine 终止时减少计数。然后我们等待所有 goroutine 完成,然后再返回我们的聚合结果。

在实践中,最好使用通道来传递错误和响应。

在执行此类异步操作时,你应该考虑修改共享映射等线程安全。如果你记住这一点,waitgroups是等待任何类型异步操作的有用功能。

使用原子操作和互斥锁

在像 Go 这样的语言中,你拥有内置的异步操作和并行性,考虑线程安全变得很重要。例如,同时从多个 goroutine 访问映射是危险的。Go 在syncsync/atomic包中提供了一些辅助工具,以确保某些事件只发生一次,或者 goroutine 可以在操作上序列化。

这个示例将展示如何使用这些包安全地修改映射,以及如何保持一个可以被多个 goroutine 安全访问的全局序数值。它还将展示Once.Do方法,该方法可以用来确保 Go 应用程序只执行一次某些操作,例如读取配置或初始化变量。

准备就绪

参考本章中“使用通道和 select 语句”食谱的“准备就绪”部分。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建chapter9/atomic目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter9/atomic复制测试或将其作为练习编写一些自己的代码。

  3. 创建一个名为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")
        }

  1. 创建一个名为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)
        }

  1. 创建一个名为example的新目录并导航到它。

  2. 创建一个名为main.go的文件,内容如下,并确保你修改atomic导入以使用步骤 2 中设置的路径:

        package main

        import (
            "fmt"
            "sync"

            "github.com/agtorre/go-cookbook/chapter9/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'")
        }

  1. 运行go run main.go

  2. 你也可以运行以下命令:

 go build ./example

你现在应该看到以下内容:

 $ go run main.go

 initial ordinal is: 1123
 final ordinal is: 1133
 all keys found and marked as: 'success'

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行go test。确保所有测试都通过。

它是如何工作的...

对于我们的映射食谱,我们使用了ReadWrite互斥锁。这种互斥锁背后的想法是,任何数量的读者都可以获取读锁,但只有一个写者可以获取写锁。此外,当其他人(读者或写者)拥有锁时,写者无法获取锁。这很有用,因为与标准互斥锁相比,读取操作非常快且非阻塞。每次我们想要设置数据时,我们使用Lock()对象,每次我们想要读取数据时,我们使用RLock()。最终使用Unlock()RUnlock()是至关重要的,这样您就不会使应用程序发生死锁。defer Unlock()对象可能很有用,但可能比手动调用Unlock()慢。

当您想要将额外的操作与锁定值分组时,这种模式可能不够灵活。例如,在某些情况下,您可能想要锁定,执行一些额外的处理,然后才解锁。在设计时考虑这一点非常重要。

sync/atomic包被Ordinal用于获取和设置值。还有原子比较操作,如atomic.CompareAndSwapUInt64(),它们非常有价值。本食谱允许在Ordinal对象上仅调用一次 Init;否则,它只能原子性地增加。

我们循环创建 10 个 goroutine(与sync.Waitgroup同步)并显示序号正确增加了 10 次,以及我们映射中的每个键都适当地设置了。

使用上下文包

本书中的几个食谱都使用了context包。本食谱将探讨创建和管理上下文的基本知识。了解上下文的良好参考资料是blog.golang.org/context。自从这篇博客文章被撰写以来,上下文已从net/context移动到一个名为context的包。这仍然偶尔会在与 GRPC 等第三方库交互时引起问题。

本食谱将探讨为上下文设置和获取值、取消和超时。

准备就绪

参考本章中使用通道和 select 语句食谱的准备就绪部分。

如何操作...

这些步骤涵盖了编写和运行您的应用程序:

  1. 在您的终端/控制台应用程序中,创建chapter9/context目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter9/context复制测试或将其作为练习编写一些自己的代码。

  3. 创建一个名为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 ""

        }

  1. 创建一个名为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
                }
            }
        }

  1. 创建一个名为example的新目录并导航到它。

  2. 创建一个名为main.go的文件,内容如下。确保您修改context导入以使用步骤 2 中设置的路径:

        package main

            import "github.com/agtorre/go-cookbook/chapter9/context"

        func main() {
            context.Exec()
        }

  1. 运行go run main.go

  2. 您还可以运行以下命令:

 go build ./example

您现在应该看到以下输出:

 $ go run main.go
 timeout exceeded

 OR

 $ go run main.go
 deadline exceeded

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

当处理上下文值时,创建一个新的类型来表示键是个好主意。在这种情况下,我们创建了一个 key 类型,然后声明了一些相应的 const 值来表示我们所有的可能键。

在这个例子中,我们使用 Setup() 函数同时初始化所有的键值对。当修改上下文时,函数通常需要一个 context 参数并返回一个 context 值。所以签名通常看起来像这样:

func ModifyContext(ctx context.Context) context.Context

有时,这些方法也会返回一个错误或 cancel() 函数,例如在 context.WithCancelcontext.WithTimeoutcontext.WithDeadline 的情况下。所有子上下文都继承父上下文的属性。

在这个菜谱中,我们创建了两个子上下文,一个带有截止日期,一个带有超时。我们将这些设置为随机范围的超时,然后在接收到任何一个时终止。最后,我们根据给定的键提取一个值并打印它。

执行通道的状态管理

在 Go 中,通道可以是任何类型。结构体的通道允许你通过单个消息传递大量的状态。这个菜谱将探讨使用通道传递复杂的请求结构体并在复杂的响应结构体中返回它们的结果。

在下一道菜谱中,使用工作池设计模式,这个值变得更加明显,因为你可以创建能够执行各种任务的一般用途的工作者。

准备就绪

参考本章中 准备就绪 部分的 使用通道和选择语句 菜谱。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建并导航到 chapter9/state 目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter9/state 复制测试或将其用作练习来编写你自己的代码。

  3. 创建一个名为 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
        }

  1. 创建一个名为 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)
                }
            }
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 example 的新目录并导航到它。

  2. 创建一个名为 main.go 的文件,内容如下。确保将 state 导入修改为你在步骤 2 中设置的路径:

        package main

        import (
            "context"
            "fmt"

            "github.com/agtorre/go-cookbook/chapter9/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)
            }
        }

  1. 运行 go run main.go.

  2. 你也可以运行以下命令:

 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

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

在这个菜谱中的 Processor() 函数是一个无限循环直到其上下文被取消的函数,无论是通过显式调用取消还是通过超时。它将所有工作调度到 Process(),它可以处理各种操作给出的不同函数。也有可能让这些情况中的每一个调度另一个函数以实现更模块化的代码。

最终,响应被返回到响应通道,我们在最后循环并打印所有结果。我们还演示了除以 0 的错误情况。

使用工作池设计模式

工作池设计模式是一种将长时间运行的 goroutines 作为工作者调度的模式。这些工作者可以通过多个通道或使用具有指定类型的具有状态请求结构来处理各种工作,正如前一个菜谱中描述的那样。

这个菜谱将创建具有状态的工作者,并演示如何协调和启动多个工作者,它们在同一个通道上并发处理请求。这些工作者将像在 Web 身份验证应用程序中的加密工作者一样。他们的目的是使用 bcrypt 包对纯文本字符串进行哈希处理,并将文本密码与哈希进行比较。

准备工作

参考本章中 Using channels and the select statement 菜谱的 Getting ready 部分。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建 chapter9/pool 目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter9/pool 复制测试或使用这个练习来编写你自己的代码。

  3. 创建一个名为 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)
                }
            }
        }

  1. 创建一个名为 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")}
            }
        }

  1. 创建一个名为 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,
            }
        }

  1. 创建一个名为 example 的新目录,并导航到它。

  2. 创建一个名为 main.go 的文件,并包含以下内容。确保你修改 state 导入以使用你在第 2 步中设置的路径:

        package main

        import (
            "fmt"

            "github.com/agtorre/go-cookbook/chapter9/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)
            }
        }

  1. 运行 go run main.go

  2. 你也可以运行以下命令:

 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

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试都通过。

工作原理...

这个菜谱使用 Dispatch() 方法在单个输入通道、输出通道以及连接到单个 cancel() 函数的通道上创建多个工作者。如果你想要为不同的目的创建不同的池,这可以用来。例如,你可以通过使用单独的池来创建 10 个加密和 20 个比较工作者。对于这个菜谱,我们使用一个单独的池,向工作者发送哈希请求,检索响应,然后向同一个池发送比较请求。因此,执行工作的工作者每次都会不同,但它们都能执行这两种类型的工作。

这种方法的优点是,它既允许并行处理,也可以控制最大并发量。限制 goroutine 的最大数量对于限制内存也很重要。我选择加密作为这个菜谱的例子,因为如果为每个新请求启动一个新的 goroutine,例如在 Web 服务中,加密代码可能会耗尽你的 CPU 或内存。

使用工作者创建管道

这个菜谱演示了创建工作池组并将它们连接起来形成管道。对于这个菜谱,我们连接了两个池,但这个模式可以用于更复杂的操作,类似于中间件。

工作池可以用来保持工作者相对简单,并进一步控制并发。例如,在并行其他操作的同时序列化日志可能很有用。这也可以用于为更昂贵的操作创建更小的池,这样就不会超载机器资源。

准备就绪

参考本章中 准备就绪 部分 使用通道和 select 语句 菜谱。

如何做到...

这些步骤涵盖了编写和运行你的应用程序:

  1. 在你的终端/控制台应用程序中,创建名为 chapter9/pipeline 的目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter9/pipeline 复制测试或将其作为练习编写你自己的代码。

  3. 创建一个名为 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
            }
        }

  1. 创建一个名为 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
                }
            }
        }

  1. 创建一个名为 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)))
                }
            }
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 example 的新目录并导航到它。

  2. 创建一个名为 main.go 的文件,并包含以下内容,并确保将 state 导入修改为你在步骤 2 中设置的路径:

        package main

        import (
            "context"
            "fmt"

            "github.com/agtorre/go-cookbook/chapter9/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
            }
        }

  1. 运行 go run main.go

  2. 你还可以运行以下命令:

 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=

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

main 包创建了一个由 10 个编码器和两个打印机组成的管道。它在输入通道上排队 20 个字符串,并在输出通道上等待 20 个响应。如果消息到达输出通道,则表示它们已成功通过整个管道。

NewPipeline 函数用于连接池。它确保通道以适当的缓冲大小创建,并且某些池的输出通道连接到其他池的适当输入通道。还可以通过在每个工作器上使用输入通道和输出通道的数组、多个命名通道或通道映射来扩展管道。这将允许发送消息到每个步骤的记录器等操作。

第十章:分布式系统

在本章中,我们将涵盖以下食谱:

  • 使用 Consul 进行服务发现

  • 使用 Raft 实现基本共识

  • 使用 Docker 进行容器化

  • 编排和部署策略

  • 监控应用程序

  • 收集指标

简介

有时,应用级别的并行性不足以解决问题,开发中看似简单的事情在部署时可能会变得复杂。分布式系统在单机开发时不会遇到许多挑战。这些应用程序为监控、编写需要强一致性保证的应用程序和服务发现等问题增加了复杂性。此外,您必须始终注意单点故障,例如数据库。否则,当这个单一组件失败时,您的分布式应用程序可能会失败。

本章将探讨管理分布式数据、编排、容器化、指标和监控的方法。这些将成为您编写和维护微服务和大型分布式应用程序的工具箱的一部分。

使用 Consul 进行服务发现

当使用微服务方法构建应用程序时,你将拥有许多服务器,它们监听各种 IP、域名和端口。这些 IP 地址会因环境(预发布与生产)而异,并且在不同服务之间保持静态配置可能会很棘手。你还想了解当机器或服务因网络分区而无法访问或宕机时的情况。Consul 是一个提供许多功能的工具,但我们将探讨如何使用 Consul 注册服务以及如何从我们的其他服务中查询它们。

准备工作

根据以下步骤配置您的环境:

  1. golang.org/doc/install 下载并安装 Go 到您的操作系统上,并配置您的 GOPATH 环境变量。

  2. 打开终端/控制台应用程序。

  3. 导航到 GOPATH/src 并创建一个项目目录,例如,$GOPATH/src/github.com/yourusername/customrepo

    所有代码都将从这个目录运行和修改。

  4. 可选地,通过运行 go get github.com/agtorre/go-cookbook/ 命令安装最新测试版本的代码。

  5. www.consul.io/intro/getting-started/install.html 安装 Consul。

  6. 运行 go get github.com/hashicorp/consul/api 命令。

如何做到...

这些步骤涵盖了编写和运行应用程序:

  1. 从您的终端/控制台应用程序中创建 chapter10/discovery 目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter10/discovery 复制测试,或者将其作为练习编写一些自己的代码。

  3. 创建一个名为 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
        }

  1. 创建一个名为 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)
        }

  1. 创建一个名为 exec.go 的文件,并包含以下内容:
        package discovery

        import (
            "fmt"

            consul "github.com/hashicorp/consul/api"
        )

        // Exec creates a consul entry then queries it
        func Exec() error {
            config := consul.DefaultConfig()
            config.Address = "localhost:8500"
            name := "discovery"

            // faked name and port for example
            cli, err := NewClient(config, "localhost", name, 8080)
            if err != nil {
                return err
            }

            if err := cli.Register([]string{"Go", "Awesome"}); err !=   
            nil {
                return err
            }

            entries, _, err := cli.Service(name, "Go")
            if err != nil {
                return err
            }
            for _, entry := range entries {
                fmt.Printf("%#v\n", entry.Service)
            }

            return nil
        }

  1. 创建一个名为 example 的新目录并导航到它。

  2. 创建一个名为 main.go 的文件,并包含以下内容。确保你修改 channels 导入以使用你在步骤 2 中设置的路径:

        package main

        import "github.com/agtorre/go-cookbook/chapter10/discovery"

        func main() {
            if err := discovery.Exec(); err != nil {
                panic(err)
            }
        }

  1. 在一个单独的终端中使用 consul agent -dev -node=localhost 命令启动 Consul。

  2. 运行 go run main.go 命令。

  3. 你还可以运行:

      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}

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

Consul 提供了一个健壮的 Go API 库。一开始可能会觉得有些令人畏惧,但这个配方展示了你如何接近封装它。配置 Consul 超出了本配方的范围,但本配方展示了注册服务和根据键和标签查询其他服务的基本方法。

使用这个方法可以在启动时注册新的微服务,查询所有依赖的服务,并在关闭时注销。你可能还希望缓存这些信息,这样你就不必为每个请求都调用 Consul,但本配方提供了你可以扩展的基本工具。Consul 代理还使这些重复请求变得快速高效(www.consul.io/intro/getting-started/agent.html)。

使用 Raft 实现基本共识

Raft 是一种共识算法,允许分布式系统保持共享和管理状态(raft.github.io/)。在许多方面设置 Raft 系统都是复杂的,例如,你需要达成共识以进行选举并成功。当与多个节点一起工作时,这可能很难启动,也可能很难开始。一个基本的集群可以在单个节点/领导者上运行,但如果你想要冗余,至少需要三个节点以允许单个节点故障。

本配方实现了一个基本的内存 Raft 集群,构建了一个可以在某些允许的状态之间转换的状态机,并将分布式状态机连接到一个可以触发转换的 Web 处理器。当你实现 Raft 所需的基本有限状态机接口或进行测试时,这可能很有用。本配方使用 github.com/hashicorp/raft 作为基本的 Raft 实现。

准备工作

根据以下步骤配置你的环境:

  1. 参考本章中 使用 Consul 进行服务发现 配方的 准备工作 部分。

  2. 运行 go get github.com/hashicorp/raft 命令。

如何做到这一点...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建 chapter10/consensus 目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter10/consensus 复制测试,或者将其作为练习编写一些自己的代码。

  3. 创建一个名为 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
            }
        }

  1. 创建一个名为 config.go 的文件,内容如下:
        package consensus

        import "github.com/hashicorp/raft"

        var rafts map[string]*raft.Raft

        func init() {
            rafts = make(map[string]*raft.Raft)
        }

        // Config creates num in-memory raft
        // nodes and connects them
        func Config(num int) {
            conf := raft.DefaultConfig()
            snapshotStore := raft.NewDiscardSnapshotStore()

            addrs := []string{}
            transports := []*raft.InmemTransport{}
            for i := 0; i < num; i++ {
                addr, transport := raft.NewInmemTransport("")
                addrs = append(addrs, addr)
                transports = append(transports, transport)
            }
            peerStore := &raft.StaticPeers{StaticPeers: addrs}
            memstore := raft.NewInmemStore()

            for i := 0; i < num; i++ {
                for j := 0; j < num; j++ {
                    if i != j {
                        transports[i].Connect(addrs[j], transports[j])
                    }
                }

                r, err := raft.NewRaft(conf, NewFSM(), memstore, 
                memstore, snapshotStore, peerStore, transports[i])
                if err != nil {
                    panic(err)
                }
                r.SetPeers(addrs)
                rafts[addrs[i]] = r
            }
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 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()
            for k, rf := range rafts {
                if k == rf.Leader() {
                    state := r.FormValue("next")
                    result := rf.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(result.Response().(string)))
                    return
                }
            }
        }

  1. 创建一个名为 example 的新目录,并导航到它。

  2. 创建一个名为 main.go 的文件,内容如下。确保你修改 channels 导入以使用步骤 2 中设置的路径:

        package main

        import (
            "net/http"

            "github.com/agtorre/go-cookbook/chapter10/consensus"
        )

        func main() {
            consensus.Config(3)

            http.HandleFunc("/", consensus.Handler)
            err := http.ListenAndServe(":3333", nil)
            panic(err)
        }

  1. 运行 go run main.go 命令。或者,你也可以运行以下命令:
 go build
 ./example

通过运行前面的命令,你现在应该会看到以下输出:

 $ go run main.go
 2017/04/23 16:49:24 [INFO] raft: Node at 95c86c4c-9192-a8a6-  
      5e38-66c033bb3955 [Follower] entering Follower state (Leader:   
      "")
 2017/04/23 16:49:24 [INFO] raft: Node at 2406e36b-7e3e-0965- 
      8863-70a5dc1a2e69 [Follower] entering Follower state (Leader: 
      "")
 2017/04/23 16:49:24 [INFO] raft: Node at 2b5367e6-eea6-e195-  
      df40-1aeebfe8cdc7 [Follower] entering Follower state (Leader:   
      "")
 2017/04/23 16:49:25 [WARN] raft: Heartbeat timeout from ""   
      reached, starting election
 2017/04/23 16:49:25 [INFO] raft: Node at 2406e36b-7e3e-0965-  
      8863-70a5dc1a2e69 [Candidate] entering Candidate state
 2017/04/23 16:49:25 [DEBUG] raft: Votes needed: 2
 2017/04/23 16:49:25 [DEBUG] raft: Vote granted from 2406e36b-
      7e3e-0965-8863-70a5dc1a2e69\. Tally: 1
 2017/04/23 16:49:25 [DEBUG] raft: Vote granted from 95c86c4c-  
      9192-a8a6-5e38-66c033bb3955\. Tally: 2
 2017/04/23 16:49:25 [INFO] raft: Election won. Tally: 2
 2017/04/23 16:49:25 [INFO] raft: Node at 2406e36b-7e3e-0965-  
      8863-70a5dc1a2e69 [Leader] entering Leader state
 2017/04/23 16:49:25 [INFO] raft: pipelining replication to peer   
      95c86c4c-9192-a8a6-5e38-66c033bb3955
 2017/04/23 16:49:25 [INFO] raft: pipelining replication to peer   
      2b5367e6-eea6-e195-df40-1aeebfe8cdc7
 2017/04/23 16:49:25 [DEBUG] raft: Node 2406e36b-7e3e-0965-8863- 
      70a5dc1a2e69 updated peer set (2): [2406e36b-7e3e-0965-8863- 
      70a5dc1a2e69 95c86c4c-9192-a8a6-5e38-66c033bb3955 2b5367e6-
      eea6-e195-df40-1aeebfe8cdc7]
 2017/04/23 16:49:25 [DEBUG] raft: Node 95c86c4c-9192-a8a6-5e38-  
      66c033bb3955 updated peer set (2): [2406e36b-7e3e-0965-8863-  
      70a5dc1a2e69 95c86c4c-9192-a8a6-5e38-66c033bb3955 2b5367e6-
      eea6-e195-df40-1aeebfe8cdc7]
 2017/04/23 16:49:25 [DEBUG] raft: Node 2b5367e6-eea6-e195-df40- 
      1aeebfe8cdc7 updated peer set (2): [2406e36b-7e3e-0965-8863-  
      70a5dc1a2e69 95c86c4c-9192-a8a6-5e38-66c033bb3955 2b5367e6-  
      eea6-e195-df40-1aeebfe8cdc7]

  1. 在另一个终端中,运行以下命令:
 $ 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

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

当应用程序启动时,我们初始化多个 Raft 对象。每个对象都有自己的地址和传输。InmemTransport{} 函数还提供了一个连接其他传输的方法,称为 Connect()。一旦建立这些连接,Raft 集群就会进行选举。当与 Raft 集群通信时,客户端必须与领导者通信。在我们的情况下,一个处理器可以与所有节点通信,因此处理器负责让领导者 Raft 对象调用 Apply()。这反过来在所有其他节点上运行 apply()

此配方不处理快照,仅关注有限状态机(FSM)状态的变化。

InmemTransport{} 函数通过允许所有内容都驻留在内存中来简化选举和引导过程。在实践中,这除了测试和概念验证之外并不很有用,因为 go 线程可以自由访问共享内存。

使用 Docker 进行容器化

Docker 是一种用于打包和运输应用程序的容器技术。其他优点包括可移植性,容器将在宿主操作系统上以相同的方式运行。它提供了虚拟机的大部分优点,但更轻量级的容器。可以限制单个容器的资源消耗并沙盒化环境。对于在本地为应用程序创建一个通用环境以及将代码部署到生产环境时非常有用。Docker 用 Go 编写且是开源的,因此可以利用客户端和库。本配方将为基本的 Go 应用程序设置 Docker 容器,存储有关容器的某些版本信息,并演示从 Docker 端点调用处理器。

准备工作

根据以下步骤配置你的环境:

  1. 参考本章中 使用服务发现进行 Consul 配方的 准备工作 部分。

  2. store.docker.com/search?type=edition&offering=community安装 Docker。这将包括 Docker Compose。

如何做...

这些步骤涵盖了编写和运行你的应用程序:

  1. 在你的终端/控制台应用程序中,创建chapter10/docker目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter10/docker复制测试或使用这个练习来编写一些你自己的代码。

  3. 创建一个名为dockerfile的文件,内容如下:

        FROM alpine

        ADD ./example/example /example
        EXPOSE 8000
        ENTRYPOINT /example 

  1. 创建一个名为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 

  1. 创建一个名为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)
            }
        }

  1. 创建一个名为example的新目录并导航到它。

  2. 创建一个名为main.go的文件,内容如下。确保你修改了channels导入以使用你在步骤 2 中设置的路径:

        package main

        import (
            "fmt"
            "net/http"
            "strconv"
            "time"

            "github.com/agtorre/go-cookbook/chapter10/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))
        }

  1. 返回到起始目录。

  2. 运行以下命令:

 $ bash setup.sh

你现在应该看到以下输出:

 $ bash setup.sh
 ~/go/src/github.com/agtorre/go- 
      cookbook/chapter10/docker/example   
      ~/go/src/github.com/agtorre/go-cookbook/chapter10/docker
 ~/go/src/github.com/agtorre/go-cookbook/chapter10/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

  1. 运行以下命令:
 $ 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

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行go test。确保所有测试都通过。

它是如何工作的...

这个菜谱创建了一个脚本,用于编译 Linux 架构的 Go 二进制文件,并在main.go中设置各种私有变量。这些变量用于在版本端点上返回版本信息。一旦编译了二进制文件,就会创建一个包含二进制文件的 Docker 容器。这允许我们使用非常小的容器镜像,因为 Go 运行时在二进制文件中是自包含的。然后我们运行容器,同时暴露容器监听 HTTP 流量的端口。最后,我们在 localhost 上 curl 该端口,并看到返回的版本信息。

编排和部署策略

Docker 使得编排和部署变得更加简单。在这个菜谱中,我们将设置一个连接到 MongoDB 的连接,从 Docker 容器中插入文档并查询它。这个菜谱将设置与第五章中“所有关于数据库和存储”的使用 NoSQL 与 MongoDB 和 mgo菜谱相同的相同环境,但将在容器内运行应用程序和环境,并使用 Docker Compose 来编排和连接它们。这可以后来与 Docker Swarm 结合使用,这是一个集成的 Docker 工具,允许你管理一个集群,创建和部署可以轻松扩展或缩减的节点,并管理负载均衡(docs.docker.com/engine/swarm/)。容器编排的另一个好例子是 Kubernetes(kubernetes.io/),这是一个由 Google 使用 Go 编程语言编写的容器编排框架。

准备工作

根据以下步骤配置你的环境:

  1. 参考使用 Docker 进行容器化的准备工作部分。

  2. 执行 go get gopkg.in/mgo.v2 命令。

  3. 执行 go get github.com/tools/godep 命令。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 在你的终端/控制台应用程序中,创建 chapter10/orchestrate 目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter10/orchestrate 复制测试或使用此作为练习编写一些你自己的代码。

  3. 创建一个名为 dockerfile 的文件,内容如下:

        FROM golang:alpine

        ENV GOPATH /code/
        ADD . /code/src/github.com/agtorre/go-   
        cookbook/chapter10/docker
        WORKDIR /code/src/github.com/agtorre/go-    
        cookbook/chapter10/docker/example
        RUN go build

        ENTRYPOINT /code/src/github.com/agtorre/go-  
        cookbook/chapter10/docker/example/example

  1. 创建一个名为 docker-compose.yml 的文件,内容如下:
        version: '2'
        services:
         app:
         build: .
         mongodb:
         image: "mongo:latest"

  1. 创建一个名为 mongo.go 的文件,内容如下:
        package orchestrate

        import (
            "fmt"

            mgo "gopkg.in/mgo.v2"
            "gopkg.in/mgo.v2/bson"
        )

        // State is our data model
        type State struct {
            Name string `bson:"name"`
            Population int `bson:"pop"`
        }

        // ConnectAndQuery connects, inserts a document, then
        // queries it
        func ConnectAndQuery(session *mgo.Session) error {
            conn := session.DB("gocookbook").C("example")

            // we can inserts many rows at once
            if err := conn.Insert(&State{"Washington", 7062000}, 
            &State{"Oregon", 3970000}); err != nil {
                return err
            }

            var s State
            if err := conn.Find(bson.M{"name": "Washington"}).One(&s); 
            err!= nil {
                return err
            }
            fmt.Printf("State: %#v\n", s)
            return nil
        }

  1. 创建一个名为 example 的新目录并导航到它。

  2. 创建一个 main.go 文件,内容如下。确保你修改 orchestrate 导入以使用步骤 2 中设置的路径:

        package main

        import (
             "github.com/agtorre/go-cookbook/chapter10/orchestrate"
             mgo "gopkg.in/mgo.v2"
        )

        func main() {
            session, err := mgo.Dial("mongodb")
            if err != nil {
                panic(err)
            }
            if err := orchestrate.ConnectAndQuery(session); err != nil 
            {
                panic(err)
            }
        }

  1. 返回到起始目录。

  2. 执行 godep save ./... 命令。

  3. 执行 docker-compose up -d 命令。

  4. 执行 docker logs docker_app_1 命令。

你现在应该看到以下输出:

 $ docker logs docker_app_1
 State: docker.State{Name:"Washington", Population:7062000}

  1. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试都通过。

工作原理...

这种配置适合本地开发。一旦运行了 docker-compose up 命令,本地目录将被重建,它将使用最新版本建立与 MongoDB 实例的连接,并开始对其操作。这个菜谱使用 godeps 进行依赖管理,因此不需要通过 Dockerfile 文件挂载整个 GOPATH 环境变量。

这在开始需要连接到外部服务的应用程序时可以提供一个良好的基线,所有 第五章,关于数据库和存储的一切,都可以使用这种方法,而不是创建数据库的本地实例。对于生产环境,你可能不希望在 Docker 容器后面运行数据存储,但你通常也会有静态的主机名用于配置。

监控应用程序

监控 Go 应用程序有多种方法。其中一种最简单的方法是设置 Prometheus,这是一个用 Go 编写的监控应用程序 (prometheus.io)。这是一个根据你的配置文件轮询端点的应用程序,并收集大量关于你的应用程序的信息,包括 goroutine 的数量、内存使用情况等等。此应用程序将使用前一个菜谱中的技术来设置一个 Docker 环境,以托管 Prometheus 并连接到它。

准备工作

根据以下步骤配置你的环境:

  1. 参考使用 Docker 容器化的 Using containerization with Docker 菜谱中的 Getting ready 部分。

  2. 执行 go get github.com/prometheus/client_golang/prometheus/promhttp 命令。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建 chapter10/monitoring 目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter10/monitoring 复制测试或使用此作为练习编写一些自己的代码。

  3. 创建一个名为 Dockerfile 的文件,并包含以下内容:

        FROM golang:alpine

        ENV GOPATH /code/
        ADD . /code/src/github.com/agtorre/go-
        cookbook/chapter10/monitoring
        WORKDIR /code/src/github.com/agtorre/go-
        cookbook/chapter10/monitoring
        RUN go build

        ENTRYPOINT /code/src/github.com/agtorre/go-
        cookbook/chapter10/monitoring/monitoring

  1. 创建一个名为 docker-compose.yml 的文件,并包含以下内容:
        version: '2'
        services:
         app:
         build: .
         prometheus:
         ports: 
         - 9090:9090
         volumes: 
         - ./prometheus.yml:/etc/prometheus/prometheus.yml
         image: "prom/prometheus"

  1. 创建一个名为 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))
        }

  1. 创建一个名为 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']

  1. 运行 godep save ./... 命令。

  2. 运行 docker-compose up -d 命令。

你现在应该看到以下内容:

 $ docker-compose up
 Creating monitoring_app_1
 Creating monitoring_prometheus_1
 Attaching to monitoring_app_1, monitoring_prometheus_1
 prometheus_1 | time="2017-04-30T02:35:17Z" level=info 
      msg="Starting prometheus (version=1.6.1, branch=master,       
      revision=4666df502c0e239ed4aa1d80abbbfb54f61b23c3)" 
      source="main.go:88" 
 prometheus_1 | time="2017-04-30T02:35:17Z" level=info msg="Build       
      context (go=go1.8.1, user=root@7e45fa0366a7, date=20170419-
      14:32:22)" source="main.go:89" 
 prometheus_1 | time="2017-04-30T02:35:17Z" level=info 
      msg="Loading configuration file /etc/prometheus/prometheus.yml"       
      source="main.go:251" 
 prometheus_1 | time="2017-04-30T02:35:17Z" level=info 
      msg="Loading series map and head chunks..."      
      source="storage.go:421" 
 prometheus_1 | time="2017-04-30T02:35:17Z" level=info msg="0       
      series loaded." source="storage.go:432" 
 prometheus_1 | time="2017-04-30T02:35:17Z" level=info 
      msg="Starting target manager..." source="targetmanager.go:61" 
 prometheus_1 | time="2017-04-30T02:35:17Z" level=info 
      msg="Listening on :9090" source="web.go:259" 

  1. 将你的浏览器导航到 http://localhost:9090/。你应该能看到与你的应用程序相关的各种指标!

它是如何工作的...

Prometheus 客户端处理程序将返回有关你的应用程序的各种统计信息到 Prometheus 服务器。这允许你将多个 Prometheus 服务器指向应用程序,而无需重新配置或部署应用程序。大多数这些统计信息是通用的,对检测内存泄漏等事物有益。许多其他解决方案需要你定期向服务器发送信息。下一个配方 收集指标 将演示如何将自定义指标发送到 Prometheus 服务器。

收集指标

除了关于你的应用程序的一般信息之外,发出特定于应用程序的指标可能很有帮助。例如,我们可能想要收集时间数据或跟踪事件发生的次数。

此配方将使用 github.com/rcrowley/go-metrics 包来收集指标并通过端点公开它们。有各种导出工具可以将指标导出到 Prometheus 和 InfluxDB 等地方,这些工具也用 Go 编写。

准备就绪

根据以下步骤配置你的环境:

  1. 参考本章中 使用 Consul 进行服务发现 配方的 准备就绪 部分。

  2. 运行 go get github.com/rcrowley/go-metrics 命令。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建 chapter10/metrics 目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter10/metrics 复制测试或使用此作为练习编写一些自己的代码。

  3. 创建一个名为 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)
        }

  1. 创建一个名为 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)
            })
        }

  1. 创建一个名为 example 的新目录并导航到它。

  2. 创建一个名为 main.go 的文件,并包含以下内容。确保将 channels 导入修改为你在第二步中设置的路径:

        package main

        import (
            "net/http"

            "github.com/agtorre/go-cookbook/chapter10/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))
        }

  1. 运行 go run main.go。或者,你也可以运行以下命令:
 go build ./example

你现在应该看到以下内容:

 $ go run main.go
 listening on :8080

  1. 在单独的 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.ti
      mer":{"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,"mea
      n":60485,"mean.rate":1.1334543719787356,"median":60485,"min":6048
      5,"stddev":0}}

  1. 尝试多次访问所有端点,看看它们是如何变化的。

  2. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

Gometrics 将所有度量存储在一个注册表中。一旦设置好,你可以使用任何度量选项,例如计数器或计时器,它将这个更新存储在注册表中。有多个导出器可以将度量导出到第三方工具。在我们的案例中,我们设置了一个处理器,将所有度量以 JSON 格式导出。

我们设置了三个处理器——一个用于增加计数器,一个用于记录退出处理器的时间,还有一个用于打印报告(同时也会增加一个额外的计数器)。GetOrRegister 函数在原子操作中获取或创建一个度量发射器非常有用,如果它当前不存在于线程安全的方式中。或者,你也可以预先一次性注册所有内容。

第十一章:响应式编程和数据流

在本章中,我们将涵盖以下配方:

  • Goflow 用于数据流编程

  • 使用 RxGo 进行响应式编程

  • 使用 Sarama 与 Kafka

  • 使用 Kafka 的异步生产者

  • 将 Kafka 连接到 Goflow

  • 在 Go 中编写 GraphQL 服务器

简介

本章将讨论 Go 中的响应式编程设计模式。响应式编程是一种编程概念,它关注数据流和变化的传播(en.wikipedia.org/wiki/Reactive_programming)。像 Kafka 这样的技术允许你快速产生或消费数据流。因此,这些技术彼此之间是自然匹配的。在 将 Kafka 连接到 Goflow 的配方中,我们将探讨将 kafka 消息队列与 goflow 结合起来,以展示使用这些技术的实际示例。本章还将探讨与 Kafka 连接的各种方式,并使用它来处理消息。最后,本章将演示如何在 Go 中创建一个基本的 graphql 服务器。

Goflow 用于数据流编程

github.com/trustmaster/goflow 包对于创建基于数据流的应用程序很有用。它试图抽象概念,以便您可以使用自定义网络编写组件并将它们连接起来。这个配方将重新创建第八章 Testing 中讨论的应用程序,但它将使用 goflow 包来完成。

准备工作

根据以下步骤配置您的环境:

  1. golang.org/doc/install 下载并安装 Go 到您的操作系统上,并配置您的 GOPATH 环境变量。

  2. 打开终端/控制台应用程序。

  3. 导航到您的 GOPATH/src 并创建一个项目目录,例如,$GOPATH/src/github.com/yourusername/customrepo。所有代码都将从这个目录运行和修改。

  4. 可选地,使用 go get github.com/agtorre/go-cookbook/ 命令安装代码的最新测试版本。

  5. 运行 go get github.com/trustmaster/goflow 命令。

如何做到这一点...

这些步骤涵盖了编写和运行您的应用程序:

  1. 在您的终端/控制台应用程序中创建 chapter11/goflow 目录并进入它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter11/goflow 复制测试或将其用作练习来编写您自己的测试。

  3. 创建一个名为 components.go 的文件,内容如下:

        package goflow

        import (
            "encoding/base64"
            "fmt"
            flow "github.com/trustmaster/goflow"
        )

        // Encoder base64 encodes all input
        type Encoder struct {
            flow.Component
            Val <-chan string
            Res chan<- string
        }

        // OnVal does the encoding then pushes the result onto Re
        func (e *Encoder) OnVal(val string) {
            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 {
            flow.Component
            Line <-chan string
        }

        // OnLine Prints the current line received
        func (p *Printer) OnLine(line string) {
            fmt.Println(line)
        }

  1. 创建一个名为 network.go 的文件,内容如下:
        package goflow

        import flow "github.com/trustmaster/goflow"

        // EncodingApp creates a flow-based
        // pipeline to encode and print the
        // result
        type EncodingApp struct {
            flow.Graph
        }

        // NewEncodingApp wires together the components
        func NewEncodingApp() *EncodingApp {
            e := &EncodingApp{}
            e.InitGraphState()

            // define component types
            e.Add(&Encoder{}, "encoder")
            e.Add(&Printer{}, "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
        }

  1. 创建一个名为 example 的新目录并进入它。

  2. 创建一个名为 main.go 的文件,内容如下。确保将 goflow 导入修改为步骤 2 中设置的路径:

        package main

        import (
            "fmt"

            "github.com/agtorre/go-cookbook/chapter11/goflow"
            flow "github.com/trustmaster/goflow"
        )

        func main() {

            net := goflow.NewEncodingApp()

            in := make(chan string)
            net.SetInPort("In", in)

            flow.RunNet(net)

            for i := 0; i < 20; i++ {
                in <- fmt.Sprint("Message", i)
            }

            close(in)
            <-net.Wait()
        }

  1. 运行 go run main.go

  2. 您还可以运行以下命令:

 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

  1. 如果您复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

github.com/trustmaster/goflow 包通过定义一个网络/图、注册一些组件,然后将它们连接起来来工作。由于这些是用字符串描述的,所以这可能会感觉有点容易出错,但通常在运行时设置正确之前,这会在早期失败。

在这个食谱中,我们设置了两个组件,一个用于对传入的字符串进行 base64 编码,另一个用于打印传递给它的任何内容。我们将其连接到一个在 main.go 中初始化的输入通道,任何传递到该通道的内容都将通过我们的管道流动。

这种方法的重点很多是忽略正在发生的事情的内部。我们像对待一个连接的黑盒一样对待一切,让 goflow 做其余的工作。您可以从这个食谱中看到完成这个任务管道所需的代码有多小,以及我们控制工人数量的旋钮更少,等等。

使用 RxGo 进行响应式编程

ReactiveX (reactivex.io/) 是一个用于使用可观察流进行编程的 API。RxGo (github.com/reactivex/rxgo) 是一个库,用于在 Go 中支持这种模式。它帮助您将应用程序视为一个大的事件流,当这些事件发生时,它会以不同的方式做出响应。这个食谱将创建一个使用这种方法处理不同葡萄酒的应用程序。理想情况下,这种方法可以与葡萄酒数据或葡萄酒 API 相关联,并汇总有关葡萄酒的信息。

准备就绪

根据以下步骤配置您的环境:

  1. 参考本章中 Goflow for dataflow programming 食谱的 准备就绪 部分。

  2. 运行 go get github.com/reactivex/rxgo 命令。

如何做...

这些步骤涵盖了编写和运行您的应用程序:

  1. 从您的终端/控制台应用程序中,创建 chapter11/reactive 目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter11/reactive 复制测试或将其作为练习编写一些自己的测试。

  3. 创建一个名为 wine.go 的文件,内容如下:

        package reactive

        // Wine represents a bottle
        // of wine and is our
        // input stream
        type Wine struct {
            Name string
            Age int
            Rating float64 // 1-5
        }

        // GetWine returns an array of wines,
        // ages, and ratings
        func GetWine() interface{} {
            // some example wines
            w := []interface{}{
                Wine{"Merlot", 2011, 3.0},
                Wine{"Cabernet", 2010, 3.0},
                Wine{"Chardonnay", 2010, 4.0},
                Wine{"Pinot Grigio", 2009, 4.5},
            }
            return w
        }

        // Results holds a list of results by age
        type Results map[int]Result

        // Result is used for aggregation
        type Result struct {
            SumRating float64
            NumSamples int
        }

  1. 创建一个名为 exec.go 的文件,内容如下:
        package reactive

        import (
            "github.com/reactivex/rxgo/iterable"
            "github.com/reactivex/rxgo/observable"
            "github.com/reactivex/rxgo/observer"
            "github.com/reactivex/rxgo/subscription"
        )

        // Exec connects rxgo and returns
        // our results side-effect + a subscription
        // channel to block on at the end
        func Exec() (Results, <-chan subscription.Subscription) {
            results := make(Results)
            watcher := observer.Observer{
                NextHandler: func(item interface{}) {
                    wine, ok := item.(Wine)
                    if ok {
                        result := results[wine.Age]
                        result.SumRating += wine.Rating
                        result.NumSamples++
                        results[wine.Age] = result
                    }
                },
            }
            wine := GetWine()
            it, _ := iterable.New(wine)

            source := observable.From(it)
            sub := source.Subscribe(watcher)

            return results, sub
        }

  1. 创建一个名为 example 的新目录并导航到它。

  2. 创建一个名为 main.go 的文件,内容如下。确保您将 reactive 导入修改为在步骤 2 中设置的路径:

        package main

        import (
            "fmt"

            "github.com/agtorre/go-cookbook/chapter11/reactive"
        )

        func main() {
            results, sub := reactive.Exec()

            // wait for the channel to emit a Subscription
            <-sub

            // process results
            for key, val := range results {
                fmt.Printf("Age: %d, Sample Size: %d, Average Rating: 
                %.2f\n", key, val.NumSamples, 
                val.SumRating/float64(val.NumSamples))
            }
        }

  1. 运行 go run main.go

  2. 您还可以运行以下命令:

 go build ./example

您现在应该看到以下内容:

 $ go run main.go
 Age: 2011, Sample Size: 1, Average Rating: 3.00
 Age: 2010, Sample Size: 2, Average Rating: 3.50
 Age: 2009, Sample Size: 1, Average Rating: 4.50

  1. 如果您复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

RxGo 通过抽象源流(可以是数组或通道)来工作,允许你聚合流,并最终创建处理事件的观察者。这些可以处理错误或数据。RxGo 使用 interface{} 类型作为其参数,这样你可以传递任意值。因此,你必须使用反射来将传入的数据转换为正确的类型。如果你需要在观察者上返回错误,这可能很棘手。此外,增加的反射可能在性能上造成开销。

最后,你必须修改一些共享状态,无论是全局的还是局部闭包内的,这些状态将在最后使用。在我们的例子中,我们有一个 Results 类型,它是一个键为年份、值为总分和样本数量的映射。这使我们能够发出关于每年的平均值。如果我们使用酒名而不是类型,我们也可以按类型进行聚合。这个库还处于早期阶段。在许多方面,你可以使用基本的 Go 通道达到相同的效果。这有助于说明这些想法如何转化为 Go。

使用 Sarama 与 Kafka

Kafka 是一个流行的分布式消息队列,具有许多用于构建分布式系统的先进功能。本菜谱将展示如何使用同步生产者写入 Kafka 主题,以及如何使用分区消费者消费相同的主题。本菜谱不会探讨 Kafka 的不同配置,因为这是一个更广泛的话题,但我建议从 kafka.apache.org/intro 开始。

准备工作

根据以下步骤配置你的环境:

  1. 参考本章中 Goflow for dataflow programming 菜单的 Getting ready 部分。

  2. 使用以下步骤安装 Kafka:www.tutorialspoint.com/apache_kafka/apache_kafka_installation_steps.htm

  3. 或者,你也可以访问 github.com/spotify/docker-kafka

  4. 运行 go get gopkg.in/Shopify/sarama.v1 命令。

如何做到这一点...

这些步骤涵盖了编写和运行应用程序的过程:

  1. 从你的终端/控制台应用程序中创建 chapter11/synckafka 目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter11/synckafka 复制测试或将其作为练习来编写一些自己的测试。

  3. 确保 Kafka 在 localhost:9092 上运行。

  4. 在名为 consumer 的目录中创建一个名为 main.go 的文件,内容如下:

        package main

        import (
            "log"

            sarama "gopkg.in/Shopify/sarama.v1"
        )

        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)
            }
        }

  1. 在名为 producer 的目录中创建一个名为 main.go 的文件,内容如下:
        package main

        import (

           "fmt"
           "log"

            sarama "gopkg.in/Shopify/sarama.v1"
        )

        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))
            }
        }

  1. 运行 go run consumer/main.go

  2. 在另一个终端中运行 go run producer/main.go

  3. 在生产者终端中,你应该看到以下内容:

 $ go run producer/main.go 
 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

  1. 在消费者终端中,你应该看到以下内容:
 $ go run consumer/main.go 
 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

  1. 如果你复制或编写了自己的测试,向上导航一个目录并运行 go test。确保所有测试通过。

它是如何工作的...

这个食谱演示了通过 Kafka 传递简单消息。更复杂的方法应使用如 jsongobprotobuf 或其他序列化格式。生产者可以通过 sendMessage 同步地将消息发送到 Kafka。这并不很好地处理 Kafka 集群宕机的情况,可能会导致进程挂起。对于像网络处理器这样的应用程序来说,这是一个重要的考虑因素,因为它可能导致超时和对 Kafka 集群的硬依赖。

假设消息队列正确无误,我们的消费者将观察 Kafka 流并处理结果。本章前面的食谱可能已经使用此流进行一些额外的处理。

使用 Kafka 的异步生产者

在进行下一项任务之前,通常不需要等待 Kafka 生产者完成。在这种情况下,你可以使用异步生产者。这些生产者从 Sarama 消息通道接收消息,并具有返回可以单独检查的成功/错误通道的方法。

在这个食谱中,我们将创建一个 go 线程来处理成功和失败的消息,同时允许处理器在结果未知的情况下排队发送消息。

准备阶段

参考使用 Sarama 与 Kafka 的 准备阶段 部分。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建 chapter11/asyncsarama 目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter11/asyncsarama 复制测试或将其作为练习来编写你自己的测试。

  3. 确保 Kafka 在 localhost:9092 上运行。

  4. 从上一个食谱复制消费者目录。

  5. 创建一个名为 producer 的目录并导航到它。

  6. 创建一个名为 producer.go 的文件:

        package main

        import (
            "log"

            sarama "gopkg.in/Shopify/sarama.v1"
        )

        // 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)
                }
            }
        }

  1. 创建一个名为 handler.go 的文件:
        package main

        import (
            "net/http"

            sarama "gopkg.in/Shopify/sarama.v1"
        )

        // 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(r.FormValue("msg"))}
            w.WriteHeader(http.StatusOK)
        }

  1. 创建一个名为 main.go 的文件:
        package main

        import (
            "fmt"
            "net/http"

            sarama "gopkg.in/Shopify/sarama.v1"
        )

        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))
        }

  1. 运行 go build 命令。

  2. 向上导航一个目录。

  3. 运行 go run consumer/main.go

  4. 在同一目录下的另一个终端中,运行 ./producer/producer

  5. 在第三个终端中,运行以下命令:

 $ curl "http://localhost:3333/?msg=this" 
 $ curl "http://localhost:3333/?msg=is" 
 $ curl "http://localhost:3333/?msg=an" 
 $ curl "http://localhost:3333/?msg=example" 

在生产者终端中,你应该看到以下内容:

 $ ./producer/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

  1. 在消费者终端中,你应该看到以下内容:
 $ go run consumer/main.go 
 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

  1. 如果你复制或编写了自己的测试,向上导航一个目录并运行 go test。确保所有测试通过。

它是如何工作的...

本章的所有修改都是针对生产者进行的。这次,我们创建了一个单独的 go 线程来处理成功和错误。如果这些没有被处理,你的应用程序将发生死锁。接下来,我们将我们的生产者附加到处理器,并在处理器通过 GET 调收到的消息时在其上发出消息。

无论响应如何,处理程序在发送消息后会立即返回成功。如果这不可接受,应使用同步方法。在我们的情况下,我们接受稍后分别处理成功和错误。

最后,我们使用几个不同的消息 curl 我们的端点,您可以看到它们从处理程序流向我们之前章节中编写的 Kafka 消费者最终打印的地方。

将 Kafka 连接到 Goflow

这个配方将结合一个 Kafka 消费者和一个 Goflow 管道。随着我们的消费者从 Kafka 接收消息,它将对它们运行strings.ToUpper(),然后打印结果。这些自然地配对,因为 Goflow 被设计为在传入的流上操作,这正是 Kafka 为我们提供的。

准备就绪

参考配方使用 Sarama 的 Kafka中的准备就绪部分.

如何做到这一点...

这些步骤涵盖了编写和运行您的应用程序:

  1. 从您的终端/控制台应用程序中,创建chapter11/kafkaflow目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter11/kafkaflow复制测试或将其用作练习来编写一些自己的测试。

  3. 确保 Kafka 在localhost:9092上运行。

  4. 创建一个名为components.go的文件,内容如下:

        package kafkaflow

        import (
            "fmt"
            "strings"

            flow "github.com/trustmaster/goflow"
        )

        // Upper upper cases the incoming
        // stream
        type Upper struct {
            flow.Component
            Val <-chan string
            Res chan<- string
        }

        // OnVal does the encoding then pushes the result onto Re
        func (e *Upper) OnVal(val string) {
            e.Res <- strings.ToUpper(val)
        }

        // Printer is a component for printing to stdout
        type Printer struct {
            flow.Component
            Line <-chan string
        }

        // OnLine Prints the current line received
        func (p *Printer) OnLine(line string) {
            fmt.Println(line)
        }

  1. 创建一个名为network.go的文件,内容如下:
        package kafkaflow

        import flow "github.com/trustmaster/goflow"

        // UpperApp creates a flow-based
        // pipeline to upper case and print the
        // result
        type UpperApp struct {
            flow.Graph
        }

        // NewUpperApp wires together the compoents
        func NewUpperApp() *UpperApp {
            u := &UpperApp{}
            u.InitGraphState()

            u.Add(&Upper{}, "upper")
            u.Add(&Printer{}, "printer")

            u.Connect("upper", "Res", "printer", "Line")
            u.MapInPort("In", "upper", "Val")

            return u
        }

  1. 在名为consumer的目录中创建一个名为main.go的文件,内容如下:
        package main

        import (
            "github.com/agtorre/go-cookbook/chapter11/kafkaflow"
            flow "github.com/trustmaster/goflow"
            sarama "gopkg.in/Shopify/sarama.v1"
        )

        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)

            flow.RunNet(net)
            defer func() {
                close(in)
                <-net.Wait()
            }()

            for {
                msg := <-partitionConsumer.Messages()
                in <- string(msg.Value)
            }
        }

  1. 使用 Sarama 的 Kafka配方复制消费者目录。

  2. 运行go run consumer/main.go

  3. 在另一个终端中运行go run producer/main.go

  4. 在生产者终端中,您现在应该看到以下内容:

 $ go run producer/main.go 
 go run producer/main.go !3300
 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/main.go 
 MESSAGE 0
 MESSAGE 1
 MESSAGE 2
 MESSAGE 3
 MESSAGE 4
 MESSAGE 5
 MESSAGE 6
 MESSAGE 7
 MESSAGE 8
 MESSAGE 9

  1. 如果您复制或编写了自己的测试,请向上移动一个目录并运行go test。确保所有测试都通过。

它是如何工作的...

这个配方结合了本章中先前配方中的想法。像之前的配方一样,我们设置了一个 Kafka 消费者和生产者。这个配方使用了使用 Sarama 的 Kafka配方中的同步生产者,但也可以使用异步生产者。一旦收到消息,我们就像在Goflow 数据流编程配方中做的那样,将其入队到输入通道中。我们修改了这个配方中的组件,将传入的字符串转换为大写,而不是进行 base64 编码。我们重用了打印组件,并且最终的网络配置相似。

最终结果是,通过 Kafka 消费者接收的所有消息都被传输到我们的基于流的作业管道中进行操作。这使我们能够将管道组件模块化和可重用,我们可以在不同的配置中使用相同的组件多次。同样,我们将接收任何写入 Kafka 的生产者的流量,因此我们可以将生产者多路复用到单个数据流中。

使用 Go 编写 GraphQL 服务器

GraphQL 是 Facebook 创建的 REST 的替代品 (graphql.org/)。这项技术允许服务器实现并发布一个模式,然后客户端可以请求他们所需的信息,而不是理解和利用各种 API 端点。

对于这个菜谱,我们将创建一个 Graphql 模式,它代表一副扑克牌。我们将公开一个资源卡片,可以根据花色和值进行过滤。如果没有指定参数,它还可以返回牌组中的所有卡片。

准备就绪

根据以下步骤配置你的环境:

  1. 参考本章中 Goflow for dataflow programming 菜单的 准备就绪 部分。

  2. 运行 go get github.com/graphql-go/graphql 命令。

如何做...

这些步骤涵盖了编写和运行你的应用程序:

  1. 在你的终端/控制台应用程序中创建 chapter11/graphql 目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter11/graphql 复制测试或将其作为练习编写一些自己的测试。

  3. 创建并导航到 cards 目录。

  4. 创建一个名为 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"},
            }
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 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
        }

  1. 返回到 graphql 目录。

  2. 创建一个名为 example 的新目录并导航到它。

  3. 创建一个名为 main.go 的文件,内容如下。确保你修改 cards 导入以使用步骤 2 中设置的路径:

        package main

        import (
            "encoding/json"
            "fmt"
            "log"

            "github.com/agtorre/go-cookbook/chapter11/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)
        }

  1. 运行 go run main.go

  2. 你也可以运行以下命令:

 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"
 }
 ]
 }
 } 

  1. 测试一些额外的查询,例如以下内容:

    • cards(suit: "Spades")

    • cards(value: "3", suit:"Diamonds")

  2. 如果你复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

cards.go 文件定义了一个 card 对象,并在名为 cards 的全局变量中初始化基础牌组。这种状态也可以存储在长期存储中,如数据库。然后我们在 types.go 中定义 CardType,允许 graphql 将卡片对象解析为响应。接下来,我们跳转到 resolve.go,在那里我们定义如何根据值和类型过滤卡片。这个 Resolve 函数将被最终的模式使用,该模式在 schema.go 中定义。

例如,你可能需要修改这个菜谱中的 Resolve 函数以从数据库中检索数据。最后,我们加载模式并对它运行查询。将我们的模式挂载到 REST 端点是一个小的修改,但为了简洁,这个菜谱只是运行了一个硬编码的查询。有关 GraphQL 查询的更多信息,请访问 graphql.org/learn/queries/

第十二章:服务器端编程

在本章中,我们将介绍以下食谱:

  • 使用 Apex 在 Lambda 上进行 Go 编程

  • Apex 服务器端无服务器日志和指标

  • 使用 Go 在 Google App Engine 上

  • 使用 zabawaba99/firego 与 Firebase 一起工作

简介

本章将重点介绍无服务器架构以及使用 Go 语言。它还将探索应用引擎和 Firebase,这两个服务可以快速将应用程序和数据存储部署到网络上。

本章中的所有食谱都涉及第三方服务,这些服务按使用情况收费;确保你在使用完毕后清理。否则,将这些食谱视为在这些平台上启动更大应用程序的启动器。

使用 Apex 在 Lambda 上进行 Go 编程

Apex 是一个用于构建、部署和管理 AWS Lambda 函数的工具。它为 Go 提供了包装器(使用 Node.js 模拟器)。目前,没有这样的模拟器就无法在 Lambda 上运行原生 Go 代码。本食谱将探索创建 Go Lambda 函数并使用 Apex 部署它们。

准备工作

根据以下步骤配置你的环境:

  1. 在你的操作系统上下载并安装 Go (golang.org/doc/install) 并配置你的 GOPATH 环境变量。

  2. 打开一个终端/控制台应用程序。

  3. 导航到你的 GOPATH/src 目录并创建一个项目目录,例如,$GOPATH/src/github.com/yourusername/customrepo。所有代码都将从这个目录运行和修改。

  4. 可选地,使用以下步骤安装代码的最新测试版本:

    go get github.com/agtorre/go-cookbook/... 命令。

  5. apex.run/ 安装 Apex。

  6. 运行 go get github.com/apex/go-apex 命令。

如何做...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建 chapter12/lambda 目录并导航到它。

  2. 创建一个 Amazon 账户和一个 IAM 角色,可以编辑 Lambda 函数,这可以从 aws.amazon.com/lambda/ 完成。

  3. 创建一个名为 ~/.aws/credentials 的文件,内容如下,从你在 Amazon 控制台中设置的凭证中复制:

        [example]
        aws_access_key_id = xxxxxxxx
        aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxxxx

  1. 创建一个环境变量来保存你想要的区域:
        export AWS_REGION=us-west-2

  1. 运行 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

  1. 删除 lambda/functions/hello 目录。

  2. 创建一个名为 lambda/functions/greeter/main.go 的新文件,内容如下:

        package main

        import (
            "encoding/json"
            "fmt"

            "github.com/apex/go-apex"
        )

        type message struct {
            Name string `json:"name"`
        }

        func main() {
            apex.HandleFunc(func(event json.RawMessage, ctx 
            *apex.Context) (interface{}, error) {
                var m message
                if err := json.Unmarshal(event, &m); err != nil {
                    return nil, err
                }

                resp := map[string]string{
                    "greeting": fmt.Sprintf("Hello, %s", m.Name),
                }

                return resp, nil
            })
        }

  1. 要测试你的函数,你可以运行以下命令:
 $ echo '{"event":{"name": "test"}}' | go run 
 functions/greeter1/main.go 

 {"value":{"greeting":"Hello, test"}}

  1. 将其部署到指定的区域:
 $apex deploy
 • creating function env= function=greeter
 • created alias current env= function=greeter version=1
 • function created env= function=greeter name=go-
 cookbook_greeter1 version=1

  1. 要调用它,请运行以下命令:
 $ echo '{"name": "test"}' | apex invoke greeter
 {"greeting":"Hello, test"}

  1. 现在修改 lambda/functions/greeter/main.go
        package main

        import (
            "encoding/json"
            "fmt"

            "github.com/apex/go-apex"
        )

        type message struct {
            FirstName string `json:"first_name"`
            LastName string `json:"last_name"`
        }

        func main() {
            apex.HandleFunc(func(event json.RawMessage, ctx 
            *apex.Context) (interface{}, error) {
                var m message
                if err := json.Unmarshal(event, &m); err != nil {
                    return nil, err
                }

                resp := map[string]string{
                    "greeting": fmt.Sprintf("Hello, %s %s", 
                    m.FirstName, m.LastName),
                }

                return resp, nil
            })
        }

  1. 重新部署,创建版本 2:
 $ apex deploy 
 • creating function env= function=greeter
 • created alias current env= function=greeter version=2
 • function created env= function=greeter name=go-
 cookbook_greeter1 version=2

  1. 调用新部署的函数:
 $ echo '{"first_name": "Go", "last_name": "Coders"}' | apex 
      invoke greeter2
 {"greeting":"Hello, Go Coders"}

  1. 查看日志:
 $ apex logs greeter
 apex logs greeter
 /aws/lambda/go-cookbook_greeter START RequestId: 7c0f9129-3830-
 11e7-8755-75aeb52a51b9 Version: 2
 /aws/lambda/go-cookbook_greeter END RequestId: 7c0f9129-3830-
 11e7-8755-75aeb52a51b9
 /aws/lambda/go-cookbook_greeter REPORT RequestId: 7c0f9129-3830-
 11e7-8755-75aeb52a51b9 Duration: 93.84 ms Billed Duration: 100 ms 
 Memory Size: 128 MB Max Memory Used: 19 MB 

  1. 清理已部署的服务:
 $ apex delete
 The following will be deleted:

 - greeter

 Are you sure? (yes/no) yes
 • deleting env= function=greeter
 • function deleted env= function=greeter

它是如何工作的...

AWS Lambda 使你能够按需运行函数而无需维护服务器。Apex 提供了部署、版本控制和测试函数的设施,当你将它们发送到 Lambda 时。它还提供了一个允许我们执行任意 Go 代码的适配器。这是通过定义一个处理程序、处理传入的请求有效载荷并返回一个响应来实现的,这与标准网络处理程序的流程非常相似。

在这个菜谱中,我们最初接收一个名字输入并问候这个名字。后来,我们利用版本控制将名字拆分为首名和姓氏。也可以部署一个单独的功能。还可以使用 apex rollback greeter 进行回滚。

Apex 无服务器日志和指标

当与 Lambda 等无服务器函数一起工作时,拥有可移植的、结构化的日志非常有价值。此外,你还可以将处理日志的早期菜谱与此菜谱结合。本章中涵盖的 第四章 中关于 Go 中的错误处理的菜谱同样相关。因为我们使用 Apex 来处理我们的 Lambda 函数,所以我们选择使用 Apex 日志记录器来完成这个菜谱。我们还将依赖 Apex 提供的指标以及 AWS 控制台。早期的菜谱探讨了更复杂的日志和指标示例,这些仍然适用——Apex 日志记录器可以轻松配置,以便使用类似 Amazon Kinesis 或 Elasticsearch 这样的工具聚合日志。

准备工作

根据以下步骤配置你的环境:

  1. 参考本章中 Go 在 Lambda 上使用 Apex 编程 菜谱的 准备工作 部分。

  2. 运行 go get github.com/apex/log 命令。

如何完成...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建 chapter12/logging 目录并导航到它。

  2. 创建一个可以编辑 Lambda 函数的 Amazon 账户和 IAM 角色,这可以在 aws.amazon.com/lambda/ 完成。

  3. 创建一个包含以下内容的 ~/.aws/credentials 文件,从你在亚马逊控制台设置的内容中复制你的凭证:

        [example]
        aws_access_key_id = xxxxxxxx
        aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxxxx

  1. 创建一个环境变量来保存你期望的区域:
        export AWS_REGION=us-west-2

  1. 运行 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

  1. 删除 lambda/functions/hello 目录。

  2. 创建一个名为 lambda/functions/secret/main.go 的新文件,并包含以下内容:

        package main

        import (
            "encoding/json"
            "os"

            "github.com/apex/go-apex"
            "github.com/apex/log"
            "github.com/apex/log/handlers/text"
        )

        // Input takes in a secret
        type Input struct {
            Secret string `json:"secret"`
        }

        func main() {
            apex.HandleFunc(func(event json.RawMessage, ctx 
            *apex.Context) (interface{}, error) {
                log.SetHandler(text.New(os.Stderr))

                var input Input
                if err := json.Unmarshal(event, &input); err != nil {
                    log.WithError(err).Error("failed to unmarshal key 
                    input")
                    return nil, err
                }
                log.WithField("secret", input.Secret).Info("secret 
                guessed")

                if input.Secret == "klaatu barada nikto" {
                    return "secret guessed!", nil
                }
                return "try again", nil
            })
        }

  1. 将其部署到指定的区域:
 $ 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

  1. 要调用它,请运行以下命令:
 $ echo '{"secret": "open sesame"}' | apex invoke secret
 "try again"

 $ echo '{"secret": "open sesame"}' | apex invoke secret
 "secret guessed!"

  1. 检查日志:
 $ 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 

  1. 检查你的指标:
 $ apex metrics secret !3445

 secret
 total cost: $0.00
 invocations: 0 ($0.00)
 duration: 0s ($0.00)
 throttles: 0
 errors: 0
 memory: 128

  1. 清理已部署的服务:
 $ apex delete
 Are you sure? (yes/no) yes
 • deleting env= function=secret
 • function deleted env= function=secret

它是如何工作的...

在这个菜谱中,我们创建了一个新的 Lambda 函数,名为 secret,它会响应你是否猜对了秘密短语。该函数解析传入的 JSON 请求,使用 Stderr 进行一些日志记录,并返回一个响应。

使用该功能几次后,我们发现可以使用 apex logs 命令查看日志。此命令可以在单个 lambda 函数或所有我们的管理函数上运行。如果您正在将 Apex 命令链接在一起并希望跨多个服务查看日志,这特别有用。

此外,我们展示了如何使用 apex metrics 命令收集有关您的应用程序的一般指标,包括成本和调用次数。您还可以在 AWS 控制台的 Lambda 部分直接查看大量此类信息。与其他菜谱一样,我们试图在结束时清理。

使用 Go 运行 Google App Engine

App Engine 是一个 Google 服务,它简化了快速部署 Web 应用程序。这些应用程序可以访问云存储和各种其他 Google API。一般思路是 App Engine 将会轻松地根据负载进行扩展,并简化与托管应用程序相关的任何操作管理。本菜谱将展示如何创建和可选部署一个基本的 App Engine 应用程序。本菜谱不会涉及设置 Google 云账户、设置计费或清理实例的细节。至少,需要访问 Google Cloud Datastore (cloud.google.com/datastore/docs/concepts/overview) 才能使本菜谱工作。

准备就绪

根据以下步骤配置您的环境:

  1. golang.org/doc/install 下载并安装 Go 到您的操作系统上,并配置 GOPATH 环境变量。

  2. 打开终端/控制台应用程序。

  3. 导航到您的 GOPATH/src 并创建一个项目目录,例如,$GOPATH/src/github.com/yourusername/customrepo。所有代码都将从这个目录运行和修改。

  4. 可选地,使用 go get github.com/agtorre/go-cookbook/... 命令安装代码的最新测试版本。

  5. cloud.google.com/appengine/docs/flexible/go/quickstart 下载 Google Cloud SDK。

  6. 创建一个允许部署和访问数据存储的应用程序,并记录应用程序名称。

  7. 执行 go get cloud.google.com/go/datastore 命令。

  8. 执行 go get google.golang.org/appengine 命令。

如何操作...

这些步骤涵盖了编写和运行您的应用程序:

  1. 在您的终端/控制台应用程序中,创建 chapter12/appengine 目录并导航到它。

  2. https://github.com/agtorre/go-cookbook/tree/master/chapter12/appengine 复制测试用例,或者将其作为练习编写一些自己的测试用例。

  3. 创建一个名为 app.yml 的文件,并包含以下内容,将 go-cookbook 替换为 准备就绪 部分中创建的应用程序的名称:

        runtime: go
        env: flex

        #[START env_variables]
        env_variables:
            GCLOUD_DATASET_ID: go-cookbook
        #[END env_variables]

  1. 创建一个名为 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 := &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, &messages)
            return messages, err
        }

  1. 创建一个名为 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)
            }

            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, fmt.Sprintf("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)
            }
        }

  1. 创建一个名为 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)
            appengine.Main()
        }

  1. 运行gcloud config set project go-cookbook命令,其中go-cookbook是你准备工作部分中创建的项目。

  2. 运行gcloud auth application-default login命令并遵循指示。

  3. 运行export PORT=8080命令。

  4. 运行export GCLOUD_DATASET_ID=go-cookbook命令,其中go-cookbook是你准备工作部分中创建的项目。

  5. 运行go build命令。

  6. 运行./example命令。

  7. 导航到localhost:8080/?message=hello%20there

  8. 尝试发送几条更多的消息(?message=other

  9. 可选地,使用gcloud app deploy将应用程序部署到你的实例上。

  10. 使用gcloud app browse导航到已部署的应用程序。

  11. 清理你的 appengine 实例和数据存储:

  12. 如果你复制或编写了自己的测试,运行go test命令。确保所有测试都通过。

它是如何工作的...

一旦云 SDK 配置为指向你的应用程序并经过认证,GCloud 工具允许快速部署和配置,使本地应用程序能够访问 Google 服务。

在认证和设置端口后,我们在 localhost 上运行应用程序,然后我们可以开始使用代码。应用程序定义了一个可以存储和从数据存储中检索的消息对象。这展示了你可能如何隔离这类代码。你还可以使用如前几章所示的数据存储/数据库接口。

接下来,我们设置一个处理程序,尝试将消息插入到数据存储中,然后检索所有消息,并在浏览器中显示它们。这创建了一个类似基本留言簿的东西。你可能注意到消息并不总是立即出现。如果你没有消息参数进行导航或发送另一条消息,它应该在重新加载时出现。

最后,确保如果你不再使用它们,清理实例。

使用 zabawaba99/firego 与 Firebase 一起工作

Firebase 是另一个 Google 云服务,它创建了一个可扩展、易于管理的数据库,可以支持身份验证,并且与移动应用程序配合得非常好。该服务提供的功能远不止本食谱中涵盖的内容,但我们将探讨如何存储数据、读取数据、修改数据以及恢复数据。我们还将探讨如何为你的应用程序设置身份验证,并用我们自己的自定义客户端包装 Firebase 客户端。

准备工作

根据以下步骤配置你的环境:

  1. golang.org/doc/installand下载并安装 Go 到你的操作系统上,并配置你的GOPATH环境变量。

  2. 打开一个终端/控制台应用程序。

  3. 导航到你的 GOPATH/src 并创建一个项目目录,例如,$GOPATH/src/github.com/yourusername/customrepo。所有代码都将从这个目录运行和修改。

  4. 可选地,使用 go get github.com/agtorre/go-cookbook/... 命令安装代码的最新测试版本。

  5. console.firebase.google.com/ 创建一个账户和数据库。

  6. console.firebase.google.com/project/go-cookbook/settings/serviceaccounts/adminsdk 生成一个服务管理员令牌。

  7. 将下载的令牌移动到 /tmp/service_account.json

如何做到这一点...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序创建 chapter12/firebase 目录并进入它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter12/firebase 复制测试代码,或者将其作为练习编写一些自己的代码。

  3. 创建一个名为 client.go 的文件,并包含以下内容:

        package firebase

        import (
            "log"

            "gopkg.in/zabawaba99/firego.v1"
        )

        // Client Interface for mocking
        type Client interface {
            Get() (map[string]interface{}, error)
            Set(key string, value interface{}) error
        }
        type firebaseClient struct {
            *firego.Firebase
        }

        func (f *firebaseClient) Get() (map[string]interface{}, error) 
        {
            var v2 map[string]interface{}
            if err := f.Value(&v2); err != nil {
                log.Fatalf("error getting")
            }
            return v2, nil
        }

        func (f *firebaseClient) Set(key string, value interface{}) 
        error {
            v := map[string]interface{}{key: value}
            if err := f.Firebase.Set(v); err != nil {
                return err
            }
            return nil
        }

  1. 创建一个名为 auth.go 的文件,并包含以下内容。调整 go-cookbook.firebaseio.com 以匹配你的应用程序名称:
        package firebase

        import (
            "io/ioutil"

            "golang.org/x/oauth2"
            "golang.org/x/oauth2/google"
            "gopkg.in/zabawaba99/firego.v1"
        )

        // 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() (Client, error) {
            d, err := ioutil.ReadFile("/tmp/service_account.json")
            if err != nil {
                return nil, err
            }

            conf, err := google.JWTConfigFromJSON(d, 
            "https://www.googleapis.com/auth/userinfo.email",
            "https://www.googleapis.com/auth/firebase.database")
            if err != nil {
                return nil, err
            }
            f := firego.New("https://go-cookbook.firebaseio.com", 
            conf.Client(oauth2.NoContext))
            return &firebaseClient{f}, err
        }

  1. 创建一个名为 example 的新目录并进入它。

  2. 创建一个名为 main.go 的文件,并包含以下内容。确保你修改 channels 导入以使用步骤 2 中设置的路径:

        package main

        import (
            "fmt"
            "log"

            "github.com/agtorre/go-cookbook/chapter12/firebase"
        )

        func main() {
            f, err := firebase.Authenticate()
            if err != nil {
                log.Fatalf("error authenticating")
            }
            f.Set("key", []string{"val1", "val2"})
            res, _ := f.Get()
            fmt.Println(res)

            vals := res["key"].([]interface{})
            vals = append(vals, map[string][]string{"key2": 
            []string{"val3"}})
            f.Set("key", vals)
            res, _ = f.Get()
            fmt.Println(res)
        }

  1. 运行 go run main.go

  2. 你也可以运行 go build ./example

你现在应该看到以下输出:

 $ go run main.go
 map[key:[val1 val2]]
 map[key:[val1 val2 map[key2:[val3]]]]

  1. 如果你复制或编写了自己的测试,向上移动一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

Firebase 使用 OAuth2 进行身份验证。在这种情况下,我们下载了一个凭证文件,可以与适当的范围请求一起使用,以返回可能与 Firebase 数据库一起工作的令牌。我们可以存储任何类型的结构化类似映射的对象。在这种情况下,我们存储 map[string]interface{}

客户端代码将所有操作封装在一个接口中,以便于测试。这在编写客户端代码时是一个常见的模式,在其他食谱中也被使用。

第十三章:性能改进、技巧和窍门

在本章中,我们将介绍以下菜谱:

  • 加快编译和测试周期

  • 使用 pprof 工具

  • 基准测试和查找瓶颈

  • 内存分配和堆管理

  • 依赖项和项目布局

  • 使用 fasthttprouter 和 fasthttp

简介

本章将专注于优化应用程序,发现瓶颈,以及依赖项的 vendoring。这些是一些可以立即用于现有应用程序的技巧和窍门。如果你或你的组织需要完全可重复构建,这些菜谱是必要的。当你想基准测试应用程序的性能时,它们也非常有用。最后的菜谱专注于提高 HTTP 的速度,然而,始终要记住,网络世界变化迅速,了解最佳实践很重要。例如,如果你需要 HTTP/2,从版本 1.6 开始,可以使用内置的 Go net/http包来使用它。

加快编译和测试周期

有几个原因可能导致应用程序编译缓慢,进而运行测试也缓慢。通常,这是需要应用程序每次从头开始编译(没有中间构建)、大型代码库和许多依赖的组合。这个菜谱将探讨一些可以用来查看当前依赖列表并加快编译速度的工具。

准备工作

通过以下步骤配置你的环境:

  1. golang.org/doc/install下载并安装 Go 到你的操作系统,并配置你的GOPATH环境变量。

  2. 打开一个终端/控制台应用程序。

  3. 导航到你的GOPATH/src目录并创建一个项目目录,例如,$GOPATH/src/github.com/yourusername/customrepo

所有代码都将从这个目录运行和修改。

  1. 可选地,使用go get github.com/agtorre/go-cookbook/命令安装代码的最新测试版本。

如何做...

这些步骤涵盖了编写和运行你的应用程序:

  1. 为了演示 go 构建性能如何退化,你可以通过运行rm -rf $GOPATH/pkg/命令来删除你的pkg目录,或者为这个菜谱设置一个新的GOPATH。确保$GOPATH已设置。

  2. 通过运行cd $GOPATH/src/github.com/agtorre/go-cookbook/chapter6/grpc/server命令来构建github.com/agtorre/go-cookbook/chapter6/grpc/server包。

  3. 运行time go build命令:

 $ time go build .
 go build 4.10s user 0.59s system 181% cpu 2.580 total

  1. 使用以下命令测试github.com/agtorre/go-cookbook/chapter6/grpc/server包:
 $ time go test
 PASS
 ok github.com/agtorre/go-cookbook/chapter6/grpc/server 0.014s
 go test 4.01s user 0.60s system 176% cpu 2.608 total

  1. 探索导致 4 秒构建的原因;看起来并不是我们项目的大小:
 $ wc -l *.go
 25 greeter.go
 44 greeter_test.go
 20 server.go
 89 total

  1. 列出所有导入:
 $ go list -f '{{ join .Imports "\n"}}'
 fmt
 github.com/agtorre/go-cookbook/chapter6/grpc/greeter
 golang.org/x/net/context
 google.golang.org/grpc
 net

 $go list -f '{{ join .Imports "\n"}}' github.com/agtorre/go-
 cookbook/chapter6/grpc/greeter
 fmt
 github.com/golang/protobuf/proto
 golang.org/x/net/context
 google.golang.org/grpc
 math

  1. 列出依赖项;检查数量。注意与空main.go文件相比的差异:
 $ go list -f '{{ join .Deps "\n"}}' . 
 .
 .
 .
 google.golang.org/grpc
 google.golang.org/grpc/codes
 google.golang.org/grpc/credentials
 google.golang.org/grpc/grpclog
 google.golang.org/grpc/internal
 google.golang.org/grpc/metadata
 google.golang.org/grpc/naming
 google.golang.org/grpc/peer
 google.golang.org/grpc/stats
 google.golang.org/grpc/tap
 google.golang.org/grpc/transport
 .
 .
 .

 $ go list -f '{{ join .Deps "\n"}}' . | wc -l 
 111

 $ go list -f '{{ join .Deps "\n"}}' /path/to/empty/main/package | 
 wc -l
 4

  1. 加速:
 $ cd $GOPATH/src/github.com/agtorre/go-
 cookbook/chapter6/grpc/server
 $ go install ./...
 $ go test -i ./...

  1. 再次尝试运行以下命令:
 $ time go build .
 go build . 0.65s user 0.15s system 117% cpu 0.683 total

 $ time go test .
 ok github.com/agtorre/go-cookbook/chapter6/grpc/server 0.015s
 go test . 0.63s user 0.17s system 118% cpu 0.669 total

它是如何工作的...

如果你遇到 Go 编译速度慢的问题,有几个方面需要考虑。首先,Go 1.5 是第一个完全用 Go 编写的 Go 编译器。这带来了编译时间的显著增加,并且从那时起,每个版本都对此进行了改进。如果你使用的是 Go 1.5 或更高版本,你的第一步应该是升级到最新的 Go 版本。

接下来,对依赖关系的分析可能至关重要。一些 Go 包有较大的依赖关系变化,你可能会在不知情的情况下通过单个导入添加数十万行代码。分析你的依赖关系是值得的。这可以通过 Go list 工具实现,但也有一些第三方工具,包括新的 dep (github.com/golang/dep)、godep (github.com/tools/godep)和 glide (github.com/Masterminds/glide),以及大多数供应商仓库都会将所有依赖项放在供应商目录中。

最后,保存库的中间构建版本通常会带来显著的提升。这可以通过go install ./...go test -i ./...命令实现,这些命令将在pkg目录中创建工件。install命令为导入的包执行此操作,而go test -i为测试包执行相同的操作。如果你使用的是goconvey这样的框架,这可能会很有用。

使用 pprof 工具

pprof 工具允许 Go 应用程序收集和导出运行时分析数据。它还提供了通过 Web 界面访问工具的 Web 钩子。这个方案将创建一个基本的应用程序,用于验证 bcrypt 散列密码与明文密码是否匹配,然后分析该应用程序。

你可能期望在第十章“分布式系统”中找到 pprof 工具,以及其他指标和监控方案。然而,它被放在了这一章,因为它将用于分析和改进程序,这与基准测试的使用方式非常相似。因此,这个方案将主要关注 pprof 在分析和改进应用程序内存使用方面的应用。

准备工作

通过执行以下步骤来配置你的环境:

  1. 参考本章中“加快编译和测试周期”方案的准备工作部分。

  2. 可选地,从www.graphviz.org/Home.php安装 Graphviz。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 在你的终端/控制台应用程序中,创建并导航到chapter13/pprof目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter13/pprof复制测试或将其作为练习编写一些自己的代码。

  3. 创建一个名为crypto的目录并导航到它。

  4. 创建一个名为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) {
            r.ParseForm()

            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
        }

  1. 向上导航一个目录。

  2. 创建一个名为 example 的新目录并导航到它。

  3. 创建一个包含以下内容的 main.go 文件。确保你将 crypto 导入修改为在步骤 2 中设置的路径:

        package main

        import (
            "fmt"
            "log"
            "net/http"
            _ "net/http/pprof"

            "github.com/agtorre/go-cookbook/chapter13/pprof/crypto"
        )

        func main() {

            http.HandleFunc("/guess", crypto.GuessHandler)
            fmt.Println("server started at localhost:8080")
            log.Panic(http.ListenAndServe("localhost:8080", nil))
        }

  1. 运行 go run main.go

  2. 你也可以运行以下命令:

 go build ./example

你现在应该看到以下输出:

 $ go run main.go
 server started at localhost:8080

  1. 在另一个终端中运行以下命令:
 $ go tool pprof http://localhost:8080/debug/pprof/profile

  1. 这将启动一个 30 秒的计时器。

  2. 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  

  1. 返回到 pprof 命令并等待其完成。

  2. 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

  1. 如果你安装了 Graphviz,运行 pprof web 命令。你应该看到类似以下的内容:

图片

  1. 如果你复制或编写了自己的测试用例,请向上导航一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

pprof 工具提供了关于你的应用程序的大量运行时信息。使用 net/pprof 包通常是最简单的配置方式——所需做的就是监听一个端口并进行导入。

在我们的案例中,我们编写了一个使用非常计算密集型应用程序(bcrypt)的处理程序,以便我们可以演示它们在 pprof 分析时是如何出现的。这将快速隔离创建应用程序瓶颈的代码块。

我们选择收集一个通用配置文件,使 pprof 在 30 秒内轮询我们的应用程序端点。然后我们生成针对端点的流量以帮助产生结果。这在你尝试检查单个处理器或代码分支时可能很有帮助。

最后,我们查看 CPU 利用率最高的前 10 个函数。也可以使用 pprof http://localhost:8080/debug/pprof/heap 命令查看内存/堆管理。pprof web 命令可以用来查看你的 CPU/内存配置文件的可视化,并有助于突出更活跃的代码。

基准测试和查找瓶颈

确定代码中缓慢部分的另一种方法是使用基准测试。基准测试可以用来测试函数的平均性能,也可以并行运行基准测试。这在对函数进行比较或对某些代码进行微优化时可能很有用,特别是为了查看函数实现在使用并发时可能的表现。对于这个菜谱,我们将创建两个都实现了原子计数器的结构体。第一个将使用 sync 包,另一个将使用 sync/atomic。然后我们将基准测试这两个解决方案。

准备工作

参考本章中 加快编译和测试周期 菜单的 准备工作 部分。

如何做到...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建名为 chapter13/bench 的目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter13/bench 复制测试用例,或者将其作为练习编写一些自己的代码。

注意,复制的测试也包括在此配方中稍后编写的基准测试。

  1. 创建一个名为 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
        }

  1. 创建一个名为 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
        }

  1. 创建一个名为 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()
                }
            })
        }

  1. 创建一个名为 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()
                }
            })
        }

  1. 运行 go test -bench1 命令,你将看到以下输出:
 $ 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/agtorre/go-cookbook/chapter13/bench 10.919s

  1. 如果你复制或编写了自己的测试,请向上导航一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

本配方是代码关键路径比较的一个示例。例如,有时你的应用程序必须经常执行某些功能,可能每次调用都要执行。在这种情况下,我们编写了一个原子计数器,可以从多个 goroutine 中添加或读取值。

第一个解决方案使用 RWMutexLockRLock 对象分别进行写入和读取。第二个使用提供相同功能的原子包。我们使函数的签名相同,以便基准测试可以经过少量修改后重用,并且两个都可以满足相同的原子整数接口。

最后,我们编写了添加值和读取值的基准测试。然后,我们编写了一个并行基准测试,该测试调用添加和读取函数。并行基准测试将创建大量的锁竞争,因此我们预计会有性能下降。也许出乎意料的是,原子包在性能上显著优于 RWMutex

内存分配和堆管理

一些应用程序可以从优化中受益良多。以路由器为例,我们将在后面的配方中探讨。幸运的是,基准测试工具集提供了收集大量内存分配以及内存分配大小的标志。这有助于调整某些关键代码路径以最小化这两个属性。

本配方将展示两种编写将字符串粘合在一起的方法,类似于 strings.Join("a", "b", "c")。一种方法将使用连接,而另一种将使用 strings 包。然后我们将比较两种方法的性能和内存分配。

准备就绪

参考本章中 Speeding up compilation and testing cycles 配方的 准备就绪 部分。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 在你的终端/控制台应用程序中,创建 chapter13/tuning 目录并导航到该目录。

  2. github.com/agtorre/go-cookbook/tree/master/chapter13/tuning 复制测试或将其作为练习编写一些自己的代码。

注意,复制的测试也包括在此配方中稍后编写的基准测试。

  1. 创建一个名为 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
        }

  1. 创建一个名为 join.go 的文件,内容如下:
        package tuning

        import "strings"

        func join(vals ...string) string {
            c := strings.Join(vals, " ")
            return c
        }

  1. 创建一个名为 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...)
                }
            })
        }

  1. 创建一个名为 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...)
                    }
            })
        }

  1. 运行 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/agtorre/go-cookbook/chapter13/tuning 12.003s

  1. 如果您复制或编写了自己的测试,请向上移动一个目录并运行 go test。确保所有测试都通过。

它是如何工作的...

基准测试帮助我们调整应用程序,并对内存分配等事物进行某些微优化。当对具有输入的应用程序进行基准测试时,尝试各种输入大小以确定它是否会影响分配是很重要的。我们编写了两个函数,concatjoin。这两个函数都将一个可变字符串参数与空格连接起来,所以参数 (a, b, c) 将返回字符串 a b c

concat 方法仅通过字符串连接来完成这项工作。我们创建一个字符串,并在 for 循环中追加列表中的字符串和空格。在最后一个循环中我们省略添加空格。join 函数使用内部的 Strings.Join 函数在大多数情况下更有效地完成这项工作。将标准库与您自己的函数进行比较进行基准测试可能有助于更好地理解性能、简单性和功能方面的权衡。

我们使用了子基准测试来测试所有参数,这也非常适合与表格驱动基准测试一起使用。我们可以看到 concat 方法在至少对于单长度输入的情况下,比连接方法产生了更多的分配。一个很好的练习是尝试使用可变长度的输入字符串以及不同的参数数量。

供应商化和项目布局

将 Go 应用程序进行供应商化仍然是一个很大程度上未解决的问题。有关于创建官方供应商解决方案的讨论和计划(github.com/golang/dep),但目前还处于早期阶段,事情远未完成。目前,有几种替代方案。默认情况下,您可以将包放置到本地供应商目录中,以使用它们而不是 GOPATH 环境变量中的那些。这允许包锁定其供应商目录中的版本,并允许在不将整个 GOPATH 提交到版本控制的情况下进行可重复构建。大多数包管理器都利用了这一点。对于这个食谱,我们将探讨 Web 应用程序的布局以及如何使用 godep github.com/tools/godep,一个流行的依赖管理工具来管理其供应商依赖。

准备就绪

通过以下步骤配置您的环境:

  1. 参考本章中“加速编译和测试周期”食谱的“准备就绪”部分。

  2. 运行 go get github.com/tools/godep 命令。

  3. 运行 go get github.com/sirupsen/logrus 命令。

如何操作...

这些步骤涵盖了编写和运行您的应用程序:

  1. 在您的终端/控制台应用程序中,创建 chapter13/vendoring 目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter13/vendoring 复制测试

    或者将其用作练习来编写你自己的代码。

  3. 创建一个名为 models 的目录并进入该目录。

  4. 创建一个名为 models.go 的文件,内容如下:

        package models

        import "sync/atomic"

        // DB Interface is our storage
        // layer
        type DB interface {
            GetScore() (int64, error)
            SetScore(int64) error
        }

        // NewDB returns our db struct that
        // satisfies DB interface
        func NewDB() DB {
            return &db{0}
        }

        type db struct {
            score int64
        }

        // GetScore returns the score atomically
        func (d *db) GetScore() (int64, error) {
            return atomic.LoadInt64(&d.score), nil
        }

        // SetScore stores a new value atomically
        func (d *db) SetScore(score int64) error {
            atomic.StoreInt64(&d.score, score)
            return nil
        }

  1. 向上导航一个目录。

  2. 创建一个名为 handlers 的目录并进入该目录。

  3. 创建一个名为 controller.go 的文件,内容如下:

        package handlers

        import "github.com/agtorre/go-
        cookbook/chapter13/vendoring/models"

        type Controller struct {
            db models.DB
        }

        func NewController(db models.DB) *Controller {
            return &Controller{db: db}
        }

        type resp struct {
            Status string `json:"status"`
            Value int64 `json:"value"`
        }

  1. 创建一个名为 get.go 的文件,内容如下:
        package handlers

        import (
            "encoding/json"
            "net/http"

            "github.com/sirupsen/logrus"
        )

        // GetHandler returns the current score in a resp object
        func (c *Controller) GetHandler(w http.ResponseWriter, r 
        *http.Request) {
            enc := json.NewEncoder(w)
            payload := resp{Status: "error"}
            oldScore, err := c.db.GetScore()
            if err != nil {
                logrus.WithField("error", err).Error("failed to get the 
                score")
                w.WriteHeader(http.StatusInternalServerError)
                enc.Encode(&payload)
                return
            }
            w.WriteHeader(http.StatusOK)
            payload.Value = oldScore
            payload.Status = "ok"
            enc.Encode(&payload)
        }

  1. 创建一个名为 set.go 的文件,内容如下:
        package handlers

        import (
            "encoding/json"
            "net/http"
            "strconv"

            "github.com/sirupsen/logrus"
        )

        // SetHandler Sets the value, and returns it in a resp
        func (c *Controller) SetHandler(w http.ResponseWriter, r 
        *http.Request) {
            enc := json.NewEncoder(w)
            payload := resp{Status: "error"}
            r.ParseForm()
            val := r.FormValue("score")
            score, err := strconv.ParseInt(val, 10, 64)
            if err != nil {
                logrus.WithField("error", err).Error("failed to parse 
                input")
                w.WriteHeader(http.StatusBadRequest)
                enc.Encode(&payload)
                return
            }

            if err := c.db.SetScore(score); err != nil {
                logrus.WithField("error", err).Error("failed to set the 
                score")
                w.WriteHeader(http.StatusInternalServerError)
                enc.Encode(&payload)
                return
            }
            w.WriteHeader(http.StatusOK)
            payload.Value = score
            payload.Status = "ok"
            enc.Encode(&payload)
        }

  1. 供应商你的依赖项:
 $ godep save ./...
 $ cat Godeps/Godeps.json
 {
 "ImportPath": "github.com/agtorre/go-
 cookbook/chapter13/vendoring",
 "GoVersion": "go1.8",
 "GodepVersion": "v79",
 "Packages": [
 "./..."
 ],
 "Deps": [
 {
 "ImportPath": "github.com/sirupsen/logrus",
 "Comment": "v0.11.2-1-g3f603f4",
 "Rev": "3f603f494d61c73457fb234161d8982b9f0f0b71"
 },
 {
 "ImportPath": "golang.org/x/sys/unix",
 "Rev": "dbc2be9168a660ef302e04b6ff6406de6f967473"
 }
 ]
 }

  1. 向上导航一个目录。

  2. 创建一个名为 main.go 的文件,内容如下:

        package main

        import (
            "net/http"

            "github.com/agtorre/go-
            cookbook/chapter13/vendoring/handlers"
            "github.com/agtorre/go-cookbook/chapter13/vendoring/models"
            "github.com/sirupsen/logrus"
        )

        func main() {
            c := handlers.NewController(models.NewDB())

            logrus.SetFormatter(&logrus.JSONFormatter{})

            http.HandleFunc("/get", c.GetHandler)
            http.HandleFunc("/set", c.SetHandler)
            fmt.Println("server started at localhost:8080")
            panic(http.ListenAndServe("localhost:8080", nil))
        }

  1. 运行 go run main.go

  2. 你也可以运行以下命令:

 go build
 ./vendoring

你应该看到以下输出:

 $ go run main.go
 server started at localhost:8080

  1. 在另一个终端中运行一些 curl 命令:
 $ curl "http://localhost:8080/set?score=24" 
 {"status":"ok","value":24}

 $ curl "http://localhost:8080/get"
 {"status":"ok","value":24}

 $ curl "http://localhost:8080/set?score=abc" 
 {"status":"error","value":0}

  1. 查看服务器日志:
 {"error":"strconv.ParseInt: parsing \"abc\": invalid 
 syntax","level":"error","msg":"failed to parse 
 input","time":"2017-05-26T20:49:47-07:00"}

  1. 如果你复制或编写了自己的测试,请运行 go test。确保所有测试都通过。

它是如何工作的...

本食谱展示了如何在应用程序中分离基本关注点。对于模型或客户端等资源,首先创建一个执行动作的接口是一个好主意,然后满足该接口并提供方便的设置函数。模型/客户端代码也会经常产生自定义错误类型。

接下来,我们创建我们的控制器和处理器,将所有客户端请求隔离到服务器。Controller 对象使用存储接口,这使得在不修改应用程序代码的情况下轻松切换存储解决方案变得容易。最后,main.go 用于设置路由、初始化控制器和配置诸如日志记录等事项。我们使用包级别的全局日志记录器,以便我们的任何方法都可以在需要时自由记录。我们仍然尝试只在处理错误时记录,而不是在遇到并快速返回时。

我们使用了 logrus 作为我们的日志系统,这引入了一个我们希望供应商以便于可重复构建的依赖项。我们使用了 Godep 工具将 logrus 的本地副本存储在我们的供应商目录中。本项目检查将使用 vendors 中的固定版本进行未来构建,并且可以在准备就绪时升级。

使用 fasthttprouter 和 fasthttp

虽然 Go 标准库提供了运行 HTTP 服务器所需的一切,但有时你需要进一步优化路由和请求时间等问题。本食谱将探讨一个名为 fasthttp 的库,它可以加快请求处理速度(github.com/valyala/fasthttp)以及一个名为 fasthttprouter 的路由器,它可以显著提高路由性能(github.com/buaazp/fasthttprouter)。虽然 fasthttp 很快,但需要注意的是,它不支持 HTTP/2 (github.com/valyala/fasthttp/issues/45)。

准备工作

通过执行以下步骤来配置你的环境:

  1. 参考本章中“加速编译和测试周期”食谱的“准备就绪”部分。

  2. 运行 go get github.com/buaazp/fasthttprouter 命令。

  3. 运行 go get github.com/valyala/fasthttp 命令。

如何操作...

这些步骤涵盖了编写和运行你的应用程序:

  1. 从你的终端/控制台应用程序中,创建 chapter13/fastweb 目录并导航到它。

  2. github.com/agtorre/go-cookbook/tree/master/chapter13/fastweb 复制测试或将其作为练习编写你自己的代码。

  3. 创建一个名为 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
        }

  1. 创建一个名为 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)
            }

            AddItem(item)
            ctx.SetStatusCode(fasthttp.StatusOK)
        }

  1. 创建一个名为 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))
        }

  1. 运行 go build 命令。

  2. 运行 ./fastweb 命令:

 $ ./fastweb
 server starting on localhost:8080

  1. 在另一个终端中,使用一些 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"]

  1. 如果你复制或编写了自己的测试,运行 go test。确保所有测试都通过。

它是如何工作的...

fasthttpfasthttprouter 包可以为加快 Web 请求的生命周期做很多事情。这两个包在代码的热路径上做了很多优化,但不幸的是,你必须将处理器重写为使用新的上下文对象,而不是传统的请求和响应写入器。

有许多框架采用了类似的路由方法,其中一些直接集成了 fasthttp。这些项目在它们的 README 文件中保持信息更新。

我们的食谱实现了一个简单的列表对象,我们可以通过一个端点向其添加内容,另一个端点将返回它。本食谱的主要目的是演示如何使用参数,设置一个现在明确定义了支持方法的路由器,而不是通用的 HandleHandleFunc,并展示它如何与标准处理器相似,但具有许多其他优点。

posted @ 2025-09-06 13:43  绝不原创的飞龙  阅读(15)  评论(0)    收藏  举报