Skip to content

Latest commit

 

History

History
1124 lines (828 loc) · 33.9 KB

File metadata and controls

1124 lines (828 loc) · 33.9 KB

六、所有关于数据库和存储的信息

Go 应用经常需要使用长期存储。这通常以关系数据库和非关系数据库以及键值存储等形式存在。在使用这些存储应用时,将您的操作包装在一个界面中会有所帮助。本章中的食谱将检查各种存储接口,考虑与连接池之类的事物并行访问,并查看集成新库的一般技巧,这通常是使用新存储技术时的情况。

本章将介绍以下配方:

  • 在 MySQL 中使用 database/sql 包
  • 执行数据库事务接口
  • SQL 的连接池、速率限制和超时
  • 与 Redis 合作
  • 在 MongoDB 中使用 NoSQL
  • 为数据可移植性创建存储接口

在 MySQL 中使用 database/sql 包

关系数据库是一些最容易理解和最常见的数据库选项。MySQL 和 PostgreSQL 是两种最流行的开源关系数据库。这个方法将演示database/sql包,它为许多关系数据库提供挂钩,自动处理连接池和连接持续时间,并提供对一些基本数据库操作的访问。

此配方将使用 MySQL 数据库建立连接,插入一些简单数据,然后进行查询。它将在使用后通过删除表来清理数据库。

准备

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

  1. 在您的操作系统上下载并安装 Go 1.12.6 或更高版本 https://golang.org/doc/install
  2. 打开终端或控制台应用,创建项目目录,如~/projects/go-programming-cookbook并导航到此目录。所有代码都将从此目录运行和修改。
  3. 将最新的代码克隆到~/projects/go-programming-cookbook-original中,并且可以选择从该目录工作,而不是手动键入示例,如下所示:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
  1. 使用安装并配置 MySQLhttps://dev.mysql.com/doc/mysql-getting-started/en/
  2. 运行export MYSQLUSERNAME=<your mysql username>命令。
  3. 运行export MYSQLPASSWORD=<your mysql password>命令。

怎么做。。。

这些步骤包括编写和运行应用:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter6/database的新目录并导航到此目录。
  2. 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/database 

您应该看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/database    
  1. ~/projects/go-programming-cookbook-original/chapter6/database复制测试,或者将其作为练习来编写自己的代码!
  2. 创建一个名为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, name string) error {
            name := "Aaron"
            rows, err := db.Query("SELECT name, created FROM example 
            where name=?", name)
            if err != nil {
                return err
            }
            defer rows.Close()
            for rows.Next() {
                var e Example
                if err := rows.Scan(&e.Name, &e.Created); err != nil {
                    return err
                }
                fmt.Printf("Results:\n\tName: %s\n\tCreated: %v\n", 
                e.Name, e.Created)
            }
            return rows.Err()
        }
  1. 创建一个名为exec.go的文件,其内容如下:
        package database

        // Exec replaces the Exec from the previous
        // recipe
        func Exec(db DB) error {

            // uncaught error on cleanup, but we always
            // want to cleanup
            defer db.Exec("DROP TABLE example")

            if err := Create(db); err != nil {
                return err
            }

            if err := Query(db, "Aaron"); err != nil {
                return err
            }
            return nil
        }
  1. 创建并导航到example目录。
  2. 创建一个名为main.go的文件,其内容如下:
        package main

        import (
            "PacktPublishing/Go-Programming-Cookbook-Second-Edition/
             go-cookbook/chapter6/database"
            _ "github.com/go-sql-driver/mysql" //we import supported 
            libraries for database/sql
        )

        func main() {
            db, err := database.Setup()
            if err != nil {
                panic(err)
            }

            if err := database.Exec(db); err != nil {
                panic(err)
            }
        }
  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.mod文件可能会被更新,go.sum文件现在应该存在于顶级配方目录中。
  2. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

_ "github.com/go-sql-driver/mysql"行代码是如何将各种数据库连接器连接到database/sql包。也有其他 MySQL 包可以以相同的方式导入,以获得类似的结果。如果您要连接到 PostgreSQL、SQLite 或任何其他实现database/sql接口的程序,则这些命令将类似

一旦连接,该包将建立一个连接池,该连接池包含在 SQL 配方的连接池、速率限制和超时中,您可以直接在连接上执行 SQL,也可以创建事务对象,该事务对象可以完成连接通过commitrollback命令所能完成的一切。

mysql包在与数据库对话时为 Go-time 对象提供了一些方便的支持。此配方还从MYSQLUSERNAMEMYSQLPASSWORD环境变量中检索用户名和密码。

执行数据库事务接口

在处理与数据库等服务的连接时,编写测试可能会很困难。这是因为在运行时很难进行模拟或 duck 类型的操作。尽管我建议在处理数据库时使用存储接口,但在该接口中模拟数据库事务接口仍然很有用。为数据可移植性创建存储接口配方将涵盖存储接口;此配方将重点介绍包装数据库连接和事务对象的接口。

为了展示这样一个接口的使用,我们将重写前面配方中的创建和查询文件以使用我们的接口。最终输出将是相同的,但创建和查询操作都将在事务中执行。

准备

请参阅中的准备部分,使用 MySQL配方的数据库/sql 包。

怎么做。。。

这些步骤包括编写和运行应用:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter6/dbinterface的新目录并导航到此目录。
  2. 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/dbinterface 

您应该看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/dbinterface    
  1. ~/projects/go-programming-cookbook-original/chapter6/dbinterface复制测试,或者将其作为练习来编写自己的代码!

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

package dbinterface

import "database/sql"

// DB is an interface that is satisfied
// by an sql.DB or an sql.Transaction
type DB interface {
  Exec(query string, args ...interface{}) (sql.Result, error)
  Prepare(query string) (*sql.Stmt, error)
  Query(query string, args ...interface{}) (*sql.Rows, error)
  QueryRow(query string, args ...interface{}) *sql.Row
}

// Transaction can do anything a Query can do
// plus Commit, Rollback, or Stmt
type Transaction interface {
  DB
  Commit() error
  Rollback() error
}
  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/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/database"
)

// Query grabs a new connection
// creates tables, and later drops them
// and issues some queries
func Query(db DB) error {
  name := "Aaron"
  rows, err := db.Query("SELECT name, created FROM example where name=?", name)
  if err != nil {
    return err
  }
  defer rows.Close()
  for rows.Next() {
    var e database.Example
    if err := rows.Scan(&e.Name, &e.Created); err != nil {
      return err
    }
    fmt.Printf("Results:\n\tName: %s\n\tCreated: %v\n", e.Name, 
                e.Created)
  }
  return rows.Err()
}
  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的文件,其内容如下:
package main

import (
  "github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/database"
  "github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/dbinterface"
  _ "github.com/go-sql-driver/mysql" //we import supported libraries for database/sql
)

func main() {
  db, err := database.Setup()
  if err != nil {
    panic(err)
  }

  tx, err := db.Begin()
  if err != nil {
    panic(err)
  }
  // this wont do anything if commit is successful
  defer tx.Rollback()

  if err := dbinterface.Exec(tx); err != nil {
    panic(err)
  }
  if err := tx.Commit(); err != nil {
    panic(err)
  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.mod文件可能会被更新,go.sum文件现在应该存在于顶级配方目录中。
  2. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

此配方的工作方式与之前使用 MySQL 数据库/sql 包的数据库配方*非常相似。*此配方执行创建数据和查询数据的相同操作,但也演示了如何使用事务,以及如何使通用数据库函数与sql.DB连接和sql.Transaction对象一起工作

以这种方式编写的代码允许我们重用执行数据库操作的函数,这些操作可以使用事务单独或成组运行。这允许更多的代码重用,同时仍然将功能与在数据库上操作的函数或方法隔离开来。例如,您可以为多个表使用Update(db DB)函数,并将它们全部传递给一个共享事务,以原子方式执行多个更新。模拟这些接口也更简单,如第 9 章测试 Go 代码中所示。

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

尽管database/sql包提供了对连接池、速率限制和超时的支持,但调整默认值以更好地适应数据库配置通常很重要。当您在微服务上进行水平扩展并且不想保留太多到数据库的活动连接时,这一点可能会变得非常重要。

准备

请参阅中的准备部分,使用 MySQL配方的数据库/sql 包。

怎么做。。。

这些步骤包括编写和运行应用:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter6/pools的新目录并导航到此目录。
  2. 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/pools 

您应该看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/pools    
  1. ~/projects/go-programming-cookbook-original/chapter6/pools复制测试,或者将其作为练习来编写自己的代码!
  2. 创建一个名为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, cancel := context.WithDeadline(ctx, time.Now())

  // call cancel after we complete
  defer cancel()

  // our transaction is context aware
  _, err = db.BeginTx(ctx, nil)
  return err
}
  1. 导航到example
  2. 创建一个名为main.go的文件,其内容如下:
        package main

        import "PacktPublishing/
                Go-Programming-Cookbook-Second-Edition/
                go-cookbook/chapter6/pools"

        func main() {
            if err := pools.ExecWithTimeout(); err != nil {
                panic(err)
            }
        }
  1. 运行go run main.go
  2. 您还可以运行以下操作:
$ go build $ ./example

您应该看到以下输出:

$ go run main.go
panic: context deadline exceeded

goroutine 1 [running]:
main.main()
/go/src/PacktPublishing/Go-Programming-Cookbook-Second-
Edition/go-cookbook/chapter6/pools/example/main.go:7 +0x4e
exit status 2
  1. go.mod文件可能会被更新,go.sum文件现在应该存在于顶级配方目录中。
  2. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

能够控制连接池的深度非常有用。这将防止我们超载一个数据库,但重要的是要考虑它在超时上下文中意味着什么。如果您同时强制执行一组连接和严格的基于上下文的超时,就像我们在本配方中所做的那样,那么在一些情况下,您将经常在试图建立太多连接的过载应用上请求超时。

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

对于这个和其他配方,使用一个全局config对象传递到Setup()函数是有意义的,尽管这个配方只使用环境变量。

与 Redis 合作

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

由于 Redis 支持具有简单界面的键值存储,因此它是会话存储或具有持续时间的临时数据的最佳候选。对存储在 Redis 中的数据指定超时的功能非常有价值。本食谱将探索从配置到查询再到使用自定义排序的基本 Redis 用法。

准备

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

  1. 下载 Go 1.11.1 或更高版本并安装到您的操作系统上 https://golang.org/doc/install
  2. 安装领事 https://www.consul.io/intro/getting-started/install.html
  3. 打开终端或控制台应用,创建并导航到项目目录,如~/projects/go-programming-cookbook,所有代码都将从此目录运行和修改。
  4. 将最新代码克隆到~/projects/go-programming-cookbook-original并(可选)从该目录工作,而不是手动键入示例:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
  1. 使用安装和配置 Redishttps://redis.io/topics/quickstart

怎么做。。。

这些步骤包括编写和运行应用:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter6/redis的新目录并导航到此目录。

  2. 运行以下命令:

$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/redis 

您应该看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/redis    
  1. ~/projects/go-programming-cookbook-original/chapter6/redis复制测试,或者将其作为练习来编写自己的代码!

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

  listkey := "list"
  if err := conn.LPush(listkey, 1).Err(); err != nil {
    return err
  }
  // this will clean up the list key if any of the subsequent commands error
  defer conn.Del(listkey)

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

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

  return nil
}
  1. 导航到example
  2. 创建一个名为main.go的文件,其内容如下:
        package main

        import "PacktPublishing/
                Go-Programming-Cookbook-Second-Edition/
                go-cookbook/chapter6/redis"

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

            if err := redis.Sort(); err != nil {
                panic(err)
            }
        }
  1. 运行go run main.go
  2. 您还可以运行以下命令:
$ go build $ ./example

您应该看到以下输出:

$ go run main.go
result = value
[1 2 3]
  1. go.mod文件可能会被更新,go.sum文件现在应该存在于顶级配方目录中。
  2. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

在 Go 中使用 Redis 与使用 MySQL 非常相似。虽然没有标准的库,但许多相同的约定都遵循在函数中,例如Scan()将 Redis 中的数据读取到 Go 类型中。在这样的情况下,选择最好的库来使用可能是一个挑战,我建议定期调查什么是可用的,因为事情可能会快速变化。

此配方使用redis包进行基本设置和获取、更复杂的排序功能和基本配置。与database/sql类似,您可以以写入超时、池大小等形式设置其他配置。Redis 本身还提供了许多附加功能,包括 Redis 群集支持、Zscore 和计数器对象以及分布式锁。

与前面的方法一样,我建议使用一个config对象,它存储您的 Redis 设置和配置详细信息,以便于设置和安全。

在 MongoDB 中使用 NoSQL

最初您可能认为,由于 Go 结构以及 Go 是一种类型化语言,Go 更适合于关系数据库。当使用类似于github.com/mongodb/mongo-go-driver包的东西时,Go 几乎可以任意存储和检索结构对象。如果您对对象进行版本化,您的模式可以进行调整,并且可以提供非常灵活的开发环境。

有些库在隐藏或提升这些抽象方面做得更好。mongo-go-driver包是一个图书馆的例子,它出色地完成了前者的工作。下面的方法将以类似于 Redis 和 MySQL 的方式创建连接,但将存储和检索对象,甚至不定义具体的模式。

准备

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

  1. 下载 Go 1.11.1 或更高版本并安装到您的操作系统上 https://golang.org/doc/install
  2. 安装领事 https://www.consul.io/intro/getting-started/install.html
  3. 打开终端或控制台应用,创建并导航到项目目录,如~/projects/go-programming-cookbook,所有代码都将从此目录运行和修改。
  4. 将最新代码克隆到~/projects/go-programming-cookbook-original并(可选)从该目录工作,而不是手动键入示例:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
  1. 安装并配置 MongoDB(https://docs.mongodb.com/getting-started/shell/

怎么做。。。

这些步骤包括编写和运行应用:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter6/mongodb的新目录并导航到此目录。
  2. 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/mongodb 

您应该看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/mongodb    
  1. ~/projects/go-programming-cookbook-original/chapter6/mongodb复制测试,或者将其作为练习来编写自己的代码!

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

package mongodb

import (
  "context"
  "time"

  "github.com/mongodb/mongo-go-driver/mongo"
  "go.mongodb.org/mongo-driver/mongo/options"
)

// Setup initializes a mongo client
func Setup(ctx context.Context, address string) (*mongo.Client, error) {
  ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
  // cancel will be called when setup exits
  defer cancel()

  client, err := mongo.NewClient(options.Client().ApplyURI(address))
  if err != nil {
    return nil, err
  }

  if err := client.Connect(ctx); err != nil {
    return nil, err
  }
  return client, nil
}
  1. 创建一个名为exec.go的文件,其内容如下:
package mongodb

import (
  "context"
  "fmt"

  "github.com/mongodb/mongo-go-driver/bson"
)

// State is our data model
type State struct {
  Name string `bson:"name"`
  Population int `bson:"pop"`
}

// Exec creates then queries an Example
func Exec(address string) error {
  ctx := context.Background()
  db, err := Setup(ctx, address)
  if err != nil {
    return err
  }

  coll := db.Database("gocookbook").Collection("example")

  vals := []interface{}{&State{"Washington", 7062000}, &State{"Oregon", 3970000}}

  // we can inserts many rows at once
  if _, err := coll.InsertMany(ctx, vals); err != nil {
    return err
  }

  var s State
  if err := coll.FindOne(ctx, bson.M{"name": "Washington"}).Decode(&s); err != nil {
    return err
  }

  if err := coll.Drop(ctx); err != nil {
    return err
  }

  fmt.Printf("State: %#v\n", s)
  return nil
}
  1. 导航到example
  2. 创建一个名为main.go的文件,其内容如下:
package main

import "github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/mongodb"

func main() {
  if err := mongodb.Exec("mongodb://localhost"); err != nil {
    panic(err)
  }
}
  1. 运行go run main.go
  2. 您还可以运行以下命令:
$ go build $ ./example

您应该看到以下输出:

$ go run main.go
State: mongodb.State{Name:"Washington", Population:7062000}
  1. go.mod文件可能会被更新,go.sum文件现在应该存在于顶级配方目录中。

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

它是如何工作的。。。

mongo-go-driver包还提供连接池,以及许多调整和配置mongodb数据库连接的方法。此配方的示例相当基本,但它们说明了对基于文档的数据库进行推理和查询是多么容易。该包实现了一个 BSON 数据类型,与之之间的封送处理与使用 JSON 非常相似。

mongodb的一致性保证和最佳实践超出了本书的范围。然而,用围棋语言与这些人一起工作是一件愉快的事情。

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

使用外部存储接口时,将操作抽象到接口后面会很有帮助。这是为了便于模拟、更改存储后端时的可移植性以及隔离问题。如果您需要在一个事务中执行多个操作,这种方法的缺点可能会出现。在这种情况下,进行复合操作,或者允许通过上下文对象或其他函数参数传入复合操作是有意义的。

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

准备

参考使用 NoSQL 和 MongoDB配方中准备部分给出的步骤。

怎么做。。。

这些步骤包括编写和运行应用:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter6/storage的新目录并导航到此目录。
  2. 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/storage 

您应该看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/storage    
  1. ~/projects/go-programming-cookbook-original/chapter6/storage复制测试,或者将其作为练习来编写自己的代码!
  2. 创建一个名为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 (
  "context"
  "time"

  "github.com/mongodb/mongo-go-driver/mongo"
)

// MongoStorage implements our storage interface
type MongoStorage struct {
  *mongo.Client
  DB string
  Collection string
}

// NewMongoStorage initializes a MongoStorage
func NewMongoStorage(ctx context.Context, connection, db, collection string) (*MongoStorage, error) {
  ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
  defer cancel()

  client, err := mongo.Connect(ctx, "mongodb://localhost")
  if err != nil {
    return nil, err
  }

  ms := MongoStorage{
    Client: client,
    DB: db,
    Collection: collection,
  }
  return &ms, nil
}
  1. 创建一个名为mongointerface.go的文件,其内容如下:
package storage

import (
  "context"

  "github.com/mongodb/mongo-go-driver/bson"
)

// GetByName queries mongodb for an item with
// the correct name
func (m *MongoStorage) GetByName(ctx context.Context, name string) (*Item, error) {
  c := m.Client.Database(m.DB).Collection(m.Collection)
  var i Item
  if err := c.FindOne(ctx, bson.M{"name": name}).Decode(&i); err != nil {
    return nil, err
  }

  return &i, nil
}

// Put adds an item to our mongo instance
func (m *MongoStorage) Put(ctx context.Context, i *Item) error {
  c := m.Client.Database(m.DB).Collection(m.Collection)
  _, err := c.InsertOne(ctx, i)
  return err
}
  1. 创建一个名为exec.go的文件,其内容如下:
package storage

import (
  "context"
  "fmt"
)

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

  if err := m.Client.Database(m.DB).Collection(m.Collection).Drop(ctx); err != nil {
    return err
  }

  return nil
}

// PerformOperations creates a candle item
// then gets it
func PerformOperations(s Storage) error {
  ctx := context.Background()
  i := Item{Name: "candles", Price: 100}
  if err := s.Put(ctx, &i); err != nil {
    return err
  }

  candles, err := s.GetByName(ctx, "candles")
  if err != nil {
    return err
  }
  fmt.Printf("Result: %#v\n", candles)
  return nil
}
  1. 导航到example
  2. 创建一个名为main.go的文件,其内容如下:
        package main

        import "PacktPublishing/Go-Programming-Cookbook-Second-Edition/
                go-cookbook/chapter6/storage"

        func main() {
            if err := storage.Exec(); err != nil {
                panic(err)
            }
        }
  1. 运行go run main.go
  2. 您还可以运行以下命令:
$ go build $ ./example

您应该看到以下输出:

$ go run main.go
Result: &storage.Item{Name:"candles", Price:100}
  1. go.mod文件可能会被更新,go.sum文件现在应该存在于顶级配方目录中。

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

它是如何工作的。。。

演示此配方最重要的功能是PerformOperations。此函数以Storage的接口作为参数。这意味着我们可以动态地替换底层存储,而无需修改此函数。例如,将存储连接到一个单独的 API 以使用和修改它是很简单的。

我们使用这些接口的上下文来增加额外的灵活性,并允许接口处理超时。将应用逻辑与底层存储分离提供了多种好处,但很难选择正确的位置来划定边界,这将因应用而异。