Go-秘籍-三-

84 阅读16分钟

Go 秘籍(三)

原文:Go Recipes

协议:CC BY-NC-SA 4.0

五、使用标准库包

包是 Go 生态系统中非常重要的组成部分。Go 代码被组织成包,使你的 Go 程序具有可重用性和可组合性。Go 安装附带了许多可重用的包,称为标准库包。这些包扩展了 Go 语言,并为构建各种应用程序提供了可重用的库。它们可以帮助您快速构建应用程序,因为您不需要为许多常见功能编写自己的包。如果您想扩展标准库包,您可以创建自己的包,也可以获得 Go 开发者社区提供的第三方包。标准库包的功能非常丰富。您可以只使用标准库包来构建成熟的 web 应用程序,而不使用任何第三方包。本章介绍了如何使用标准库包来实现一些常见的功能,例如编码和解码 JavaScript 对象符号(JSON)对象、解析命令行标志、记录 Go 程序和归档文件。标准库包的文档可从 https://golang.org/pkg/ 获得。

5-1.编码和解码 JSON

问题

您希望将 Go 类型的值编码成 JSON 对象,并将 JSON 对象解码成 Go 类型的值。

解决办法

标准库包encoding/json用于编码和解码 JSON 对象。

它是如何工作的

JSON 是一种数据交换格式,广泛用于 web 后端服务器与 web 和移动应用程序前端之间的通信。当您使用 Go 构建 RESTful 应用程序编程接口(API)时,您可能需要从 HTTP 请求体中解码 JSON 值,并将这些数据解析为 Go 值,并将 Go 值编码为 JSON 值以发送到 HTTP 响应。

编码 JSON

json包的Marshal函数用于将 Go 值编码成 JSON 值。要使用json包,您必须将包encoding/json添加到导入列表中。

import (
       "encoding/json"
)

下面是函数Marshal的签名:

func Marshal(v interface{}) ([]byte, error)

函数Marshal返回两个值:作为slice byte的编码 JSON 数据和一个error值。

让我们声明一个 struct 类型来演示将 struct 类型的值解析到 JSON 中:

type Employee struct {
        ID                            int
        FirstName, LastName, JobTitle string
}

下面的代码块创建了一个Employee struct 的实例,并将值解析成 JSON。

emp := Employee{
                ID:        100,
                FirstName: "Shiju",
                LastName:  "Varghese",
                JobTitle:  "Architect",
        }
    // Encoding to JSON
    data, err := json.Marshal(emp)

函数Marshal返回Employee结构值的 JSON 编码。当您构建基于 JSON 的 RESTful APIs 时,您主要是将 struct 类型的值解析到 JSON 对象中。使用Marshal,您可以轻松地将 struct 类型的值编码为 JSON 值,这将帮助您快速构建基于 JSON 的 API。

解码 JSON

json包的函数Unmarshal用于将 JSON 值解码成 Go 值。下面是函数Unmarshal的签名:

func Unmarshal(data []byte, v interface{}) error

函数Unmarshal解析 JSON 编码的数据,并将结果存储到第二个参数中(v interface{})。下面的代码块解码 JSON 数据,并将结果存储到Employee结构的值中:

b := []byte(`{"ID":101,"FirstName":"Irene","LastName":"Rose","JobTitle":"Developer"}`)
var emp1 Employee
// Decoding JSON data into the value of Employee struct
err = json.Unmarshal(b, &emp1)

前面的语句解析变量b的 JSON 数据,并将结果存储到变量emp1中。JSON 数据是使用反引号作为原始字符串提供的。在反引号中,除了反引号之外,任何字符都是有效的。现在,您可以像读取普通结构值一样读取Employee结构的字段,如下所示:

fmt.Printf("ID:%d, Name:%s %s, JobTitle:%s", emp1.ID, emp1.FirstName, emp1.LastName, emp1.JobTitle)

示例:编码和解码

清单 5-1 显示了一个示例程序,该程序演示了将 struct 类型的值编码到 JSON 对象中,以及将 JSON 对象解码成 struct 类型的值。

package main

import (
        "encoding/json"
        "fmt"
)

// Employee struct
type Employee struct {
        ID                            int
        FirstName, LastName, JobTitle string
}

func main() {
        emp := Employee{
                ID:        100,
                FirstName: "Shiju",
                LastName:  "Varghese",
                JobTitle:  "Architect",
        }
    // Encoding to JSON
        data, err := json.Marshal(emp)
        if err != nil {
                fmt.Println(err.Error())
                return
        }
        jsonStr := string(data)
              fmt.Println("The JSON data is:")
        fmt.Println(jsonStr)

        b := []byte(`{"ID":101,"FirstName":"Irene","LastName":"Rose","JobTitle":"Developer"}`)
        var emp1 Employee
    // Decoding JSON data to a value of struct type
        err = json.Unmarshal(b, &emp1)
        if err != nil {
                fmt.Println(err.Error())
                return
        }
               fmt.Println("The Employee value is:")
        fmt.Printf("ID:%d, Name:%s %s, JobTitle:%s", emp1.ID, emp1.FirstName, emp1.LastName, emp1.JobTitle)
}

Listing 5-1.Encoding and Decoding of JSON with a Struct Type

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

The JSON data is:
{"ID":100,"FirstName":"Shiju","LastName":"Varghese","JobTitle":"Architect"}
The Employee value is:
ID:101, Name:Irene Rose, JobTitle:Developer

Note

当使用 struct 类型的值对 JSON 数据进行编码和解码时,必须将 struct 类型的所有字段指定为导出字段(标识符名称以大写字母开头),因为在调用MarshalUnmarshal函数时,json包正在使用 struct 字段的值。

结构标记

当您将 struct 类型的值编码到 JSON 中时,您可能需要在 JSON 编码中使用与 struct 类型的字段不同的字段。例如,您可以以大写字母开头来指定 struct 字段的名称,以将它们标记为导出字段,但是在 JSON 中,元素通常以小写字母开头。在这里,我们可以使用 struct 标记将 struct 字段的名称与 JSON 中的字段名称进行映射,以便在编码和解码 JSON 对象时使用。

下面是用 JSON 编码中要使用的标签和不同名称指定的Employee结构:

type Employee struct {
        ID        int    `json:"id,omitempty"`

        FirstName string `json:"firstname"`

        LastName  string `json:"lastname"`

        JobTitle  string `json:"job"`

}

注意,反引号(`)用于指定标签。在引号中,您将包json的元数据称为标签。在引号内,除了另一个反引号之外,任何字符都是有效的。结构字段IDid标记,用于 JSON 表示。omitempty标志指定如果该字段有默认值,则该字段不包含在 JSON 表示中。如果您没有为Employee结构的ID字段提供值,那么当您将Employee值解析到 JSON 时,JSON 对象的输出不包括id字段。对于 JSON 数据,Employee结构的所有字段都用不同的名称标记。

如果你想从结构中跳过字段,你可以给标签名为"-"。这里显示的User结构指定在编码和解码 JSON 对象时必须跳过字段Password:

type User struct {
    UserName string `json:"user"`
    Password string `json:"-"`

}

示例:使用 Struct 标记进行编码和解码

清单 5-2 展示了一个示例程序,演示了用 struct 标签对 JSON 对象进行编码和解码。

package main

import (
        "encoding/json"
        "fmt"
)

// Employee struct with struct tags
type Employee struct {
        ID        int    `json:"id,omitempty"`
        FirstName string `json:"firstname"`
        LastName  string `json:"lastname"`
        JobTitle  string `json:"job"`
}

func main() {
        emp := Employee{
                FirstName: "Shiju",
                LastName:  "Varghese",
                JobTitle:  "Architect",
        }
        // Encoding to JSON
        data, err := json.Marshal(emp)
        if err != nil {
                fmt.Println(err.Error())
                return
        }
        jsonStr := string(data)
        fmt.Println("The JSON data is:")
        fmt.Println(jsonStr)

        b := []byte(`{"id":101,"firstname":"Irene","lastname":"Rose","job":"Developer"}`)
        var emp1 Employee
        // Decoding JSON to a struct type
        err = json.Unmarshal(b, &emp1)
        if err != nil {
                fmt.Println(err.Error())
                return
        }
        fmt.Println("The Employee value is:")
        fmt.Printf("ID:%d, Name:%s %s, JobTitle:%s", emp1.ID, emp1.FirstName, emp1.LastName, emp1.JobTitle)
}

Listing 5-2.Encoding and Decoding of JSON with Struct Tags

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

The JSON data is:
{"firstname":"Shiju","lastname":"Varghese","job":"Architect"}
The Employee value is:
ID:101, Name:Irene Rose, JobTitle:Developer

Employee结构用字段名称标记,用于 JSON 对象。在没有指定ID字段的情况下创建了Employee结构的值,因此在将Employee结构的值编码到 JSON 中时,JSON 对象不包括id字段。JSON 输出还显示了在 struct 声明中标记的相应 JSON 字段名。当我们解码 JSON 对象时,id字段不为空,因此它被解析到Employee结构的ID字段中。

5-2.使用命令行标志

问题

您希望解析命令行标志,以便为 Go 程序提供一些值。

解决办法

标准库包flag用于解析命令行标志。

它是如何工作的

有时,在运行程序时,您可能需要通过命令行从最终用户那里接收值。这是构建命令行应用程序时的一个基本特性。命令行选项,也称为标志,可用于在运行程序时向程序提供值。标准库包flag提供了解析命令行标志的函数。包flag提供了使用flag.String()flag.Bool()flag.Int()解析stringintegerboolean值的函数。

要使用flag包,您必须将其添加到导入列表中:

import (
       "flag"
)

清单 5-3 显示了一个示例程序,演示了如何在 Go 程序中定义标志。

package main

import (
        "flag"
        "fmt"
)

func main() {

        fileName := flag.String("filename", "logfile", "File name for the log file")
        logLevel := flag.Int("loglevel", 0, "An integer value for Level (0-4)")
        isEnable := flag.Bool("enable", false, "A boolean value for enabling log options")
        var num int
        // Bind the flag to a variable.
        flag.IntVar(&num, "num", 25, "An integer value")

        // Parse parses flag definitions from the argument list.
        flag.Parse()
        // Get the values from pointers
        fmt.Println("filename:", *fileName)
        fmt.Println("loglevel:", *logLevel)
        fmt.Println("enable:", *isEnable)
        // Get the value from a variable
        fmt.Println("num:", num)
        // Args returns the non-flag command-line arguments.
        args := flag.Args()
        if len(args) > 0 {
                fmt.Println("The non-flag command-line arguments are:")
                // Print the arguments
                for _, v := range args {
                        fmt.Println(v)
                }
        }

}

Listing 5-3.Defining Flags Using Package flag

函数flag.String用于定义通过命令行获取string值的标志。

fileName := flag.String("filename", "logfile", "File name for the log file")

前面的语句声明了一个string标志,标志名为filename,并提供了一个默认值"logfile "filename标志(-filename的用户输入存储在指针fileName中,类型为*string。第三个参数描述了标志的用法。函数flag.Bool()flag.Int()用于声明booleaninteger值的标志。

logLevel := flag.Int("loglevel", 0, "An integer value for Level (0-4)")
isEnable := flag.Bool("enable", false, "A boolean value for enabling log options")

如果您想将标志绑定到一个现有的变量,您可以使用函数flag.IntVarflag.BoolVarflag.StringVar。下面的代码块将标志num ( -num)绑定到integer变量num

var num int
// Bind the flag to a variable.
flag.IntVar(&num, "num", 25, "An integer value")

函数flag.Parse()从命令行解析标志定义。因为函数flag.String()flag.Bool()flag.Int()是返回指针,所以我们解引用这些指针来获取值。

fmt.Println("name:", *fileName)
fmt.Println("num:", *logLevel)
fmt.Println("enable:", *isEnable)

函数flag.IntVar返回一个integer值,而不是一个指针,这样就可以在不引用指针的情况下读取该值。

fmt.Println("num:", num)

flag提供了一个名为Args的函数,可以用来读取非 flag 命令行参数。如果您提供非 flag 命令行参数,这个函数调用将返回一个stringslice。命令行参数位于命令行标志之后。如果用户提供的话,命令行参数会打印到控制台中。

args := flag.Args()
        if len(args) > 0 {
                fmt.Println("The non-flag command-line arguments are:")
                // Print the arguments
                for _, v := range args {
                        fmt.Println(v)
                }
        }

让我们构建程序,并使用不同的命令行选项运行它:

$ go build

首先,让我们通过提供所有的标志和参数来运行程序。

$ ./ cmdflags -filename=applog -loglevel=2 -enable -num=50 10 20 30 test
filename: applog
loglevel: 2
enable: true
num: 50
The non-flag command-line arguments are:
10
20
30
test

必须在给出标志后提供非标志命令行参数。标志-h--help为命令行程序的使用提供帮助。该帮助文本将由程序中定义的标志定义生成。让我们通过提供-h标志来运行程序。

$ ./ cmdflags -h
Usage of cmdflags:
  -enable
        A boolean value for enabling log options
  -filename string
        File name for the log file (default "logfile")
  -loglevel int
        An integer value for Level (0-4)
  -num int
        An integer value (default 25)

现在让我们通过提供几个不带非标志参数的标志来运行程序:

$ ./ cmdflags -filename=applog -loglevel=1
filename: applog
loglevel: 1
enable: false
num: 25

如果用户没有为标志提供值,将采用默认值。

5-3.记录 Go 程序

问题

您希望为您的 Go 程序实现日志记录。

解决办法

标准库包log提供了一个基本的日志基础设施,可以用来记录你的 Go 程序。

它是如何工作的

尽管有许多第三方包可用于日志记录,但如果您想继续使用标准库或使用简单的包,标准库包log应该是您的选择。包log允许你将日志信息写入所有支持io.Writer接口的标准输出设备。struct type log.Logger是包log,中的主要组件,它提供了几种日志记录方法,也支持格式化日志数据。

要使用包log,您必须将其添加到导入列表:

import (
       "log"
)

示例:一个基本的记录器

清单 5-4 显示了一个使用log.Logger类型提供基本日志实现的示例程序。日志消息分为跟踪、信息、警告和错误,每个日志类别使用四个log.Logger对象。

package main

import (
        "errors"
        "io"
        "io/ioutil"
        "log"
        "os"
)

// Package level variables, which are pointers to log.Logger.
 var (
        Trace   *log.Logger
        Info    *log.Logger
        Warning *log.Logger
        Error   *log.Logger
)

// initLog initializes log.Logger objects
func initLog(
        traceHandle io.Writer,
        infoHandle io.Writer,
        warningHandle io.Writer,
        errorHandle io.Writer) {

        // Flags for defineing the logging properties, to log.New
        flag := log.Ldate | log.Ltime | log.Lshortfile

        // Create log.Logger objects
        Trace = log.New(traceHandle, "TRACE: ", flag)
        Info = log.New(infoHandle, "INFO: ", flag)
        Warning = log.New(warningHandle, "WARNING: ", flag)
        Error = log.New(errorHandle, "ERROR: ", flag)

}

func main() {
        initLog(ioutil.Discard, os.Stdout, os.Stdout, os.Stderr)
        Trace.Println("Main started")
        loop()
        err := errors.New("Sample Error")
        Error.Println(err.Error())
        Trace.Println("Main completed")
}
func loop() {
        Trace.Println("Loop started")
        for i := 0; i < 10; i++ {
                Info.Println("Counter value is:", i)
        }
        Warning.Println("The counter variable is not being used")
        Trace.Println("Loop completed")
}

Listing 5-4.A Basic Logging Implementation with Categorized Logging for Trace, Information, Warning, and Error Messages

为跟踪、信息、警告和错误的分类日志记录声明了四个指向类型log.Logger的指针。通过调用函数initLog来创建log.Logger对象,该函数接收接口io.Writer的参数来设置日志消息的目的地。

// Package level variables, which are pointers to log.Logger.
var (
        Trace   *log.Logger
        Info    *log.Logger
        Warning *log.Logger
        Error   *log.Logger
)

// initLog initializes log.Logger objects
func initLog(
        traceHandle io.Writer,
        infoHandle io.Writer,
        warningHandle io.Writer,
        errorHandle io.Writer) {

        // Flags for defining the logging properties, to log.New
        flag := log.Ldate | log.Ltime | log.Lshortfile

        // Create log.Logger objects
        Trace = log.New(traceHandle, "TRACE: ", flag)
        Info = log.New(infoHandle, "INFO: ", flag)
        Warning = log.New(warningHandle, "WARNING: ", flag)
        Error = log.New(errorHandle, "ERROR: ", flag)

}

函数log.New创建一个新的log.Logger。在函数New中,第一个参数设置日志数据的目的地,第二个参数设置出现在每个生成的日志行开头的前缀,第三个参数定义日志属性。给定的日志记录属性在日志数据中提供日期、时间和短文件名。日志数据可以写入任何支持接口io.Writer的目的地。从功能main调用功能initLog

initLog(ioutil.Discard, os.Stdout, os.Stdout, os.Stderr)

ioutil.Discard提供给 Trace 的目的地,这是一个空设备,因此这个目的地的所有日志写调用都将成功,而无需做任何事情。os.Stdout是给目的地的信息和警告,因此该目的地的所有日志写调用都将出现在控制台窗口中。将os.Stderr赋予错误的目的地,以便该目的地的所有日志写调用将作为标准错误出现在控制台窗口中。在这个示例程序中,Logger跟踪、信息、警告和错误的对象用于记录消息。因为跟踪的目的地被配置为ioutil.Discard,日志数据不会出现在控制台窗口中。

您应该会看到类似如下的输出:

INFO: 2016/06/11 18:47:28 main.go:48: Counter value is: 0
INFO: 2016/06/11 18:47:28 main.go:48: Counter value is: 1
INFO: 2016/06/11 18:47:28 main.go:48: Counter value is: 2
INFO: 2016/06/11 18:47:28 main.go:48: Counter value is: 3
INFO: 2016/06/11 18:47:28 main.go:48: Counter value is: 4
INFO: 2016/06/11 18:47:28 main.go:48: Counter value is: 5
INFO: 2016/06/11 18:47:28 main.go:48: Counter value is: 6
INFO: 2016/06/11 18:47:28 main.go:48: Counter value is: 7
INFO: 2016/06/11 18:47:28 main.go:48: Counter value is: 8
INFO: 2016/06/11 18:47:28 main.go:48: Counter value is: 9
WARNING: 2016/06/11 18:47:28 main.go:50: The counter variable is not being used
ERROR: 2016/06/11 18:47:28 main.go:42: Sample Error

示例:可配置的记录器

在前面的例子中,日志数据被写入StdoutStderr接口。然而,当您开发真实世界的应用程序时,您可能会使用持久化存储作为日志数据的目的地。您可能还需要一个可配置的选项来指定跟踪、信息、警告或错误的日志级别。这使您可以随时更改日志级别。例如,您可能将日志级别设置为跟踪,但在将应用程序投入生产时,您可能不需要跟踪级别的日志。

清单 5-5 显示了一个示例程序,它提供了一个日志基础设施,允许您将日志级别配置为跟踪、信息、警告或错误,然后将日志数据写入一个文本文件。可以使用命令行标志来配置日志级别选项。

package main

import (
        "io"
        "io/ioutil"
        "log"
        "os"
)

const (
        // UNSPECIFIED logs nothing
        UNSPECIFIED Level = iota // 0 :
        // TRACE logs everything
        TRACE // 1
        // INFO logs Info, Warnings and Errors
        INFO // 2
        // WARNING logs Warning and Errors
        WARNING // 3
        // ERROR just logs Errors
        ERROR // 4
)

// Level holds the log level.
type Level int

// Package level variables, which are pointers to log.Logger.
var (
        Trace   *log.Logger
        Info    *log.Logger
        Warning *log.Logger
        Error   *log.Logger
)

// initLog initializes log.Logger objects
func initLog(
        traceHandle io.Writer,
        infoHandle io.Writer,
        warningHandle io.Writer,
        errorHandle io.Writer,
        isFlag bool) {

        // Flags for defining the logging properties, to log.New
        flag := 0
        if isFlag {
                flag = log.Ldate | log.Ltime | log.Lshortfile
        }

        // Create log.Logger objects.
        Trace = log.New(traceHandle, "TRACE: ", flag)
        Info = log.New(infoHandle, "INFO: ", flag)
        Warning = log.New(warningHandle, "WARNING: ", flag)
        Error = log.New(errorHandle, "ERROR: ", flag)

}

// SetLogLevel sets the logging level preference
func SetLogLevel(level Level) {

        // Creates os.*File, which has implemented io.Writer interface
        f, err := os.OpenFile("logs.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
        if err != nil {
                log.Fatalf("Error opening log file: %s", err.Error())
        }

        // Calls function initLog by specifying log level preference.
        switch level {
        case TRACE:
                initLog(f, f, f, f, true)
                return

        case INFO:
                initLog(ioutil.Discard, f, f, f, true)
                return

        case WARNING:
                initLog(ioutil.Discard, ioutil.Discard, f, f, true)
                return
        case ERROR:
                initLog(ioutil.Discard, ioutil.Discard, ioutil.Discard, f, true)
                return

        default:
                initLog(ioutil.Discard, ioutil.Discard, ioutil.Discard, ioutil.Discard, false)
                f.Close()
                return

        }
}

Listing 5-5.A Logging Infrastructure with an Option to Set the Log Level and Write Log Data into a Text File, in logger.go

logger.go源提供两个功能:initLogSetLogLevel。函数SetLogLevel通过调用标准库包os的函数OpenFile来创建文件对象,然后调用函数initLog通过提供日志级别首选项来初始化Logger对象。它打开带有指定标志的命名文件。函数initLog根据函数提供的日志首选项创建Logger对象。

声明常量变量是为了指定不同级别的日志级别首选项。标识符iota用于构造一组相关的常数;在这里,它用于组织应用程序中可用的日志级别,这将产生一个自动递增的integer常量。每当const出现在源代码中时,它将值重置为 0,并在常量声明中的每个值之后递增。

const (
        // UNSPECIFIED logs nothing
        UNSPECIFIED Level = iota // 0 :
        // TRACE logs everything
        TRACE // 1
        // INFO logs Info, Warnings and Errors
        INFO // 2
        // WARNING logs Warning and Errors
        WARNING // 3
        // ERROR just logs Errors
        ERROR // 4
)

// Level holds the log level.
type Level int

在许多编程语言中,枚举或简单的枚举是声明具有相似行为的常数的惯用方式。与某些编程语言不同,Go 不支持使用关键字来声明枚举。在 Go 中声明枚举的惯用方式是用iota声明常量。这里,名为Level的类型和类型int用于指定常量的类型。常量UNSPECIFIED的值重置为 0,然后它自动递增每个常量声明,1 代表TRACE,2 代表INFO,依此类推。

清单 5-6 显示了一个使用在logger.go中实现的日志基础设施的 Go 源文件(参见清单 5-5 )。

package main

import (
        "errors"
        "flag"
)

func main() {
        // Parse log level from command line
        logLevel := flag.Int("loglevel", 0, "an integer value (0-4)")
        flag.Parse()
        // Calling the SetLogLevel with the command-line argument
        SetLogLevel(Level(*logLevel))
        Trace.Println("Main started")
        loop()
        err := errors.New("Sample Error")
        Error.Println(err.Error())
        Trace.Println("Main completed")
}
// A simple function for the logging demo
func loop() {
        Trace.Println("Loop started")
        for i := 0; i < 10; i++ {
                Info.Println("Counter value is:", i)
        }
        Warning.Println("The counter variable is not being used")
        Trace.Println("Loop completed")
}

Listing 5-6.Logging Demo in main.go, Using logger.go

在函数main中,从命令行标志接受日志级别首选项的值,并调用logger.go的函数SetLogLevel通过指定日志级别首选项来创建Logger对象。

logLevel := flag.Int("loglevel", 0, "an integer value (0-4)")
flag.Parse()
// Calling the SetLogLevel with the command-line argument
SetLogLevel(Level(*logLevel))

在本例中,使用Logger对象记录跟踪、信息、警告和错误。让我们通过为 Trace 提供日志级别首选项(值 1)来运行程序。

$ go build
$ ./log -loglevel=1

这会将日志数据写入名为logs.txt的文本文件。要跟踪的日志级别写入了TraceInformationWarningError的日志数据。您应该会在logs.txt中看到类似如下的日志数据。

TRACE: 2016/06/13 22:04:28 main.go:14: Main started
TRACE: 2016/06/13 22:04:28 main.go:23: Loop started
INFO: 2016/06/13 22:04:28 main.go:25: Counter value is: 0
INFO: 2016/06/13 22:04:28 main.go:25: Counter value is: 1
INFO: 2016/06/13 22:04:28 main.go:25: Counter value is: 2
INFO: 2016/06/13 22:04:28 main.go:25: Counter value is: 3
INFO: 2016/06/13 22:04:28 main.go:25: Counter value is: 4
INFO: 2016/06/13 22:04:28 main.go:25: Counter value is: 5
INFO: 2016/06/13 22:04:28 main.go:25: Counter value is: 6
INFO: 2016/06/13 22:04:28 main.go:25: Counter value is: 7
INFO: 2016/06/13 22:04:28 main.go:25: Counter value is: 8
INFO: 2016/06/13 22:04:28 main.go:25: Counter value is: 9
WARNING: 2016/06/13 22:04:28 main.go:27: The counter variable is not being used
TRACE: 2016/06/13 22:04:28 main.go:28: Loop completed
ERROR: 2016/06/13 22:04:28 main.go:17: Sample Error
TRACE: 2016/06/13 22:04:28 main.go:18: Main completed

让我们通过指定信息的日志级别来运行程序(loglevel的值为 2)。

$ ./log -loglevel=2

您应该会看到类似下面的日志数据附加到logs.txt中。

INFO: 2016/06/13 22:13:25 main.go:25: Counter value is: 0
INFO: 2016/06/13 22:13:25 main.go:25: Counter value is: 1
INFO: 2016/06/13 22:13:25 main.go:25: Counter value is: 2
INFO: 2016/06/13 22:13:25 main.go:25: Counter value is: 3
INFO: 2016/06/13 22:13:25 main.go:25: Counter value is: 4
INFO: 2016/06/13 22:13:25 main.go:25: Counter value is: 5
INFO: 2016/06/13 22:13:25 main.go:25: Counter value is: 6
INFO: 2016/06/13 22:13:25 main.go:25: Counter value is: 7
INFO: 2016/06/13 22:13:25 main.go:25: Counter value is: 8
INFO: 2016/06/13 22:13:25 main.go:25: Counter value is: 9
WARNING: 2016/06/13 22:13:25 main.go:27: The counter variable is not being used
ERROR: 2016/06/13 22:13:25 main.go:17: Sample Error

因为我们将日志级别指定为 Information,所以 Information、Warning 和 Error 的日志数据被附加到输出文件logs.txt中,但是 Trace 的日志数据被写入空设备中。

5-4.以 Tar 和 Zip 格式存档文件

问题

你想读写 tar 和 zip 格式的文件。

解决办法

标准库包archive包含两个子包——包archive/tar和包archive/zip,,用于读写 tar 和 zip 格式的归档文件。

它是如何工作的

标准库包archive支持以两种文件格式归档文件。为了支持 tar 和 zip 格式的归档功能,它提供了两个独立的包:archive/tararchive/ziparchive/tararchive/zip包分别为 tar 和 zip 格式的读写提供支持。

io。作家和木卫一。阅读器界面

在开始写入和读取归档文件之前,让我们先来看看io.Writerio.Reader接口。标准库包io提供了执行 I/O 操作的基本接口。包ioWriter接口为写操作提供了一个抽象。Writer接口声明了一个名为Write的方法,该方法接受一个值byte slice作为参数。

下面是接口io.Writer的声明:

type Writer interface {
        Write(p []byte) (n int, err error)
}

下面是Write方法的 Go 文档:

Writelen(p)字节从p写入底层数据流。它返回从p (0 <= n <= len(p))写入的字节数,以及遇到的导致写入提前停止的任何errorWrite如果返回n < len(p)则必须返回一个non-nil error。写入不得修改slice数据,即使是暂时的。

ioReader接口为读操作提供了一个抽象。Reader接口声明了一个名为Read的方法,该方法接受一个值byte slice作为参数。

下面是io.Reader接口的声明:

type Reader interface {
        Read(p []byte) (n int, err error)
}

下面是关于Read方法的 Go 文档:

Read 将最多len(p)个字节读入p。它返回读取的字节数(0 <= n <= len(p)和遇到的任何error)。即使Read返回n < len(p,它也可能在调用过程中使用所有的p作为暂存空间。如果有些数据可用,但没有len(p)字节,Read通常会返回可用的数据,而不是等待更多数据。当Read在成功读取n > 0字节后遇到error或文件结束条件时,它返回读取的字节数。它可以从同一个调用返回(non-nil ) error,或者从后续调用返回error(和n == 0)。这种一般情况的一个例子是,在输入流末尾返回非零字节数的Reader可能返回err == EOFerr == nil。下一个Read应该会返回0, EOF

当您读写归档文件时,您将利用io.Writerio.Reader接口。

写入和读取 Tar 文件

archive/tar用于读写 tar 文件。Tar(磁带归档)文件是在基于 Unix 的系统中使用的归档文件。tar 存档的文件后缀是.tartar Unix shell 命令从多个指定的文件中创建一个归档文件,或者从归档文件中提取文件。要使用包archive/tar,您必须将它添加到导入列表中:

import (
       "archive/tar"
)

结构类型tar.Writer用于将文件写入 tar 文件。通过调用接受类型为io.Writer的值的函数tar.NewWriter来创建Writer对象,您可以将 tar 存档文件作为类型为os.File的对象传递给该函数,以写入所提供的 tar 文件。结构类型os.File已经实现了io.Writer接口,因此它可以用作调用函数tar.NewWriter的参数。

结构类型tar.Reader用于从 tar 文件中读取文件。通过调用函数tar.NewReader来创建Reader对象,该函数接受类型为io.Reader的值作为参数,您可以将 tar 存档文件作为类型为os.File的对象传递给该参数,以读取 tar 文件的内容。结构类型os.File已经实现了接口io.Reader,因此它可以用作调用函数tar.NewReader的参数。

清单 5-7 显示了一个示例程序,它演示了如何通过将两个文件写入一个 tar 文件,然后通过遍历 tar 文件并读取每个文件的内容来读取 tar 文件,从而对文件进行归档。

package main

import (
        "archive/tar"
        "fmt"
        "io"
        "log"
        "os"
)

// addToArchive writes a given file into a .tar file
// Returns nill if the operation is succeeded
func addToArchive(filename string, tw *tar.Writer) error {
        // Open the file to archive into tar file.
        file, err := os.Open(filename)
        if err != nil {
                return err
        }
        defer file.Close()
        // Get the FileInfo struct that describes the file.
        fileinfo, err := file.Stat()
        // Create a pointer to tar.Header struct
        hdr := &tar.Header{
                ModTime: fileinfo.ModTime(),            // modified time
                Name:    filename,                      // name of header
                Size:    fileinfo.Size(),               // length in bytes
                Mode:    int64(fileinfo.Mode().Perm()), // permission and mode bits
        }
        // WriteHeader writes tar.Header and prepares to accept the file's contents.
        if err := tw.WriteHeader(hdr); err != nil {
                return err
        }
        // Write the file contents to the tar file.
        copied, err := io.Copy(tw, file)
        if err != nil {
                return err
        }
        // Check the size of copied file with the source file.
        if copied < fileinfo.Size() {
                return fmt.Errorf("Size of the copied file doesn't match with source file %s: %s", filename, err)
        }
        return nil
}

// archiveFiles archives a group of given files into a tar file.
func archiveFiles(files []string, archive string) error {
        // Flags for open the tar file.
        flags := os.O_WRONLY | os.O_CREATE | os.O_TRUNC
        // Open the tar file
        file, err := os.OpenFile(archive, flags, 0644)
        if err != nil {
                return err
        }
        defer file.Close()
        // Creates a new Writer writing to given file object.
        // Writer provides sequential writing of a tar archive in POSIX.1 format.
        tw := tar.NewWriter(file)
        defer tw.Close()
        // Iterate through the files to write each file into the tar file.
        for _, filename := range files {
                // Write the file into tar file.
                if err := addToArchive(filename, tw); err != nil {
                        return err
                }
        }
        return nil
}

// readArchive reads the file contents from tar file.
func readArchive(archive string) error {
        // Open the tar archive file.
        file, err := os.Open(archive)
        if err != nil {
                return err
        }
        defer file.Close()
        // Create the tar.Reader to read the tar archive.
        // A Reader provides sequential access to the contents of a tar archive.
        tr := tar.NewReader(file)
        // Iterate through the files in the tar archive.
        for {
                hdr, err := tr.Next()
                if err == io.EOF {
                        // End of tar archive
                        break
                }
                if err != nil {
                        return err
                }
                size := hdr.Size
                contents := make([]byte, size)
                read, err := io.ReadFull(tr, contents)
                // Check the size of file contents
                if int64(read) != size {
                        return fmt.Errorf("Size of the opened file doesn't match with the file %s", hdr.Name)
                }
                fmt.Printf("Contents of the file %s:\n", hdr.Name)
                // Writing the file contents into Stdout.
                fmt.Fprintf(os.Stdout, "\n%s", contents)
        }
        return nil
}

func main() {
        // Name of the tar file
        archive := "source.tar"
        // Files to be archived in tar format
        files := []string{"main.go", "readme.txt"}
        // Archive files into tar format
        err := archiveFiles(files, archive)
        if err != nil {
                log.Fatalf("Error while writing to tar file:%s", err)
        }
        // Archiving is successful.
        fmt.Println("The tar file source.tar has been created")
        // Read the file contents of tar file
        err = readArchive(archive)
        if err != nil {
                log.Fatalf("Error while reading the tar file:%s", err)
        }
}

Listing 5-7.Writing and Reading a Tar File

在函数main中,声明了一个变量archive来为 tar 文件提供文件名。声明一个变量files来提供文件名作为string slice来将提供的文件写入 tar 文件。调用函数archiveFiles来归档文件,调用另一个函数readArchive来读取 tar 文件的内容,该文件是使用函数archiveFiles写入的。

func main() {
        // Name of the tar file
        archive := "source.tar"
        // Files to be archived in tar format
        files := []string{"main.go", "readme.txt"}
        // Archive files into tar format
        err := archiveFiles(files, archive)
        if err != nil {
                log.Fatalf("Error while writing to tar file:%s", err)
        }
        // Archiving is successful.
        fmt.Println("The tar file source.tar has been created")
        // Read the file contents of tar file
        err = readArchive(archive)
        if err != nil {
                log.Fatalf("Error while reading the tar file:%s", err)
        }
}

在函数archiveFiles内部,通过打开 tar 文件创建一个os.File对象,然后通过向函数tar.NewWriter传递一个File对象来创建一个新的tar.WriterWriter用于将文件写入 tar 文件。

// Open the tar file
file, err := os.OpenFile(archive, flags, 0644)
if err != nil {
        return err
}
defer file.Close()
// Create a new Writer writing to given file object.
// Writer provides sequential writing of a tar archive in POSIX.1 format.
tw := tar.NewWriter(file)

要将文件集合写入 tar 文件,您需要遍历变量files,它将文件名保存为值string slice,并调用函数addToArchive将提供的文件写入 tar 文件。

for _, filename := range files {
        // Write the file into tar file.
        if err := addToArchive(filename, tw); err != nil {
                return err
        }
}

函数addToArchive使用tar.Writer将提供的文件写入 tar 文件。为了向 tar 文件写入一个新文件,通过提供tar.Header的值来调用tar.Writer对象的函数WriteHeader。然后它调用io.Copy将文件的数据写入 tar 文件。值tar.Header包含正在写入 tar 文件的文件的元数据。

file, err := os.Open(filename)
if err != nil {
        return err
}
defer file.Close()
// Get the FileInfo struct that describes the file.
fileinfo, err := file.Stat()
// Create a pointer to tar.Header struct
hdr := &tar.Header{
        ModTime: fileinfo.ModTime(),            // modified time
        Name:    filename,                      // name of header
        Size:    fileinfo.Size(),               // length in bytes
        Mode:    int64(fileinfo.Mode().Perm()), // permission and mode bits
}
// WriteHeader writes tar.Header and prepares to accept the file's contents.
if err := tw.WriteHeader(hdr); err != nil {
        return err
}
// Write the file contents to the tar file.
copied, err := io.Copy(tw, file)

函数readArchive用于读取 tar 文件的文件内容。指向tar.Reader的指针用于读取 tar 文件,它是通过调用函数tar.NewReader并传递值os.File来创建的。

// Open the tar archive file.
file, err := os.Open(archive)
if err != nil {
        return err
}
defer file.Close()
// Create the tar.Reader to read the tar archive.
// A Reader provides sequential access to the contents of a tar archive.
tr := tar.NewReader(file)

使用tar.Reader遍历 tar 文件中的文件,并读取写入os.Stdout的内容。tar.Reader的函数Next前进到文件中的下一个条目,并在文件末尾返回一个io.EOFerror值。当对函数Next的调用返回io.EOF时,您可以退出读取操作,因为这表明您已经遍历了所有文件内容并到达了文件的末尾。

// Iterate through the files in the tar archive.
for {
        hdr, err := tr.Next()
        if err == io.EOF {
                // End of tar archive
                fmt.Println("end")
                break
        }
        if err != nil {
                return err
        }
        size := hdr.Size
        contents := make([]byte, size)
        read, err := io.ReadFull(tr, contents)
        // Check the size of file contents
        if int64(read) != size {
                return fmt.Errorf("Size of the opened file doesn't match with the file %s", hdr.Name)
        }
        // hdr.Name returns the file name.
        fmt.Printf("Contents of the file %s:\n", hdr.Name)
        // Writing the file contents into Stdout.
        fmt.Fprintf(os.Stdout, "\n%s", contents)
}

在这个例子中,您试图将源文件main.goreadme.txt归档到source.tar文件中。当您运行程序时,您应该看到应用程序目录中的归档文件source.tar作为写操作的输出,文件main.goreadme.txt的内容作为读操作的输出。

编写和读取 Zip 文件

archive/zip包用于读写 zip 文件。要使用包archive/zip,您必须将其添加到导入列表中:

import (
       "archive/zip"
)

archive/zip提供了与package archive/tar相似的功能,用包zip读写 zip 文件的过程与处理 tar 文件的过程相似。struct type zip.Writer用于将文件写入 zip 文件。通过调用接受类型为io.Writer的值的函数zip.NewWriter来创建一个新的zip.Writer

结构类型zip.ReadCloser可用于从 zip 文件中读取文件。通过调用函数zip.OpenReader可以创建Reader对象,该函数将打开 name 给出的 zip 文件并返回一个zip.ReadCloser。包zip还提供了一个类型Reader;通过调用函数zip.NewReader创建一个新的Reader

清单 5-8 显示了一个示例程序,它演示了如何通过将两个文件写入一个 zip 文件,然后通过遍历 zip 文件中包含的文件并读取每个文件的内容来读取 zip 文件,从而对文件进行归档。

package main

import (
        "archive/zip"
        "fmt"
        "io"
        "log"
        "os"
)

// addToArchive writes a given file into a zip file.
func addToArchive(filename string, zw *zip.Writer) error {
        // Open the given file to archive into a zip file.
        file, err := os.Open(filename)
        if err != nil {
                return err
        }
        defer file.Close()
        // Create adds a file to the zip file using the given name/
        // Create returns a io.Writer to which the file contents should be written.
        wr, err := zw.Create(filename)
        if err != nil {
                return err
        }
        // Write the file contents to the zip file.
        if _, err := io.Copy(wr, file); err != nil {
                return err
        }
        return nil
}

// archiveFiles archives a group of given files into a zip file.
func archiveFiles(files []string, archive string) error {
        flags := os.O_WRONLY | os.O_CREATE | os.O_TRUNC
        // Open the tar file
        file, err := os.OpenFile(archive, flags, 0644)
        if err != nil {
                return err
        }
        defer file.Close()
        // Create zip.Writer that implements a zip file writer.
        zw := zip.NewWriter(file)
        defer zw.Close()
        // Iterate through the files to write each file into the zip file.
        for _, filename := range files {
                // Write the file into tar file.
                if err := addToArchive(filename, zw); err != nil {
                        return err
                }
        }
        return nil
}

// readArchive reads the file contents from tar file.
func readArchive(archive string) error {
        // Open the zip file specified by name and return a ReadCloser.
        rc, err := zip.OpenReader(archive)
        if err != nil {
                return err
        }
        defer rc.Close()
        // Iterate through the files in the zip file to read the file contents.
        for _, file := range rc.File {
                frc, err := file.Open()
                if err != nil {
                        return err
                }
                defer frc.Close()
                fmt.Fprintf(os.Stdout, "Contents of the file %s:\n", file.Name)
                // Write the contents into Stdout
                copied, err := io.Copy(os.Stdout, frc)
                if err != nil {
                        return err
                }
                // Check the size of the file.
                if uint64(copied) != file.UncompressedSize64 {
                        return fmt.Errorf("Length of the file contents doesn't match with the file %s", file.Name)
                }
                fmt.Println()
        }
        return nil
}

func main() {
        // Name of the zip file
        archive := "source.zip"
        // Files to be archived in zip format.
        files := []string{"main.go", "readme.txt"}
        // Archive files into zip format.
        err := archiveFiles(files, archive)
        if err != nil {
                log.Fatalf("Error while writing to zip file:%s\n", err)
        }
        // Read the file contents of tar file.
        err = readArchive(archive)
        if err != nil {
                log.Fatalf("Error while reading the zip file:%s\n", err)

        }
}

Listing 5-8.Writing and Reading a Zip File

这个例子类似于清单 8-7,只是在读写 tar 和 zip 格式文件的实现上有一些不同。当您运行程序时,您应该看到应用程序目录中的归档文件source.zip作为写操作的输出,文件main.goreadme.txt的内容作为读操作的输出。

六、数据持久化

当您构建真实世界的应用程序时,您可能需要将应用程序数据保存到持久存储中。您可以使用各种 Go 类型定义应用程序的数据模型,尤其是结构。在大多数用例中,您可能需要将应用程序数据保存到数据库中。本章向您展示了如何将应用程序数据持久化到数据库中,如 MongoDB、RethinkDB、InfluxDB 和 PostgreSQL。MongoDB 是一个流行的 NoSQL 数据库,广泛用于许多现代应用程序。RethinkDB 是另一个带有实时功能的 NoSQL 数据库,允许您构建实时 web 应用程序。时间序列数据库正在成为数据管理技术中的下一个大事件,因此本章包括使用 InfluxDB 的方法,这是一个用 Go 编写的流行的时间序列数据库。本章还提供了使用传统 SQL 数据库的方法。

6-1.用 MongoDB 持久化数据

问题

您希望使用 MongoDB 作为 Go 应用程序的数据库。

解决办法

第三方包mgo为 Go 提供了一个全功能的 MongoDB 驱动程序,它允许您从 Go 应用程序中使用 MongoDB。mgo驱动程序已广泛用于生产 Go 应用。

它是如何工作的

MongoDB 是一个流行的 NoSQL 数据库,被广泛用作各种现代应用程序的数据库,包括 web 和移动应用程序。MongoDB 是一个开源文档数据库,它提供了高性能、高可用性和自动伸缩。MongoDB 将数据作为文档存储在一种称为二进制 JSON (BSON)的二进制表示中。简而言之,MongoDB 是 BSON 文档的数据存储。如果您想将 MongoDB 与关系数据库管理系统(RDBMS)进行比较,BSON 文档的集合类似于关系数据库中的数据库表,集合中的单个文档类似于关系数据库中的一行表。因为 MongoDB 将数据存储为文档,所以不能将集合与表进行比较。例如,您可以在一个文档中嵌入文档来实现父子关系,而您可以通过在关系数据库中指定外键来将数据保存在两个单独的表中。甚至 NoSQL 数据库也不支持其数据模型中的约束。像大多数 NoSQL 数据库一样,MongoDB 是一个无模式数据库,这意味着数据库在集合内的每个文档中可以有不同的字段集,并且每个字段可以有不同的类型。要获得关于 MongoDB 的更多细节,以及下载和安装的说明,请访问 MongoDB 网站 https://www.mongodb.org/

Note

NoSQL(通常不仅仅指 SQL)数据库提供了一种存储和检索数据的机制,它提供了一种设计数据模型的方法,而不是关系数据库中使用的表格关系。NoSQL 数据库旨在应对现代应用程序开发挑战,例如以更容易的可伸缩性和更好的性能处理大量数据。与关系数据库相比,NoSQL 数据库可以提供高性能、更好的可伸缩性和更便宜的存储。NoSQL 数据库有不同的类型:文档数据库、图形存储、键/值存储和宽列存储。

第三方包mgo,发音为“mango”,提供了对使用 MongoDB 数据库的支持,它的子包bson实现了使用 BSON 文档的 BSON 规范。诸如slicemapstruct之类的 Go 类型的值可以保存到 MongoDB 中。当对 MongoDB 执行写操作时,包mgo自动将 Go 类型的值序列化为 BSON 文档。在大多数用例中,您可以通过使用结构来定义您的数据模型,并对其执行 CRUD 操作。

安装 mgo

要安装软件包mgo,运行以下命令:

go get gopkg.in/mgo.v2

这将获取包mgo及其子包bson。要使用mgo包,您必须将gopkg.in/mgo.v2添加到导入列表中。

import "gopkg.in/mgo.v2"

如果您想使用bson包,您必须将gopkg.in/mgo.v2/bson添加到导入列表中:

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

正在连接到 MongoDB

要使用 MongoDB 执行 CRUD 操作,首先要使用函数Dial获得一个 MongoDB 会话,如下所示:

session, err := mgo.Dial("localhost")

函数Dial建立到由url参数标识的 MongoDB 服务器集群的连接,并返回指向mgo.Session的指针,该指针用于对 MongoDB 数据库执行 CRUD 操作。功能Dial支持与服务器集群的连接,如下图所示:

session, err := mgo.Dial("server1.mongolab.com,server2.mongolab.com")

您还可以使用函数DialWithInfo建立到一个或一个服务器集群的连接,该函数返回mgo.Session。此函数允许您使用类型mgo.DialInfo向服务器传递定制信息,如下所示:

mongoDialInfo := &mgo.DialInfo{
            Addrs:    []string{"localhost"},
            Timeout:  60 * time.Second,
            Database: "bookmarkdb",
            Username: "shijuvar",
            Password: "password123",
        }   

 session, err := mgo.DialWithInfo(mongoDialInfo)

所有会话方法都是并发安全的,因此您可以从多个 goroutines 调用它们。根据通过mgo.Session指定的一致性模式执行读取操作。会话的方法SetMode用于改变会话对象的一致性模式。有三种类型的可用一致性模式:最终、单调和强。如果没有明确指定一致性模式,默认模式是强模式。在强一致性模式下,将始终使用唯一的连接对主服务器进行读取和写入,以便它们完全一致、有序,并观察最新的数据。

使用集合

MongoDB 将数据存储为文档,文档被组织成集合。CRUD 操作是针对一个集合执行的,该集合被映射到包mgo中的类型mgo.Collection。类型为mgo.Database的方法C用于创建一个mgo.Collection对象。mgo.Database类型表示 MongoDB 的命名数据库,它是通过调用类型mgo.Session的方法DB创建的。

下面的语句创建了一个指向mgo.Collection的指针,它表示在"bookmarkdb"数据库中名为"bookmarks"的 MongoDB 集合。

collection := session.DB("bookmarkdb").C("bookmarks")

执行 CRUD 操作

一旦获得了一个Session,就可以对一个Collection值执行 CRUD 操作。让我们编写一个示例程序来演示针对Collection值的持久化和读取操作。首先,我们在两个源文件中编写示例程序:bookmark_store.gomain.go。清单 6-1 显示了bookmark_store.go文件的源代码,该文件包含一个用于定义数据模型的名为Bookmark的结构,以及一个为执行 CRUD 操作提供持久化逻辑的结构类型BookmarkStore

package main

import (
        "time"

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

// Bookmark type represents the metadata of a bookmark.
type Bookmark struct {
        ID                          bson.ObjectId `bson:"_id,omitempty"`
        Name, Description, Location string
        Priority                    int // Priority (1 -5)
        CreatedOn             time.Time
        Tags                        []string
}

// BookmarkStore provides CRUD operations against the collection "bookmarks".
type BookmarkStore struct {
        C *mgo.Collection
}

// Create inserts the value of struct Bookmark into collection.
func (store BookmarkStore) Create(b *Bookmark) error {
        // Assign a new bson.ObjectId
        b.ID = bson.NewObjectId()
        err := store.C.Insert(b)
        return err
}

//Update modifies an existing value of a collection.
func (store BookmarkStore) Update(b Bookmark) error {
        // partial update on MogoDB
        err := store.C.Update(bson.M{"_id": b.ID},
                bson.M{"$set": bson.M{
                        "name":        b.Name,
                        "description": b.Description,
                        "location":    b.Location,
                        "priority":    b.Priority,
                        "tags":        b.Tags,
                }})
        return err
}

// Delete removes an existing value from the collection.
func (store BookmarkStore) Delete(id string) error {
        err := store.C.Remove(bson.M{"_id": bson.ObjectIdHex(id)})
        return err
}

// GetAll returns all documents from the collection.
func (store BookmarkStore) GetAll() []Bookmark {
        var b []Bookmark
        iter := store.C.Find(nil).Sort("priority", "-createdon").Iter()
        result := Bookmark{}
        for iter.Next(&result) {
                b = append(b, result)
        }
        return b
}

// GetByID returns single document from the collection.
func (store BookmarkStore) GetByID(id string) (Bookmark, error) {
        var b Bookmark
        err := store.C.FindId(bson.ObjectIdHex(id)).One(&b)
        return b, err
}

// GetByTag returns all documents from the collection filtering by tags.
func (store BookmarkStore) GetByTag(tags []string) []Bookmark {
        var b []Bookmark
        iter := store.C.Find(bson.M{"tags": bson.M{"$in": tags}}).Sort("priority", "-createdon").Iter()
        result := Bookmark{}
        for iter.Next(&result) {
                b = append(b, result)
        }
        return b
}

Listing 6-1.Data Model and Persistence Logic in bookmark_store.go

名为Bookmark的结构被声明为示例程序的数据模型。

type Bookmark struct {
        ID                          bson.ObjectId `bson:"_id,omitempty"`
        Name, Description, Location string
        Priority                    int // Priority (1 -5)
        CreatedOn              time.Time
        Tags                        []string
}

字段ID的类型被指定为bson.ObjectId,它是一个 12 字节的值,并且用 BSON 表示法中的_id映射这个字段。当您插入一个文档时,您需要为字段_id提供一个唯一的值ObjectId,作为主键。如果在插入操作期间,文档在其根级别(顶层字段)中不包含字段_id,则mgo驱动程序通过提供唯一值ObjectId来添加字段_id

名为BookmarkStore的结构被声明用于提供持久化逻辑,该逻辑使用结构Bookmark;进行插入和更新操作,它接受值it,对于读取操作,它返回相同类型的值。结构BookmarkStore有一个带type mgo.Collection的字段C。通过访问字段C执行所有 CRUD 操作。从源文件main.go(见清单 6-2),Collection对象被提供给BookmarkStore值,并通过访问BookmarkStore的方法执行 CRUD 操作。

type BookmarkStore struct {
        C *mgo.Collection
}

在集合中创建文档

BookmarkStore的方法Create用于将值插入名为"bookmarks"的 MongoDB 集合中。它接受指向Bookmark的指针,并使用CollectionInsert方法将Bookmark的值插入到 MongoDB 集合中。当执行插入操作时,包mgo自动将 Go 类型的值编码成 BSON 规范。

func (store BookmarkStore) Create(b *Bookmark) error {
        // Assign a new bson.ObjectId
        b.ID = bson.NewObjectId()
        err := store.C.Insert(b)
        return err
}

通过调用函数bson.NewObjectId生成唯一值ObjectId,并将其分配给字段ID,该字段在 BSON 文档中标记为字段_i d。类型Collection的函数Insert用于将文档插入到集合中。

更新集合中的文档

类型为Collection的函数Update用于更新现有文档。方法Update从集合中查找与所提供的选择器文档匹配的单个文档,并用所提供的值修改该文档。关键字"$set"用于对文档进行部分更新。

func (store BookmarkStore) Update(b Bookmark) error {
        // partial update on MogoDB
        err := store.C.Update(bson.M{"_id": b.ID},
                bson.M{"$set": bson.M{
                        "name":        b.Name,
                        "description": b.Description,
                        "location":    b.Location,
                        "priority":    b.Priority,
                        "tags":        b.Tags,
                }})
        return err
}

类型bson.M用于为Collection的方法Update提供值。这个类型是带有map[string]interface{}签名的类型map的一个方便的别名,对于以本地方式处理 BSON 很有用。每当您想以本机方式处理 BSON 文档时,可以提供bson.M的值,这对Collection对象的UpdateReadDelete操作很有用。

从集合中删除文档

CollectionRemove功能用于从集合中删除文档。这里的文档是为给定的id移除的。

func (store BookmarkStore) Delete(id string) error {
        err := store.C.Remove(bson.M{"_id": bson.ObjectIdHex(id)})
        return err
}

从集合中读取文档

BookmarkStore的方法GetAll返回集合中的所有文档。CollectionFind方法用于从集合中查询文档。方法Find返回一个指向mgo.Query的指针,稍后可以使用函数OneForIterTail来检索文档。

func (store BookmarkStore) GetAll() []Bookmark {
        var b []Bookmark
        iter := store.C.Find(nil).Sort("priority", "-createdon").Iter()
        result := Bookmark{}
        for iter.Next(&result) {
                b = append(b, result)
        }
        return b
}

一个nil值作为选择器文档被提供给方法Find以从集合中获取所有文档。产生的mgo.Query值表示对给定选择器文档执行的结果集。使用函数Sort,得到的Query值可用于根据字段值对文档进行排序。这里,对字段priority按升序进行排序操作,对createdon按降序进行排序操作。要按降序排序,只需将"-"作为字段名的前缀,如下所示的字段createdon

iter := store.C.Find(nil).Sort("priority", "-createdon").Iter()

Query的方法Iter返回一个迭代器,能够迭代所有生成的结果,函数Next从结果集中检索下一个文档。

BookmarkStore的方法GetByID为 BSON 文档中给定的id (_id返回单个文档。这里的id是作为string提供的,因此使用函数bson.ObjectIdHex将其转换为bson.ObjectId

func (store BookmarkStore) GetByID(id string) (Bookmark, error) {
        var b Bookmark
        err := store.C.FindId(bson.ObjectIdHex(id)).One(&b)
        return b, err
}

在这个例子中,Collection中的文档也被查询给定的标签,作为stringslice,它返回与给定标签匹配的文档。

func (store BookmarkStore) GetByTag(tags []string) []Bookmark {
        var b []Bookmark
        iter := store.C.Find(bson.M{"tags": bson.M{"$in": tags}}).Sort("priority", "-createdon").Iter()
        result := Bookmark{}
        for iter.Next(&result) {
                b = append(b, result)
        }
        return b
}

查询操作符$in允许您使用匹配值列表中任何值的表达式来过滤文档。这里的$in操作符用于过滤tag字段中的文档。这里,如果任何给定的标签与字段tags匹配,查询将返回所有文档。

让我们重用bookmark_store.go的函数对 MongoDB 数据库执行 CRUD 操作。清单 6-2 显示了源文件main.go,它通过提供一个mgo.Collection值并调用其方法来创建一个BookmarkStore类型的实例。

package main

import (
        "fmt"
        "log"
        "time"

        "gopkg.in/mgo.v2"
)

var store BookmarkStore
var id string

// init will invoke before the function main.
func init() {
        session, err := mgo.DialWithInfo(&mgo.DialInfo{
                Addrs:   []string{"127.0.0.1"},
                Timeout: 60 * time.Second,
        })
        if err != nil {
                log.Fatalf("[MongoDB Session]: %s\n", err)
        }
        collection := session.DB("bookmarkdb").C("bookmarks")

        store = BookmarkStore{
                C: collection,
        }
}

// Create and update documents.
func createUpdate() {
        bookmark := Bookmark{
                Name:        "mgo",
                Description: "Go driver for MongoDB",
                Location:    "https://github.com/go-mgo/mgo",
                Priority:    2,
                CreatedOn:   time.Now(),
                Tags:        []string{"go", "nosql", "mongodb"},
        }
        // Insert a new document.
        if err := store.Create(&bookmark); err != nil {
                log.Fatalf("[Create]: %s\n", err)
        }
        id = bookmark.ID.Hex()
        fmt.Printf("New bookmark has been inserted with ID: %s\n", id)
        // Update an existing document.
        bookmark.Priority = 1
        if err := store.Update(bookmark); err != nil {
                log.Fatalf("[Update]: %s\n", err)
        }
        fmt.Println("The value after update:")
              // Retrieve the updated document
        getByID(id)

        bookmark = Bookmark{
                Name:        "gorethink",
                Description: "Go driver for RethinkDB",
                Location:    "https://github.com/dancannon/gorethink",
                Priority:    3,
                CreatedOn:   time.Now(),
                Tags:        []string{"go", "nosql", "rethinkdb"},
        }
        // Insert a new document.
        if err := store.Create(&bookmark); err != nil {
                log.Fatalf("[Create]: %s\n", err)
        }
        id = bookmark.ID.Hex()
        fmt.Printf("New bookmark has been inserted with ID: %s\n", id)

}

// Get a document by given id.
func getByID(id string) {
        bookmark, err := store.GetByID(id)
        if err != nil {
                log.Fatalf("[GetByID]: %s\n", err)
        }
        fmt.Printf("Name:%s, Description:%s, Priority:%d\n",
                bookmark.Name, bookmark.Description, bookmark.Priority)
}

// Get all documents from the collection.
func getAll() {
        // Layout for formatting dates.
        layout := "2006-01-02 15:04:05"
        // Retrieve all documents.
        bookmarks := store.GetAll()
        fmt.Println("Read all documents")
        for _, v := range bookmarks {
                fmt.Printf("Name:%s, Description:%s, Priority:%d, CreatedOn:%s\n",
                        v.Name, v.Description, v.Priority, v.CreatedOn.Format(layout))
        }
}

// Get documents by tags.
func getByTags() {
        layout := "2006-01-02 15:04:05"
        fmt.Println("Query with Tags - 'go, nosql'")
        bookmarks := store.GetByTag([]string{"go", "nosql"})
        for _, v := range bookmarks {
                fmt.Printf("Name:%s, Description:%s, Priority:%d, CreatedOn:%s\n",
                        v.Name, v.Description, v.Priority, v.CreatedOn.Format(layout))
        }
        fmt.Println("Query with Tags - 'mongodb'")
        bookmarks = store.GetByTag([]string{"mongodb"})
        for _, v := range bookmarks {
                fmt.Printf("Name:%s, Description:%s, Priority:%d, CreatedOn:%s\n",
                        v.Name, v.Description, v.Priority, v.CreatedOn.Format(layout))
        }
}

// Delete an existing document from the collection.
func delete() {
        if err := store.Delete(id); err != nil {
                log.Fatalf("[Delete]: %s\n", err)
        }
        bookmarks := store.GetAll()
        fmt.Printf("Number of documents in the collection after delete:%d\n", len(bookmarks))
}

// main - entry point of the program.
func main() {
        createUpdate()
        getAll()
        getByTags()
        delete()
}

Listing 6-2.Perform CRUD Operations on a MongoDB Collection by Using the Type BookmarkStore, in main.go

在调用函数main之前执行的函数init中,使用函数DialWithInfo获得一个mgo.Sessi on 值,然后创建一个mgo.Collection值以提供类型BookmarkStoreBookmarkStore的值用于对数据库"bookmarkdb"中名为"bookmarks"的集合执行 CRUD 操作。

var store BookmarkStore
var id string

func init() {
        session, err := mgo.DialWithInfo(&mgo.DialInfo{
                Addrs:   []string{"127.0.0.1"},
                Timeout: 60 * time.Second,
        })
        if err != nil {
                log.Fatalf("[MongoDB Session]: %s\n", err)
        }
        collection := session.DB("bookmarkdb").C("bookmarks")
        store = BookmarkStore{
                C: collection,
        }
}

创建和更新操作在函数createUpdate中实现,其中两个文档被插入到集合中,一个现有文档被更新。

func createUpdate() {
        bookmark := Bookmark{
                Name:        "mgo",
                Description: "Go driver for MongoDB",
                Location:    "https://github.com/go-mgo/mgo",
                Priority:    2,
                CreatedOn:   time.Now(),
                Tags:        []string{"go", "nosql", "mongodb"},
        }
        // Insert a new document.
        if err := store.Create(&bookmark); err != nil {
                log.Fatalf("[Create]: %s\n", err)
        }
        id = bookmark.ID.Hex()
        fmt.Printf("New bookmark has been inserted with ID: %s\n", id)
        // Update an existing document.
        bookmark.Priority = 1
        if err := store.Update(bookmark); err != nil {
                log.Fatalf("[Update]: %s\n", err)
        }
        fmt.Println("The value after update:")
        // Retrieve the updated document.
        getByID(id)

        bookmark = Bookmark{
                Name:        "gorethink",
                Description: "Go driver for RethinkDB",
                Location:    "https://github.com/dancannon/gorethink",
                Priority:    3,
                CreatedOn:   time.Now(),
                Tags:        []string{"go", "nosql", "rethinkdb"},
        }
        // Insert a new document.
        if err := store.Create(&bookmark); err != nil {
                log.Fatalf("[Create]: %s\n", err)
        }
        id = bookmark.ID.Hex()
        fmt.Printf("New bookmark has been inserted with ID: %s\n", id)

}

函数getByID用于通过给定的id检索现有文档。这个函数是从函数createUpdate中调用的,以获取更新操作后的值。

func getByID(id string) {
        bookmark, err := store.GetByID(id)
        if err != nil {
                log.Fatalf("[GetByID]: %s\n", err)
        }
        fmt.Printf("Name:%s, Description:%s, Priority:%d\n", bookmark.Name, bookmark.Description, bookmark.Priority)
}

函数getAll从集合中检索所有文档,分别按照priority升序和createdon降序排序。

func getAll() {
        // Layout for formatting dates.
        layout := "2006-01-02 15:04:05"
        // Retrieve all documents.
        bookmarks := store.GetAll()
        fmt.Println("Read all documents")
        for _, v := range bookmarks {
                fmt.Printf("Name:%s, Description:%s, Priority:%d, CreatedOn:%s\n",
                        v.Name, v.Description, v.Priority, v.CreatedOn.Format(layout))
        }
}

函数getByTags通过使用tags进行过滤来检索文档。MongoDB 查询操作符$in用于过滤文档。BookmarkStore的功能GetByTag执行两次。第一次,它是通过提供标签、gonosql,来执行的,因此您将获得提供了任何标签的所有文档;在这里你会得到两份文件。第二次,它通过提供标签mongodb来执行,因此您将得到一个文档作为结果,因为只有一个文档具有给定的标签。

func getByTags() {
        layout := "2006-01-02 15:04:05"
        fmt.Println("Query with Tags - 'go, nosql'")
        bookmarks := store.GetByTag([]string{"go", "nosql"})
        for _, v := range bookmarks {
                fmt.Printf("Name:%s, Description:%s, Priority:%d, CreatedOn:%s\n",
                        v.Name, v.Description, v.Priority, v.CreatedOn.Format(layout))
        }
        fmt.Println("Query with Tags - 'mongodb'")
        bookmarks = store.GetByTag([]string{"mongodb"})
        for _, v := range bookmarks {
                fmt.Printf("Name:%s, Description:%s, Priority:%d, CreatedOn:%s\n",
                        v.Name, v.Description, v.Priority, v.CreatedOn.Format(layout))
        }
}

函数delete用于通过给定的id删除已有的文档。

func delete() {
        if err := store.Delete(id); err != nil {
                log.Fatalf("[Delete]: %s\n", err)
        }
        bookmarks, err := store.GetAll()
        if err != nil {
                log.Fatalf("[GetAll]: %s\n", err)
        }
        fmt.Printf("Number of documents in the table after delete:%d\n", len(bookmarks))
}

从函数main中,调用函数来演示 CRUD 操作。

func main() {
        createUpdate()
        getAll()
        getByTags()
        delete()
}

让我们运行示例程序。您应该会看到类似如下的输出:

New bookmark has been inserted with ID: 57809514f7e02124b042281d
The value after update:
Name:mgo, Description:Go driver for MongoDB, Priority:1
New bookmark has been inserted with ID: 57809514f7e02124b042281e
Read all documents
Name:mgo, Description:Go driver for MongoDB, Priority:1, CreatedOn:2016-07-09 11:39:24
Name:gorethink, Description:Go driver for RethinkDB, Priority:3, CreatedOn:2016-07-09 11:39:24
Query with Tags - 'go, nosql'
Name:mgo, Description:Go driver for MongoDB, Priority:1, CreatedOn:2016-07-09 11:39:24
Name:gorethink, Description:Go driver for RethinkDB, Priority:3, CreatedOn:2016-07-09 11:39:24
Query with Tags - 'mongodb'
Name:mgo, Description:Go driver for MongoDB, Priority:1, CreatedOn:2016-07-09 11:39:24
Number of documents in the collection after delete:1

6-2.用 RethinkDB 保存数据

问题

您希望使用 RethinkDB 作为 Go 应用程序的数据库。您还想使用 RethinkDB 的实时功能。

解决办法

第三方软件包gorethink为 Go 提供了一个全功能的 RethinkDB 驱动程序,允许您从 Go 应用程序中使用 RethinkDB。该软件包还允许您使用 RethinkDB 实时订阅和更改数据馈送。

它是如何工作的

RethinkDB 是一个 NoSQL 的、可伸缩的 JSON 数据库,它提供了许多类似于 MongoDB 的功能。RethinkDB 将 JSON 文档组织成表格存储。RethinkDB 中的一个Table是 JSON 文档的集合。除了人们熟悉的 NoSQL 数据库的功能之外,RethinkDB 还为其数据库引擎提供了实时功能,从而大大简化了实时 web 应用程序的构建。实时 web 应用程序可以将实时更新推送到客户端应用程序,而不是客户端应用程序定期检查服务器的新更新。当您编写实时应用程序时,这可以提高工作效率。使用 WebSocket 协议的 Go 实现,您可以使您的 web 应用程序成为实时应用程序。通过将这一点与 RethinkDB 数据库的实时功能相结合,您可以创建优秀的实时 web 应用程序。当您的实时 web 应用程序使用 RethinkDB 时,您可以订阅实时变更提要;当数据库中有任何变化时,您可以将这些变化推送到您的客户端应用程序。有关 RethinkDB 的更多详细信息,包括安装说明,请访问网站 https://www.rethinkdb.com/

Note

Go 包golang.org/x/net/websocketgithub.com/gorilla/websocket实现了 RFC 6455 ( https://tools.ietf.org/html/rfc6455 )中指定的 WebSocket 协议的客户端和服务器。

安装 gorethink

要安装软件包gorethink,运行以下命令:

go get github.com/dancannon/gorethink

要使用包gorethink,您必须将github.com/dancannon/gorethink添加到导入列表中。

import " github.com/dancannon/gorethink"

连接到 RethinkDB

要使用 RethinkDB 执行 CRUD 操作,首先要使用函数Connect获得一个 RethinkDB 会话,如下所示:

session, err := gorethink.Connect(r.ConnectOpts{
                Address:  "localhost:28015",
                             Database: "bookmarkdb",
 })

要配置连接池,可以在调用函数Connect时指定ConnectOpts类型的属性,如MaxIdleMaxOpenTimeout,如下所示:

session, err := gorethink.Connect(gorethink.ConnectOpts{
                Address:  "localhost:28015",        
                            Database: "bookmarkdb",         
                MaxIdle:  10,
                MaxOpen:  10,
 })

您可以通过调用Session的方法来更改MaxIdleMaxOpen属性,如下所示:

session.SetMaxIdleConns(57)
session.SetMaxOpenConns(5)

要连接到具有多个节点的 RethinkDB 服务器群集,可以使用以下语法。当连接到具有多个节点的集群时,查询分布在这些节点中。

session, err := gorethink.Connect(gorethink.ConnectOpts{
    Addresses: []string{"localhost:28015", "localhost:28016"},
    Database: " bookmarkdb",
    AuthKey:  "14daak1cad13dj",
    DiscoverHosts: true,
})

AuthKey用于保护 RethinkDB 集群。

执行 CRUD 操作

一旦获得了一个Session对象,就可以对代表 JSON 文档集合的Table执行 CRUD 操作。

让我们编写一个示例程序来演示使用 RethinkDB 的持久化和读操作。让我们在两个源文件中编写示例程序:bookmark_store.gomain.go。清单 6-3 显示了bookmark_store.go文件的源代码,该文件包含一个用于定义数据模型的名为Bookmark的结构,以及一个为针对名为"bookmarks"的表执行 CRUD 操作提供持久化逻辑的结构类型BookmarkStore

package main

import (
        "time"

        r "github.com/dancannon/gorethink"
)

// Bookmark type represents the metadata of a bookmark.
type Bookmark struct {
        ID                          string `gorethink:"id,omitempty" json:"id"`
        Name, Description, Location string
        Priority                    int // Priority (1 -5)
        CreatedOn                   time.Time
        Tags                        []string
}

// BookmarkStore provides CRUD operations against the Table "bookmarks".
type BookmarkStore struct {
        Session *r.Session
}

// Create inserts the value of struct Bookmark into Table.
func (store BookmarkStore) Create(b *Bookmark) error {

        resp, err := r.Table("bookmarks").Insert(b).RunWrite(store.Session)
        if err == nil {
                b.ID = resp.GeneratedKeys[0]
        }

        return err
}

// Update modifies an existing value of a Table.
func (store BookmarkStore) Update(b *Bookmark) error {

        var data = map[string]interface{}{
                "name":        b.Name,
                "description": b.Description,
                "location":    b.Location,
                "priority":    b.Priority,
                "tags":        b.Tags,
        }
        // partial update on RethinkDB
        _, err := r.Table("bookmarks").Get(b.ID).Update(data).RunWrite(store.Session)
        return err
}

// Delete removes an existing value from the Table.
func (store BookmarkStore) Delete(id string) error {
        _, err := r.Table("bookmarks").Get(id).Delete().RunWrite(store.Session)
        return err
}

// GetAll returns all documents from the Table.
func (store BookmarkStore) GetAll() ([]Bookmark, error) {
        bookmarks := []Bookmark{}

        res, err := r.Table("bookmarks").OrderBy("priority", r.Desc("date")).Run(store.Session)
        err = res.All(&bookmarks)
        return bookmarks, err
}

// GetByID returns single document from the Table.
func (store BookmarkStore) GetByID(id string) (Bookmark, error) {
        var b Bookmark
        res, err := r.Table("bookmarks").Get(id).Run(store.Session)
        res.One(&b)
        return b, err
}

Listing 6-3.Data Model and Persistence Logic in bookmark_store.go

名为Bookmark的结构被声明为示例程序的数据模型。

type Bookmark struct {
        ID                          string `gorethink:"id,omitempty" json:"id"`
        Name, Description, Location string
        Priority                    int // Priority (1 -5)
        CreatedOn              time.Time
        Tags                        []string
}

结构字段ID在文档的 JSON 表示中用 RethinkDB Table,idid标记。RethinkDB 将为字段id自动生成一个UUID

名为BookmarkStore的结构被声明用于提供使用数据模型结构Bookmark的持久化逻辑。结构BookmarkStore有一个类型为gorethink.Session的字段Session。所有 CRUD 操作都是通过访问使用字段SessionBookmarkStore的方法来执行的。

type BookmarkStore struct {
        Session *r.Session
}

在表格中创建文档

BookmarkStore的方法Create用于将值插入名为bookmarks的表中。它接受一个指向Bookmark的指针,并将Bookmark的值插入表中。当执行插入和更新操作时,gorethink包在发送到服务器之前将结构值编码到一个map中。

func (store BookmarkStore) Create(b *Bookmark) error {

        resp, err := r.Table("bookmarks").Insert(b).RunWrite(store.Session)
        if err == nil {
                b.ID = resp.GeneratedKeys[0]
        }
        return err
}

类型gorethink.Term表示写和读查询。在包gorethink中,方法是可链接的,因此您可以轻松地构造查询。在前面的方法中,函数Table和方法Insert返回一个gorethink.Term值。函数RunWrite运行一个查询,然后返回一个WriteResponse类型的值。通过访问WriteResponse值的GeneratedKeys字段,可以得到id值。函数RunWrite用于执行写查询,如InsertUpdateDeleteDBCreateTableCreate等。

更新表格中的文档

为了更新一个现有的文档,提供一个带有签名map[string]interface{}的值map作为表中要更新的值。

func (store BookmarkStore) Update(b *Bookmark) error {

        var data = map[string]interface{}{
                "name":        b.Name,
                "description": b.Description,
                "location":    b.Location,
                "priority":    b.Priority,
                "tags":        b.Tags,
        }

        // partial update on RethinkDB
        _, err := r.Table("bookmarks").Get(b.ID).Update(data).RunWrite(store.Session)
        return err
}

从表格中删除文档

类型为Term的方法Delete用于运行删除查询,从表中删除现有文档。

func (store BookmarkStore) Delete(id string) error {
        _, err := r.Table("bookmarks").Get(id).Delete().RunWrite(store.Session)
        return err
}

从桌上阅读文件

函数Run用于运行读取查询。函数Run返回一个作为查询结果的gorethink.Cursor值。通过使用OneAllNextNextResponse等方法,您可以将文档检索到您的 Go 类型中。类型为BookmarkStore的方法GetAllbookmarks表中返回所有文档,该表按priority升序和createdon降序排序。默认情况下,排序是按升序执行的,所以如果您想按降序排序,可以使用函数Desc

func (store BookmarkStore) GetAll() ([]Bookmark, error) {
        bookmarks := []Bookmark{}
        res, err := r.Table("bookmarks").OrderBy("priority", r.Desc("createdon")).Run(store.Session)
        err = res.All(&bookmarks)
        return bookmarks, err
}

类型为BookmarkStore的方法GetByID为给定的id返回一个文档。

func (store BookmarkStore) GetByID(id string) (Bookmark, error) {
        var b Bookmark
        res, err := r.Table("bookmarks").Get(id).Run(store.Session)
        res.One(&b)
        return b, err
}

让我们重用bookmark_store.go的函数来对 RethinkDB Table执行 CRUD 操作。清单 6-4 显示了main.go文件中的源代码,它通过提供一个gorethink.Session值来创建一个BookmarkStore类型的实例,并调用其方法来执行 CRUD 操作。这个main.go还通过订阅表的变更提要来提供 RethinkDB 实时功能的实现。

package main

import (
        "fmt"
        "log"
        "time"

        r "github.com/dancannon/gorethink"
)

var store BookmarkStore
var id string

// initDB creates new database and
func initDB(session *r.Session) {
        var err error
        // Create Database
        _, err = r.DBCreate("bookmarkdb").RunWrite(session)
        if err != nil {
                log.Fatalf("[initDB]: %s\n", err)
        }
        // Create Table
        _, err = r.DB("bookmarkdb").TableCreate("bookmarks").RunWrite(session)
        if err != nil {
                log.Fatalf("[initDB]: %s\n", err)
        }
}

// changeFeeds subscribes real-time changes on table bookmarks.
func changeFeeds(session *r.Session) {
        bookmarks, _ := r.Table("bookmarks").Changes().Field("new_val").Run(session)
               if err != nil {
                log.Fatalf("[changeFeeds]: %s\n", err)
        }

        // Launch a goroutine to print real-time updates.
        go func() {
                var bookmark Bookmark
                for bookmarks.Next(&bookmark) {
                        if bookmark.ID == "" { // for delete, new_val will be null.
                                fmt.Println("Real-time update: Document has been deleted")
                        } else {
                                fmt.Printf("Real-time update: Name:%s, Description:%s, Priority:%d\n",
                                        bookmark.Name, bookmark.Description, bookmark.Priority)
                        }
                }
        }()
}

// init will invoke before the function main
func init() {
        session, err := r.Connect(r.ConnectOpts{
                Address:  "localhost:28015",
                Database: "bookmarkdb",
                MaxIdle:  10,
                MaxOpen:  10,
        })

        if err != nil {
                log.Fatalf("[RethinkDB Session]: %s\n", err)
        }

        // Create Database and Table.
        initDB(session)
        store = BookmarkStore{
                Session: session,
        }
        // Subscribe real-time changes
        changeFeeds(session)
}

// Create and update documents.
func createUpdate() {
        bookmark := Bookmark{
                Name:        "mgo",
                Description: "Go driver for MongoDB",
                Location:    "https://github.com/go-mgo/mgo",
                Priority:    1,
                CreatedOn:   time.Now(),
                Tags:        []string{"go", "nosql", "mongodb"},
        }
        // Insert a new document.
        if err := store.Create(&bookmark); err != nil {
                log.Fatalf("[Create]: %s\n", err)
        }
        id = bookmark.ID
        fmt.Printf("New bookmark has been inserted with ID: %s\n", id)
        // Update an existing document.
        bookmark.Priority = 2
        if err := store.Update(bookmark); err != nil {
                log.Fatalf("[Update]: %s\n", err)
        }
        fmt.Println("The value after update:")
        // Retrieve the updated document.
        getByID(id)
        bookmark = Bookmark{
                Name:        "gorethink",
                Description: "Go driver for RethinkDB",
                Location:    "https://github.com/dancannon/gorethink",
                Priority:    1,
                CreatedOn:   time.Now(),
                Tags:        []string{"go", "nosql", "rethinkdb"},
        }
        // Insert a new document.
        if err := store.Create(&bookmark); err != nil {
                log.Fatalf("[Create]: %s\n", err)
        }
        id = bookmark.ID
        fmt.Printf("New bookmark has been inserted with ID: %s\n", id)

}

// Get a document by given id.
func getByID(id string) {
        bookmark, err := store.GetByID(id)
        if err != nil {
                log.Fatalf("[GetByID]: %s\n", err)
        }
        fmt.Printf("Name:%s, Description:%s, Priority:%d\n", bookmark.Name, bookmark.Description, bookmark.Priority)
}

// Get all documents from bookmarks table.
func getAll() {
        // Layout for formatting dates.
        layout := "2006-01-02 15:04:05"
        // Retrieve all documents.
        bookmarks, err := store.GetAll()
        if err != nil {
                log.Fatalf("[GetAll]: %s\n", err)
        }
        fmt.Println("Read all documents")
        for _, v := range bookmarks {
                fmt.Printf("Name:%s, Description:%s, Priority:%d, CreatedOn:%s\n", v.Name, v.Description, v.Priority, v.CreatedOn.Format(layout))
        }

}

// Delete an existing document from bookmarks table.
func delete() {
        if err := store.Delete(id); err != nil {
                log.Fatalf("[Delete]: %s\n", err)
        }
        bookmarks, err := store.GetAll()
        if err != nil {
                log.Fatalf("[GetAll]: %s\n", err)
        }
        fmt.Printf("Number of documents in the table after delete:%d\n", len(bookmarks))
}

// main - entry point of the program
func main() {
        createUpdate()
        getAll()
        delete()
}

Listing 6-4.Perform CRUD Operations on a RethinkDB Table Using the Type BookmarkStore, in main.go

init函数中,通过使用函数Connect连接到 RethinkDB 服务器来获得一个Session值。与 MongoDB 不同,在 RethinkDB 中,您必须手动创建数据库和表。从 Go 代码本身,通过调用函数initDB创建一个名为bookmarkdb的数据库和一个名为bookmarks的表。如果你多次执行函数initDB,你会得到一个异常。用于演示 RethinkDB 实时功能的函数changeFeeds也是从init中调用的。我们将在本节稍后研究函数changeFeeds。函数init将在函数main之前被调用。

func init() {
        session, err := r.Connect(r.ConnectOpts{
                Address:  "localhost:28015",
                Database: "bookmarkdb",
                MaxIdle:  10,
                MaxOpen:  10,
        })

        if err != nil {
                log.Fatalf("[RethinkDB Session]: %s\n", err)
        }

        // Create Database and Table.
        initDB(session)
        store = BookmarkStore{
                Session: session,
        }
        // Subscribe real-time changes
        changeFeeds(session)
}

创建和更新操作在函数createUpdate中实现,其中两个文档被插入到bookmarks表中,一个现有文档被更新。

func createUpdate() {
        bookmark := Bookmark{
                Name:        "mgo",
                Description: "Go driver for MongoDB",
                Location:    "https://github.com/go-mgo/mgo",
                Priority:    1,
                CreatedOn:   time.Now(),
                Tags:        []string{"go", "nosql", "mongodb"},
        }
        // Insert a new document.
        if err := store.Create(&bookmark); err != nil {
                log.Fatalf("[Create]: %s\n", err)
        }
        id = bookmark.ID
        fmt.Printf("New bookmark has been inserted with ID: %s\n", id)
        // Update an existing document.
        bookmark.Priority = 2
        if err := store.Update(bookmark); err != nil {
                log.Fatalf("[Update]: %s\n", err)
        }
        fmt.Println("The value after update:")
        // Retrieve the updated document.
        getByID(id)
        bookmark = Bookmark{
                Name:        "gorethink",
                Description: "Go driver for RethinkDB",
                Location:    "https://github.com/dancannon/gorethink",
                Priority:    1,
                CreatedOn:   time.Now(),
                Tags:        []string{"go", "nosql", "rethinkdb"},
        }
        // Insert a new document.
        if err := store.Create(&bookmark); err != nil {
                log.Fatalf("[Create]: %s\n", err)
        }
        id = bookmark.ID
        fmt.Printf("New bookmark has been inserted with ID: %s\n", id)
}

函数getByID用于通过给定的id检索现有文档。这个函数从函数createUpdate中被调用,以在更新操作后获取值。

func getByID(id string) {
        bookmark, err := store.GetByID(id)
        if err != nil {
                log.Fatalf("[GetByID]: %s\n", err)
        }
        fmt.Printf("Name:%s, Description:%s, Priority:%d\n", bookmark.Name, bookmark.Description, bookmark.Priority)
}

函数getAll分别从按照priority升序和createdon降序排序的表中检索所有文档。

func getAll() {
        // Layout for formatting dates.
        layout := "2006-01-02 15:04:05"
        // Retrieve all documents.
        bookmarks, err := store.GetAll()
        if err != nil {
                log.Fatalf("[GetAll]: %s\n", err)
        }
        fmt.Println("Read all documents")
        for _, v := range bookmarks {
                fmt.Printf("Name:%s, Description:%s, Priority:%d, CreatedOn:%s\n", v.Name, v.Description, v.Priority, v.CreatedOn.Format(layout))
        }

}

函数delete用于通过给定的id删除现有的文档。

func delete() {
        if err := store.Delete(id); err != nil {
                log.Fatalf("[Delete]: %s\n", err)
        }
        bookmarks, err := store.GetAll()
        if err != nil {
                log.Fatalf("[GetAll]: %s\n", err)
        }
        fmt.Printf("Number of documents in the table after delete:%d\n", len(bookmarks))
}

从函数main中,调用函数来演示 CRUD 操作。

func main() {
        createUpdate()
        getAll()
        delete()
}

RethinkDB 中的更改源

RethinkDB 的实时功能是使用Changefeeds实现的,它允许 RethinkDB 数据库的客户端实时接收对表的更改。使用gorethink驱动程序,您可以通过对Table值调用函数Changes来订阅变更数据的提要。在清单 6-4 中,main.go中的函数changeFeeds实现了 RethinkDB 的Changefeeds来订阅表bookmarks上的数据,以便应用程序可以在表bookmarks上执行任何插入、更新或删除操作时接收这些提要。

func changeFeeds(session *r.Session) {
        bookmarks, _ := r.Table("bookmarks").Changes().Field("new_val").Run(session)
               if err != nil {
                log.Fatalf("[changeFeeds]: %s\n", err)
        }
        // Launch a goroutine to print real-time updates.
        go func() {
                var bookmark Bookmark
                for bookmarks.Next(&bookmark) {
                        if bookmark.ID == "" {  // for delete, new_val will be null.
                                fmt.Println("Real-time update: Document has been deleted")
                        } else {
                                fmt.Printf("Real-time update: Name:%s, Description:%s, Priority:%d\n",
                                        bookmark.Name, bookmark.Description, bookmark.Priority)
                        }
                }
        }()
}

调用函数Changes来订阅字段new_val的重新思考数据库的Changefeeds。当对表执行任何更新时,Changefeeds功能可以提供两个值:old_valnew_valold_val是文档的旧版本,而new_val是文档的新版本。在 insert 上,old_val将是null;在删除时,new_val将成为null。更新时,old_valnew_val都存在。在功能changeFeeds中,订阅了new_val。可以在处理函数中订阅Changefeeds的输出,以对Changefeeds提供的值执行操作。这里,处理函数是在一个 goroutine 中实现的,因此它将在后台异步执行,而不会阻塞任何执行。通过与 goroutines 和 channels 结合,您可以使用 Go 和 RethinkDB 创建高效的实时应用程序。这里,控制台窗口中打印出字段new_val结果的Changefeeds。对于删除操作,new_val将是null,这样您就不会在删除操作中访问Changefeeds的任何值。当对表bookmarks执行任何插入、更新或删除操作时,Changefeeds功能将提供提要。通过执行Run,可从函数Changes提供的Cursor值访问Changefeedsnew_val值。通过调用Cursor value 的函数Next,可以检索Changefeeds提供的值。

让我们运行清单 6-4 中编写的程序。您应该会看到类似如下的输出:

Real-time update: Name:mgo, Description:Go driver for MongoDB, Priority:1
New bookmark has been inserted with ID: f487b133-6f19-4b3b-8dfa-4d652b2f1c1b
Real-time update: Name:mgo, Description:Go driver for MongoDB, Priority:2
The value after update:
Name:mgo, Description:Go driver for MongoDB, Priority:2
Real-time update: Name:gorethink, Description:Go driver for RethinkDB, Priority:1
New bookmark has been inserted with ID: ee6a19c8-efa5-4672-ae62-37d8b0ea060f
Read all documents
Name:gorethink, Description:Go driver for RethinkDB, Priority:1, CreatedOn:2016-07-08 20:03:50
Name:mgo, Description:Go driver for MongoDB, Priority:2, CreatedOn:2016-07-08 20:03:49
Real-time update: Document has been deleted
Number of documents in the table after delete:1

输出显示Changefeeds功能提供了对表书签的实时更新。

6-3.使用 InfluxDB 处理时间序列数据

问题

您希望使用时间序列数据来构建时间序列图表和实时数据分析。

解决办法

InfluxDB 是一个用 Go 编写的时间序列数据库。InfluxDB 提供了一个本地 Go 客户端库(github.com/influxdata/influxdb/client/v2)来处理来自 Go 应用程序的 InfluxDB。

它是如何工作的

时间序列数据处理和实时数据分析是大数据和数据管理技术的下一件大事。InfluxDB 是 InfluxData 平台的一部分,是一个时间序列数据库,允许您有效地存储时间序列数据。InfluxDB 包括一个本地 Go 客户端库,它提供了读写时间序列数据的便利函数。它使用 HTTP 协议与您的 InfluxDB 集群通信。

时间序列数据库

时间序列数据是一系列数据点,通常由一段时间间隔内的连续测量值组成。当您基于时间序列数据构建图表时,其中一个轴将始终是时间(年、日、小时、分钟)。时间序列数据处理是建立预测模型和预测的重要数据管理方法。时序数据库(TSDB)是一种用于管理和存储时序数据的数据库。InfluxDB 由 InfluxData 平台提供,是市场上最流行的 TSDBs 之一。

主要概念 InfluxDB

InfluxDB 中的数据管理不同于传统的数据管理系统。以下是 InfluxDB 中关键概念的总结:

  • 数据库:InfluxDB 中的高层实体。一个 InfluxDB 实例中可以有多个数据库。
  • 度量:将时间序列数据保存到度量中。度量类似于关系表。当您基于时间序列数据构建图形时,测量值就是图形的名称。
  • 点:度量包含点,就像关系表包含记录一样。点包含强制字段和时间戳。时间戳指定该点的时间,并且字段用于存储该时间戳的数据。一个点可以有标签,标签是时间序列数据的元数据。
  • 时间戳:度量中的每个点都包含一个时间戳,因为 InfluxDB 是一个 TSDB。如果在创建新点时没有提供时间戳,InfluxDB 会自动为该点创建一个新的时间戳。点中的时间戳指定了它的创建时间。构建图表时,一个轴是时间,另一个轴是字段的值。
  • 字段集:字段的集合称为字段集。
  • 标签:点中的标签是被索引的元数据。请记住,测量是在标签上索引的,而不是在字段上。
  • 标签集:所有标签的集合称为标签集。
  • 系列:测量和标签的组合称为系列。
线路协议

线路协议是一种基于文本的格式,用于在 InfluxDB 中写入测量点。它由测量值、标签、字段和时间戳组成。当您使用 InfluxDB 的 HTTP API 向 InfluxDB 写入指针时,HTTP POST 的主体将是一个 line 协议,它表示要插入到 InfluxDB 的时序数据。线路协议中的每条线路定义一个点。多行必须用换行符\n隔开。线路协议的格式由三部分组成:

 [key] [fields] [timestamp]

线路协议中的每个部分都由空格分隔。它必须提供测量名称和至少一个字段。标签是可选的,但是在现实世界中,您应该包含标签。标记键和标记值是字符串。字段键是字符串,默认情况下,字段值是浮点数。如果一个点不包含时间戳,它将使用服务器的本地纳秒时间戳写入。除非提供了精度值,否则时间戳假定为纳秒。以下是代表单点的线路协议:

cpu,host=server01,region=uswest cpu_usage=46.26 1434055562000000000

这里cpumeasurement的名字,hostregiontagskeys,cpu_usagefield的名字,其value为 46.26。value 1434055562000000000就是timestamp。当您使用 Go 客户端库将记录写入 InfluxDB 时,您不需要使用 line 协议格式制作数据,因为这是由客户端库完成的。

安装 InfluxDB

建议您使用 https://www.influxdata.com/downloads/#influxdb 中的一个预构建包来安装 InfluxDB。你也可以从 https://github.com/influxdata/influxdb 安装 InfluxDB。

在 macOS 中,您可以使用 brew 安装 InfluxDB:

brew install influxdb

在 InfluxDB 中创建数据库

让我们使用其命令行界面influx在 InfluxDB 中创建一个数据库和用户帐户。influx工具为数据库提供了一个交互式的 shell 来写数据,交互式地查询数据,以及查看不同格式的查询输出。要启动 InfluxDB 命令行界面,运行命令influx:

$ influx

下一个命令创建一个名为opsadmin的用户帐户:

> create user opsadmin with password 'pass123'

该命令向新创建的用户opsadmin授予权限:

> grant all privileges to opsadmin

最后一个命令创建了一个名为metricsdb的数据库

> create database metricsdb

使用 Go 客户端处理 InfluxDB

InfluxDB 的 Go 客户端库的v2版本从github.com/influxdata/influxdb/client/v2开始提供。Go 客户端库由 InfluxDB 团队维护。要安装软件包的v2版本,请运行以下命令:

go get github.com/influxdata/influxdb/client/v2

要使用该包,您必须将github.com/influxdata/influxdb/client/v2添加到导入列表中。

import "github.com/influxdata/influxdb/client/v2"

正在连接到英菲尼克斯数据库

默认情况下,InfluxDB 侦听端口 8086。以下代码块使用用户帐户opsadmin连接到 InfluxDB。

c, err := client.NewHTTPClient(client.HTTPConfig{
                Addr:     "http://localhost:8086",
                Username: “opsadmin”,
                Password: “pass123”,
        })

函数NewHTTPClient从给定的配置中返回一个新的 InfluxDB Client。结构类型HTTPConfig用于为创建 InfluxDB Client提供配置。下面是 struct HTTPConfig的定义:

// HTTPConfig is the config data needed to create an HTTP Client
type HTTPConfig struct {
        // Addr should be of the form "http://host:port"
        // or "http://[ipv6-host%zone]:port".
        Addr string

        // Username is the influxdb username, optional
        Username string

        // Password is the influxdb password, optional
        Password string

        // UserAgent is the http User Agent, defaults to "InfluxDBClient"
        UserAgent string

        // Timeout for influxdb writes, defaults to no timeout
        Timeout time.Duration

        // InsecureSkipVerify gets passed to the http client, if true, it will
        // skip https certificate verification. Defaults to false
        InsecureSkipVerify bool

        // TLSConfig allows the user to set their own TLS config for the HTTP
        // Client. If set, this option overrides InsecureSkipVerify.
        TLSConfig *tls.Config
}

一旦创建了 InfluxDB Client,就可以使用它进行写和查询操作。

写入指向 InfluxDB 的点

当您向measurement写入指针以将数据持久化到 InfluxDB 中时,您应该成批地这样做。要批量写入点,首先创建一个新的BatchPoints值,如下所示:

bp, err := client.NewBatchPoints(client.BatchPointsConfig{
                Database:  “metricsdb”,
                Precision: "s",
        })

通过提供配置来创建一个BatchPoints值。属性Precision指定为每个point创建的timestamp的精度。默认情况下,Unix 中的所有时间戳都以纳秒为单位。如果您想以纳秒以外的任何单位提供时间戳,您必须提供适当的精度。分别用numssmh表示纳秒、微秒、毫秒、秒、分和小时。

下面的代码块通过向名为cpumeasurement提供tagsfieldstimestamp的值来创建一个新的point

// tagset – “host” and “region”
tags := map[string]string{
 "host":   "host1"
 "region": "us-west"
}

// field - "cpu_usage"
fields := map[string]interface{}{
 "cpu_usage": 46.22
}

// New point to measurement named “cpu”
pt, err := client.NewPoint("cpu ", tags, fields, time.Now())

 if err != nil {
       log.Fatalln("Error: ", err)
}

bp.AddPoint(pt)

因为你是批量写点的,所以用函数AddPointn的点数加到BatchPoints上。一旦所有的点都添加到BatchPoints中,调用 InfluxDB Client实例的函数Write来完成写操作。

// Write the batch
c.Write(bp) // c is the instance of InfluxDB Client instance

从 InfluxDB 读取点

InfluxDB 提供了使用熟悉的 SQL 结构查询数据的能力。该代码块决定了measurement cpu中点的count值。

command:= fmt.Sprintf("SELECT count(%s) FROM %s", "cpu_usage", "cpu")
q := client.Query{
                Command:  command,
                Database: DB,
        }
        // Query the Database
        if response, err := c.Query(q)  // // c is the instance of InfluxDB Client instance
        if err != nil {
              log.Fatalln("Error: ", err)
        }
        count :=response.Results[0].Series[0].Values[0][1]

示例:在 InfluxDB 上读写

清单 6-5 显示了一个示例程序,它将点批量写入 InfluxDB,并从数据库中读取点。

package main

import (
        "encoding/json"
        "fmt"
        "log"
        "math/rand"
        "time"

        client "github.com/influxdata/influxdb/client/v2"
)

const (
        // DB provides the database name of the InfluxDB
        DB       = "metricsdb"
        username = "opsadmin"
        password = "pass123"
)

func main() {
        // Create client
        c := influxDBClient()
        // Write operations
        // Create metrics data for measurement "cpu"
        createMetrics(c)
        // Read operations
        // Read with limit of 10
        readWithLimit(c, 10)
        // Read mean value of "cpu_usage" for a region
        meanCPUUsage(c, "us-west")
        // Read count of records for a region
        countRegion(c, "us-west")

}

// influxDBClient returns InfluxDB Client
func influxDBClient() client.Client {
        c, err := client.NewHTTPClient(client.HTTPConfig{
                Addr:     "http://localhost:8086",
                Username: username,
                Password: password,
        })
        if err != nil {
                log.Fatalln("Error: ", err)
        }
        return c
}

// createMetrics write batch points to create the metrics data
func createMetrics(clnt client.Client) {
        batchCount := 100
        rand.Seed(42)

        // Create BatchPoints by giving config for InfluxDB
        bp, _ := client.NewBatchPoints(client.BatchPointsConfig{
                Database:  DB,
                Precision: "s",
        })
        // Batch update to adds Points
        for i := 0; i < batchCount; i++ {
                regions := []string{"us-west", "us-central", "us-north", "us-east"}
                // tagset – “host” and “region”
                tags := map[string]string{
                        "host":   fmt.Sprintf("192.168.%d.%d", rand.Intn(100), rand.Intn(100)),
                        "region": regions[rand.Intn(len(regions))],
                }

                value := rand.Float64() * 100.0
                // field - "cpu_usage"
                fields := map[string]interface{}{
                        "cpu_usage": value,
                }

                pt, err := client.NewPoint("cpu", tags, fields, time.Now())

                if err != nil {
                        log.Fatalln("Error: ", err)
                }
                // Add a Point
                bp.AddPoint(pt)

        }
        // Writes the batch update to add points to measurement "cpu"
        err := clnt.Write(bp)
        if err != nil {
                log.Fatalln("Error: ", err)
        }
}

// queryDB query the database
func queryDB(clnt client.Client, command string) (res []client.Result, err error) {
        // Create the query
        q := client.Query{
                Command:  command,
                Database: DB,
        }
        // Query the Database
        if response, err := clnt.Query(q); err == nil {
                if response.Error() != nil {
                        return res, response.Error()
                }
                res = response.Results
        } else {
                return res, err
        }
        return res, nil
}

// readWithLimit reads records with a given limit
func readWithLimit(clnt client.Client, limit int) {
        q := fmt.Sprintf("SELECT * FROM %s LIMIT %d", "cpu", limit)
        res, err := queryDB(clnt, q)
        if err != nil {
                log.Fatalln("Error: ", err)
        }

        for i, row := range res[0].Series[0].Values {
                t, err := time.Parse(time.RFC3339, row[0].(string))
                if err != nil {
                        log.Fatalln("Error: ", err)
                }
                val, err := row[1].(json.Number).Float64()
                fmt.Printf("[%2d] %s: %f\n", i, t.Format(time.Stamp), val)
        }
}

// meanCPUUsage reads the mean value of cpu_usage
func meanCPUUsage(clnt client.Client, region string) {
        q := fmt.Sprintf("select mean(%s) from %s where region = '%s'", "cpu_usage", "cpu", region)
        res, err := queryDB(clnt, q)
        if err != nil {
                log.Fatalln("Error: ", err)
        }
        value, err := res[0].Series[0].Values[0][1].(json.Number).Float64()
        if err != nil {
                log.Fatalln("Error: ", err)
        }

        fmt.Printf("Mean value of cpu_usage for region '%s':%f\n", region, value)
}

// countRegion reads the count of records for a given region
func countRegion(clnt client.Client, region string) {
        q := fmt.Sprintf("SELECT count(%s) FROM %s where region = '%s'", "cpu_usage", "cpu", region)
        res, err := queryDB(clnt, q)
        if err != nil {
                log.Fatalln("Error: ", err)
        }
        count := res[0].Series[0].Values[0][1]
        fmt.Printf("Found a total of %v records for region '%s'\n", count, region)
}

Listing 6-5.Writing and Reading of Points to a Measurement “cpu” in InfluxDB

函数influxDBClient返回一个Client对象,该对象用于 InfluxDB 的读写操作。createMetrics功能用于批量写点。为了举例,100 个点被插入到一个名为cpumeasurement中。tagset中包含两个tags:?? 和 ??。measurement cpu有一个field名为cpu_usage

为了执行读操作,函数queryDB被用作助手函数,它在执行给定的查询命令后返回一部分client.Result。在这个例子中,使用助手函数queryDB执行了三个查询操作。函数readWithLimitmeasurement cpu读取数据,限制为 10。函数meanCPUUsageregion "us-west"measurement cpu中读取cpu_usagemean值。最后,函数countRegionmeasurement cpu读取region us-west的点数。

执行读取操作时,您应该会看到类似如下的输出:

[ 0] Sep 17 10:49:42: 11.901734
[ 1] Sep 17 10:49:42: 15.471216
[ 2] Sep 17 10:49:42: 32.904423
[ 3] Sep 17 10:49:42: 15.973031
[ 4] Sep 17 10:49:42: 88.648864
[ 5] Sep 17 10:49:42: 92.049809
[ 6] Sep 17 10:49:42: 83.304049
[ 7] Sep 17 10:49:42: 18.495674
[ 8] Sep 17 10:49:42: 23.389015
[ 9] Sep 17 10:49:42: 46.009337
Mean value of cpu_usage for region 'us-west':46.268998
Found a total of 27 records for region 'us-west'

您可以从influx命令行界面工具执行查询操作。让我们运行该工具并执行一个查询:

$ influx
> select * from cpu limit 10

上述命令提供了类似于以下内容的数据:

name: cpu
---------
time                     cpu_usage               host            region
1474109382000000000      11.901733613473244      192.168.1.21    us-west
1474109382000000000      15.47121626535387       192.168.99.62   us-east
1474109382000000000      32.9044231821345        192.168.98.18   us-north
1474109382000000000      15.97303140480521       192.168.97.1    us-central
1474109382000000000      88.64886440612389       192.168.96.13   us-north
1474109382000000000      92.04980918501607       192.168.95.74   us-central
1474109382000000000      83.30404929547693       192.168.91.22   us-west
1474109382000000000      18.495673741297637      192.168.90.58   us-west
1474109382000000000      23.38901519689525       192.168.9.91    us-west
1474109382000000000      46.00933676790605       192.168.9.30    us-central

6-4.使用 SQL 数据库

问题

您希望在自己的 Go 应用程序中使用 PostgreSQL、MySQL 等关系数据库。

解决办法

标准库包database/sql为使用 SQL 数据库提供了一个通用接口。要使用任何特定的 SQL 数据库,您必须使用特定于数据库的驱动程序和包database/sql。可以在 http://golang.org/s/sqldrivers 找到与包database/sql一起工作的第三方 SQL 驱动程序列表。

它是如何工作的

database/sql提供了一个通用接口,用于处理各种 SQL 数据库。虽然database/sql为 SQL 数据库提供了一个通用接口,但是它不包含任何特定的数据库驱动程序。因此你必须使用一个第三方的包,它提供了包database/sql的实现。例如,如果您想使用 PostgreSQL 数据库,您必须为database/sql使用 PostgreSQL 的数据库驱动程序。

使用 PostgreSQL

第三方包pq ( github.com/lib/pq)是database/sql的 PostgreSQL 驱动,用 Go 写的。要安装软件包pq,运行以下命令:

go get github.com/lib/pq

要使用包pq,您只需要导入驱动程序,就可以使用包database/sql提供的完整 API。下面代码块中的init函数打开一个 PostgreSQL 数据库:

import (
        "database/sql"        

        _ "github.com/lib/pq"
)

var db *sql.DB

func init() {
        var err error
        db, err = sql.Open("postgres", "postgres://user:pass@localhost/dbname")
        if err != nil {
                log.Fatal(err)
        }        
}

当您使用 SQL 数据库时,您通常使用包database/sql的 API,但是您可能不需要直接访问特定数据库驱动程序的包的功能。在这里,您使用包pq只是为了调用它的init函数,将您的驱动程序"postgres"注册到database/sql。因为包pq的导入只是为了调用它的init函数,所以使用一个空白标识符(_)作为包别名以避免编译错误。

database/sql的函数Open打开一个由其数据库驱动程序名和驱动程序特定的数据源名指定的数据库,通常至少包含一个数据库名和连接信息。这里的数据库驱动名是"postgres"。函数Open返回*sql.DB,它代表包sql为您的数据库提供的连接池。

使用 MySQL

第三方包mysql ( github.com/go-sql-driver/mysql)是database/sql的 MySQL 驱动。要安装软件包mysql,运行以下命令:

go get github.com/go-sql-driver/mysql

要使用包mysql,您只需要导入驱动程序,就可以使用包database/sql提供的完整 API。下面代码块中的init函数打开一个 MySQL 数据库:

import (
        "database/sql"        

        _ " github.com/go-sql-driver/mysql"
)

var db *sql.DB

func init() {
        var err error
        db, err = sql.Open("mysql", "user:password@/dbname")
        if err != nil {
                log.Fatal(err)
        }        
}

database/sql的函数Open打开一个带有驱动名“mysql”和给定数据源名的数据库。

PostgreSQL 数据库示例

让我们编写一个示例程序来演示如何使用包database/sqlpq使用 PostgreSQL 数据库。以下 SQL 语句用于为示例程序创建表结构:

create table products (
  id                    serial primary key,
  title           varchar(255) NOT NUL,
  description   varchar(255) NOT NUL,
  price             decimal(5,2) NOT NULL
);

清单 6-6 展示了一个示例程序,该程序演示了在名为productstore的数据库上使用 PostgreSQL 数据库进行插入和读取操作。

package main

import (
        "database/sql"
        "fmt"
        "log"

        _ "github.com/lib/pq"
)

// Product struct provides the data model for productstore
type Product struct {
        ID          int
        Title       string
        Description string
        Price       float32
}

var db *sql.DB

func init() {
        var err error
        db, err = sql.Open("postgres", "postgres://user:pass@localhost/productstore")
        if err != nil {
                log.Fatal(err)
        }
}
func main() {
        product := Product{
                Title:       "Amazon Echo",
                Description: "Amazon Echo - Black",
                Price:       179.99,
        }
        // Insert a product
        createProduct(product)
        // Read all product records
        getProducts()
}

// createProduct inserts product values into product table
func createProduct(prd Product) {
        result, err := db.Exec("INSERT INTO products(title, description, price) VALUES($1, $2, $3)", prd.Title, prd.Description, prd.Price)
        if err != nil {
                log.Fatal(err)
        }

        lastInsertID, err := result.LastInsertId()
        rowsAffected, err := result.RowsAffected()
        fmt.Printf("Product with id=%d created successfully (%d row affected)\n", lastInsertID, rowsAffected)
}

// getProducts reads all records from the product table
func getProducts() {
        rows, err := db.Query("SELECT * FROM products")
        if err != nil {
                if err == sql.ErrNoRows {
                        fmt.Println("No Records Found")
                        return
                }
                log.Fatal(err)
        }
        defer rows.Close()

        var products []*Product
        for rows.Next() {
                prd := &Product{}
                err := rows.Scan(&prd.Title, &prd.Description, &prd.Price)
                if err != nil {
                        log.Fatal(err)
                }
                products = append(products, prd)
        }
        if err = rows.Err(); err != nil {
                log.Fatal(err)
        }

        for _, pr := range products {
                fmt.Printf("%s, %s, $%.2f\n", pr.Title, pr.Description, pr.Price)
        }
}

Listing 6-6.Insert and Read Operation with a Database productstore in PostgreSQL

在函数init中创建一个*sql.DB对象,通过提供数据库驱动程序名称作为“postgres”和数据源名称来使用 PostgreSQL 数据库。

var db *sql.DB

func init() {
        var err error
        db, err = sql.Open("postgres", "postgres://user:pass@localhost/productstore")
        if err != nil {
                log.Fatal(err)
        }
}

*sql.DB对象用于执行插入和读取操作。为了向数据库表中插入记录,sql.DB对象的函数Exec用于执行查询,而不返回任何行。插入记录的值使用占位符参数传递,占位符参数使用$N符号。占位符参数的语法在不同的数据库中是不同的。例如,MySQL 和 SQL Server 使用字符?作为占位符。函数Exec返回一个sql.Result值,有两个方法:LastInsertIdRowsAffected. LastInsertId返回数据库生成的整数值,可以用来获取的值,并在插入新行时自动递增列。RowsAffected返回受更新、插入或删除操作影响的行数。

func createProduct(prd Product) {
        result, err := db.Exec("INSERT INTO products(title, description, price) VALUES($1, $2, $3)", prd.Title, prd.Description, prd.Price)
        if err != nil {
                log.Fatal(err)
        }

        lastInsertID, err := result.LastInsertId()
        rowsAffected, err := result.RowsAffected()
        fmt.Printf("Product with id=%d created successfully (%d row affected)\n", lastInsertID, rowsAffected)
}

为了执行 SQL 语句SELECT来查询数据,使用了sql.DB对象的函数Query,它返回一个 struct 类型的值Rows

rows, err := db.Query("SELECT * FROM products")

通过调用Rows对象的方法Next,可以使用Scan方法读取下一行的值。

var products []*Product
        for rows.Next() {
                prd := &Product{}
                err := rows.Scan(&prd.Title, &prd.Description, &prd.Price)
                if err != nil {
                        log.Fatal(err)
                }
                products = append(products, prd)
        }
        if err = rows.Err(); err != nil {
                log.Fatal(err)
}

当您执行一个获取单行的查询时,您可以使用函数QueryRow来执行一个查询并返回一行。下面是一个使用QueryRow获取一行的示例代码块:

   id := 1
    var product string
  err := db.QueryRow("SELECT title FROM products WHERE id=$1", id).Scan(&product)    
switch {
    case err == sql.ErrNoRows:
            log.Printf("No product with that ID.")
    case err != nil:
            log.Fatal(err)
    default:
            fmt.Printf("Product is %s\n", product)
    }

当您运行清单 6-6 中的程序时,您应该会看到类似如下的输出:

Product with id=1 created successfully (1 row affected)
Amazon Echo, Amazon Echo - Black, $179.99

使用标准库包database/sql和第三方的特定数据库驱动包,如 PostgreSQL 数据库的github.com/lib/pq,您可以使用各种 SQL 数据库。使用包database/sql的好处是你可以使用同一个接口来处理不同的数据库。