Go 应用经常需要使用长期存储。这通常以关系数据库和非关系数据库以及键值存储等形式存在。在使用这些存储应用时,将您的操作包装在一个界面中会有所帮助。本章中的食谱将检查各种存储接口,考虑与连接池之类的事物并行访问,并查看集成新库的一般技巧,这通常是使用新存储技术时的情况。
本章将介绍以下配方:
- 在 MySQL 中使用 database/sql 包
- 执行数据库事务接口
- SQL 的连接池、速率限制和超时
- 与 Redis 合作
- 在 MongoDB 中使用 NoSQL
- 为数据可移植性创建存储接口
关系数据库是一些最容易理解和最常见的数据库选项。MySQL 和 PostgreSQL 是两种最流行的开源关系数据库。这个方法将演示database/sql
包,它为许多关系数据库提供挂钩,自动处理连接池和连接持续时间,并提供对一些基本数据库操作的访问。
此配方将使用 MySQL 数据库建立连接,插入一些简单数据,然后进行查询。它将在使用后通过删除表来清理数据库。
根据以下步骤配置您的环境:
- 在您的操作系统上下载并安装 Go 1.12.6 或更高版本 https://golang.org/doc/install 。
- 打开终端或控制台应用,创建项目目录,如
~/projects/go-programming-cookbook
并导航到此目录。所有代码都将从此目录运行和修改。 - 将最新的代码克隆到
~/projects/go-programming-cookbook-original
中,并且可以选择从该目录工作,而不是手动键入示例,如下所示:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
- 使用安装并配置 MySQLhttps://dev.mysql.com/doc/mysql-getting-started/en/ 。
- 运行
export MYSQLUSERNAME=<your mysql username>
命令。 - 运行
export MYSQLPASSWORD=<your mysql password>
命令。
这些步骤包括编写和运行应用:
- 从终端或控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter6/database
的新目录并导航到此目录。 - 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/database
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/database
- 从
~/projects/go-programming-cookbook-original/chapter6/database
复制测试,或者将其作为练习来编写自己的代码! - 创建一个名为
config.go
的文件,其内容如下:
package database
import (
"database/sql"
"fmt"
"os"
"time"
_ "github.com/go-sql-driver/mysql" //we import supported
libraries for database/sql
)
// Example hold the results of our queries
type Example struct {
Name string
Created *time.Time
}
// Setup configures and returns our database
// connection poold
func Setup() (*sql.DB, error) {
db, err := sql.Open("mysql",
fmt.Sprintf("%s:%s@/gocookbook?
parseTime=true", os.Getenv("MYSQLUSERNAME"),
os.Getenv("MYSQLPASSWORD")))
if err != nil {
return nil, err
}
return db, nil
}
- 创建一个名为
create.go
的文件,其内容如下:
package database
import (
"database/sql"
_ "github.com/go-sql-driver/mysql" //we import supported
libraries for database/sql
)
// Create makes a table called example
// and populates it
func Create(db *sql.DB) error {
// create the database
if _, err := db.Exec("CREATE TABLE example (name
VARCHAR(20), created DATETIME)"); err != nil {
return err
}
if _, err := db.Exec(`INSERT INTO example (name, created)
values ("Aaron", NOW())`); err != nil {
return err
}
return nil
}
- 创建一个名为
query.go
的文件,其内容如下:
package database
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql" //we import supported
libraries for database/sql
)
// Query grabs a new connection
// creates tables, and later drops them
// and issues some queries
func Query(db *sql.DB, name string) error {
name := "Aaron"
rows, err := db.Query("SELECT name, created FROM example
where name=?", name)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var e Example
if err := rows.Scan(&e.Name, &e.Created); err != nil {
return err
}
fmt.Printf("Results:\n\tName: %s\n\tCreated: %v\n",
e.Name, e.Created)
}
return rows.Err()
}
- 创建一个名为
exec.go
的文件,其内容如下:
package database
// Exec replaces the Exec from the previous
// recipe
func Exec(db DB) error {
// uncaught error on cleanup, but we always
// want to cleanup
defer db.Exec("DROP TABLE example")
if err := Create(db); err != nil {
return err
}
if err := Query(db, "Aaron"); err != nil {
return err
}
return nil
}
- 创建并导航到
example
目录。 - 创建一个名为
main.go
的文件,其内容如下:
package main
import (
"PacktPublishing/Go-Programming-Cookbook-Second-Edition/
go-cookbook/chapter6/database"
_ "github.com/go-sql-driver/mysql" //we import supported
libraries for database/sql
)
func main() {
db, err := database.Setup()
if err != nil {
panic(err)
}
if err := database.Exec(db); err != nil {
panic(err)
}
}
- 运行
go run main.go
。 - 您还可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
Results:
Name: Aaron
Created: 2017-02-16 19:02:36 +0000 UTC
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶级配方目录中。- 如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
_ "github.com/go-sql-driver/mysql"
行代码是如何将各种数据库连接器连接到database/sql
包。也有其他 MySQL 包可以以相同的方式导入,以获得类似的结果。如果您要连接到 PostgreSQL、SQLite 或任何其他实现database/sql
接口的程序,则这些命令将类似
一旦连接,该包将建立一个连接池,该连接池包含在 SQL 配方的连接池、速率限制和超时中,您可以直接在连接上执行 SQL,也可以创建事务对象,该事务对象可以完成连接通过commit
和rollback
命令所能完成的一切。
mysql
包在与数据库对话时为 Go-time 对象提供了一些方便的支持。此配方还从MYSQLUSERNAME
和MYSQLPASSWORD
环境变量中检索用户名和密码。
在处理与数据库等服务的连接时,编写测试可能会很困难。这是因为在运行时很难进行模拟或 duck 类型的操作。尽管我建议在处理数据库时使用存储接口,但在该接口中模拟数据库事务接口仍然很有用。为数据可移植性创建存储接口配方将涵盖存储接口;此配方将重点介绍包装数据库连接和事务对象的接口。
为了展示这样一个接口的使用,我们将重写前面配方中的创建和查询文件以使用我们的接口。最终输出将是相同的,但创建和查询操作都将在事务中执行。
请参阅中的准备部分,使用 MySQL配方的数据库/sql 包。
这些步骤包括编写和运行应用:
- 从终端或控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter6/dbinterface
的新目录并导航到此目录。 - 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/dbinterface
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/dbinterface
-
从
~/projects/go-programming-cookbook-original/chapter6/dbinterface
复制测试,或者将其作为练习来编写自己的代码! -
创建一个名为
transaction.go
的文件,其内容如下:
package dbinterface
import "database/sql"
// DB is an interface that is satisfied
// by an sql.DB or an sql.Transaction
type DB interface {
Exec(query string, args ...interface{}) (sql.Result, error)
Prepare(query string) (*sql.Stmt, error)
Query(query string, args ...interface{}) (*sql.Rows, error)
QueryRow(query string, args ...interface{}) *sql.Row
}
// Transaction can do anything a Query can do
// plus Commit, Rollback, or Stmt
type Transaction interface {
DB
Commit() error
Rollback() error
}
- 创建一个名为
create.go
的文件,其内容如下:
package dbinterface
import _ "github.com/go-sql-driver/mysql" //we import supported libraries for database/sql
// Create makes a table called example
// and populates it
func Create(db DB) error {
// create the database
if _, err := db.Exec("CREATE TABLE example (name VARCHAR(20), created DATETIME)"); err != nil {
return err
}
if _, err := db.Exec(`INSERT INTO example (name, created) values ("Aaron", NOW())`); err != nil {
return err
}
return nil
}
- 创建一个名为
query.go
的文件,其内容如下:
package dbinterface
import (
"fmt"
"github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/database"
)
// Query grabs a new connection
// creates tables, and later drops them
// and issues some queries
func Query(db DB) error {
name := "Aaron"
rows, err := db.Query("SELECT name, created FROM example where name=?", name)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var e database.Example
if err := rows.Scan(&e.Name, &e.Created); err != nil {
return err
}
fmt.Printf("Results:\n\tName: %s\n\tCreated: %v\n", e.Name,
e.Created)
}
return rows.Err()
}
- 创建一个名为
exec.go
的文件,其内容如下:
package dbinterface
// Exec replaces the Exec from the previous
// recipe
func Exec(db DB) error {
// uncaught error on cleanup, but we always
// want to cleanup
defer db.Exec("DROP TABLE example")
if err := Create(db); err != nil {
return err
}
if err := Query(db); err != nil {
return err
}
return nil
}
- 导航到
example
。 - 创建一个名为
main.go
的文件,其内容如下:
package main
import (
"github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/database"
"github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/dbinterface"
_ "github.com/go-sql-driver/mysql" //we import supported libraries for database/sql
)
func main() {
db, err := database.Setup()
if err != nil {
panic(err)
}
tx, err := db.Begin()
if err != nil {
panic(err)
}
// this wont do anything if commit is successful
defer tx.Rollback()
if err := dbinterface.Exec(tx); err != nil {
panic(err)
}
if err := tx.Commit(); err != nil {
panic(err)
- 运行
go run main.go
。 - 您还可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
Results:
Name: Aaron
Created: 2017-02-16 20:00:00 +0000 UTC
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶级配方目录中。- 如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
此配方的工作方式与之前使用 MySQL 数据库/sql 包的数据库配方*非常相似。*此配方执行创建数据和查询数据的相同操作,但也演示了如何使用事务,以及如何使通用数据库函数与sql.DB
连接和sql.Transaction
对象一起工作
以这种方式编写的代码允许我们重用执行数据库操作的函数,这些操作可以使用事务单独或成组运行。这允许更多的代码重用,同时仍然将功能与在数据库上操作的函数或方法隔离开来。例如,您可以为多个表使用Update(db DB)
函数,并将它们全部传递给一个共享事务,以原子方式执行多个更新。模拟这些接口也更简单,如第 9 章、测试 Go 代码中所示。
尽管database/sql
包提供了对连接池、速率限制和超时的支持,但调整默认值以更好地适应数据库配置通常很重要。当您在微服务上进行水平扩展并且不想保留太多到数据库的活动连接时,这一点可能会变得非常重要。
请参阅中的准备部分,使用 MySQL配方的数据库/sql 包。
这些步骤包括编写和运行应用:
- 从终端或控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter6/pools
的新目录并导航到此目录。 - 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/pools
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/pools
- 从
~/projects/go-programming-cookbook-original/chapter6/pools
复制测试,或者将其作为练习来编写自己的代码! - 创建一个名为
pools.go
的文件,其内容如下:
package pools
import (
"database/sql"
"fmt"
"os"
_ "github.com/go-sql-driver/mysql" //we import supported
libraries for database/sql
)
// Setup configures the db along with pools
// number of connections and more
func Setup() (*sql.DB, error) {
db, err := sql.Open("mysql",
fmt.Sprintf("%s:%s@/gocookbook?
parseTime=true", os.Getenv("MYSQLUSERNAME"),
os.Getenv("MYSQLPASSWORD")))
if err != nil {
return nil, err
}
// there will only ever be 24 open connections
db.SetMaxOpenConns(24)
// MaxIdleConns can never be less than max open
// SetMaxOpenConns otherwise it'll default to that value
db.SetMaxIdleConns(24)
return db, nil
}
- 创建一个名为
timeout.go
的文件,其内容如下:
package pools
import (
"context"
"time"
)
// ExecWithTimeout will timeout trying
// to get the current time
func ExecWithTimeout() error {
db, err := Setup()
if err != nil {
return err
}
ctx := context.Background()
// we want to timeout immediately
ctx, cancel := context.WithDeadline(ctx, time.Now())
// call cancel after we complete
defer cancel()
// our transaction is context aware
_, err = db.BeginTx(ctx, nil)
return err
}
- 导航到
example
。 - 创建一个名为
main.go
的文件,其内容如下:
package main
import "PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
go-cookbook/chapter6/pools"
func main() {
if err := pools.ExecWithTimeout(); err != nil {
panic(err)
}
}
- 运行
go run main.go
。 - 您还可以运行以下操作:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
panic: context deadline exceeded
goroutine 1 [running]:
main.main()
/go/src/PacktPublishing/Go-Programming-Cookbook-Second-
Edition/go-cookbook/chapter6/pools/example/main.go:7 +0x4e
exit status 2
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶级配方目录中。- 如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
能够控制连接池的深度非常有用。这将防止我们超载一个数据库,但重要的是要考虑它在超时上下文中意味着什么。如果您同时强制执行一组连接和严格的基于上下文的超时,就像我们在本配方中所做的那样,那么在一些情况下,您将经常在试图建立太多连接的过载应用上请求超时。
这是因为连接将在等待连接可用时超时。database/sql
新增的上下文功能使整个请求(包括执行查询所涉及的步骤)的共享超时变得更加简单。
对于这个和其他配方,使用一个全局config
对象传递到Setup()
函数是有意义的,尽管这个配方只使用环境变量。
有时,您需要持久存储或第三方库和服务提供的附加功能。本食谱将探讨 Redis 作为非关系数据存储的一种形式,并展示 Go 等语言如何与这些第三方服务交互。
由于 Redis 支持具有简单界面的键值存储,因此它是会话存储或具有持续时间的临时数据的最佳候选。对存储在 Redis 中的数据指定超时的功能非常有价值。本食谱将探索从配置到查询再到使用自定义排序的基本 Redis 用法。
根据以下步骤配置您的环境:
- 从下载 Go 1.11.1 或更高版本并安装到您的操作系统上 https://golang.org/doc/install 。
- 从安装领事 https://www.consul.io/intro/getting-started/install.html 。
- 打开终端或控制台应用,创建并导航到项目目录,如
~/projects/go-programming-cookbook
,所有代码都将从此目录运行和修改。 - 将最新代码克隆到
~/projects/go-programming-cookbook-original
并(可选)从该目录工作,而不是手动键入示例:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
这些步骤包括编写和运行应用:
-
从终端或控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter6/redis
的新目录并导航到此目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/redis
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/redis
-
从
~/projects/go-programming-cookbook-original/chapter6/redis
复制测试,或者将其作为练习来编写自己的代码! -
创建一个名为
config.go
的文件,其内容如下:
package redis
import (
"os"
redis "gopkg.in/redis.v5"
)
// Setup initializes a redis client
func Setup() (*redis.Client, error) {
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: os.Getenv("REDISPASSWORD"),
DB: 0, // use default DB
})
_, err := client.Ping().Result()
return client, err
}
- 创建一个名为
exec.go
的文件,其内容如下:
package redis
import (
"fmt"
"time"
redis "gopkg.in/redis.v5"
)
// Exec performs some redis operations
func Exec() error {
conn, err := Setup()
if err != nil {
return err
}
c1 := "value"
// value is an interface, we can store whatever
// the last argument is the redis expiration
conn.Set("key", c1, 5*time.Second)
var result string
if err := conn.Get("key").Scan(&result); err != nil {
switch err {
// this means the key
// was not found
case redis.Nil:
return nil
default:
return err
}
}
fmt.Println("result =", result)
return nil
}
- 创建一个名为
sort.go
的文件,其内容如下:
package redis
import (
"fmt"
redis "gopkg.in/redis.v5"
)
// Sort performs a sort redis operations
func Sort() error {
conn, err := Setup()
if err != nil {
return err
}
listkey := "list"
if err := conn.LPush(listkey, 1).Err(); err != nil {
return err
}
// this will clean up the list key if any of the subsequent commands error
defer conn.Del(listkey)
if err := conn.LPush(listkey, 3).Err(); err != nil {
return err
}
if err := conn.LPush(listkey, 2).Err(); err != nil {
return err
}
res, err := conn.Sort(listkey, redis.Sort{Order: "ASC"}).Result()
if err != nil {
return err
}
fmt.Println(res)
return nil
}
- 导航到
example
。 - 创建一个名为
main.go
的文件,其内容如下:
package main
import "PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
go-cookbook/chapter6/redis"
func main() {
if err := redis.Exec(); err != nil {
panic(err)
}
if err := redis.Sort(); err != nil {
panic(err)
}
}
- 运行
go run main.go
。 - 您还可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
result = value
[1 2 3]
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶级配方目录中。- 如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
在 Go 中使用 Redis 与使用 MySQL 非常相似。虽然没有标准的库,但许多相同的约定都遵循在函数中,例如Scan()
将 Redis 中的数据读取到 Go 类型中。在这样的情况下,选择最好的库来使用可能是一个挑战,我建议定期调查什么是可用的,因为事情可能会快速变化。
此配方使用redis
包进行基本设置和获取、更复杂的排序功能和基本配置。与database/sql
类似,您可以以写入超时、池大小等形式设置其他配置。Redis 本身还提供了许多附加功能,包括 Redis 群集支持、Zscore 和计数器对象以及分布式锁。
与前面的方法一样,我建议使用一个config
对象,它存储您的 Redis 设置和配置详细信息,以便于设置和安全。
最初您可能认为,由于 Go 结构以及 Go 是一种类型化语言,Go 更适合于关系数据库。当使用类似于github.com/mongodb/mongo-go-driver
包的东西时,Go 几乎可以任意存储和检索结构对象。如果您对对象进行版本化,您的模式可以进行调整,并且可以提供非常灵活的开发环境。
有些库在隐藏或提升这些抽象方面做得更好。mongo-go-driver
包是一个图书馆的例子,它出色地完成了前者的工作。下面的方法将以类似于 Redis 和 MySQL 的方式创建连接,但将存储和检索对象,甚至不定义具体的模式。
根据以下步骤配置您的环境:
- 从下载 Go 1.11.1 或更高版本并安装到您的操作系统上 https://golang.org/doc/install 。
- 从安装领事 https://www.consul.io/intro/getting-started/install.html 。
- 打开终端或控制台应用,创建并导航到项目目录,如
~/projects/go-programming-cookbook
,所有代码都将从此目录运行和修改。 - 将最新代码克隆到
~/projects/go-programming-cookbook-original
并(可选)从该目录工作,而不是手动键入示例:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
- 安装并配置 MongoDB(https://docs.mongodb.com/getting-started/shell/
这些步骤包括编写和运行应用:
- 从终端或控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter6/mongodb
的新目录并导航到此目录。 - 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/mongodb
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/mongodb
-
从
~/projects/go-programming-cookbook-original/chapter6/mongodb
复制测试,或者将其作为练习来编写自己的代码! -
创建一个名为
config.go
的文件,其内容如下:
package mongodb
import (
"context"
"time"
"github.com/mongodb/mongo-go-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// Setup initializes a mongo client
func Setup(ctx context.Context, address string) (*mongo.Client, error) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
// cancel will be called when setup exits
defer cancel()
client, err := mongo.NewClient(options.Client().ApplyURI(address))
if err != nil {
return nil, err
}
if err := client.Connect(ctx); err != nil {
return nil, err
}
return client, nil
}
- 创建一个名为
exec.go
的文件,其内容如下:
package mongodb
import (
"context"
"fmt"
"github.com/mongodb/mongo-go-driver/bson"
)
// State is our data model
type State struct {
Name string `bson:"name"`
Population int `bson:"pop"`
}
// Exec creates then queries an Example
func Exec(address string) error {
ctx := context.Background()
db, err := Setup(ctx, address)
if err != nil {
return err
}
coll := db.Database("gocookbook").Collection("example")
vals := []interface{}{&State{"Washington", 7062000}, &State{"Oregon", 3970000}}
// we can inserts many rows at once
if _, err := coll.InsertMany(ctx, vals); err != nil {
return err
}
var s State
if err := coll.FindOne(ctx, bson.M{"name": "Washington"}).Decode(&s); err != nil {
return err
}
if err := coll.Drop(ctx); err != nil {
return err
}
fmt.Printf("State: %#v\n", s)
return nil
}
- 导航到
example
。 - 创建一个名为
main.go
的文件,其内容如下:
package main
import "github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/mongodb"
func main() {
if err := mongodb.Exec("mongodb://localhost"); err != nil {
panic(err)
}
}
- 运行
go run main.go
。 - 您还可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
State: mongodb.State{Name:"Washington", Population:7062000}
-
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶级配方目录中。 -
如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
mongo-go-driver
包还提供连接池,以及许多调整和配置mongodb
数据库连接的方法。此配方的示例相当基本,但它们说明了对基于文档的数据库进行推理和查询是多么容易。该包实现了一个 BSON 数据类型,与之之间的封送处理与使用 JSON 非常相似。
mongodb
的一致性保证和最佳实践超出了本书的范围。然而,用围棋语言与这些人一起工作是一件愉快的事情。
使用外部存储接口时,将操作抽象到接口后面会很有帮助。这是为了便于模拟、更改存储后端时的可移植性以及隔离问题。如果您需要在一个事务中执行多个操作,这种方法的缺点可能会出现。在这种情况下,进行复合操作,或者允许通过上下文对象或其他函数参数传入复合操作是有意义的。
此配方将实现一个非常简单的界面,用于处理 MongoDB 中的项。这些项目将有一个名称和价格,我们将使用一个接口来持久化和检索这些对象。
参考使用 NoSQL 和 MongoDB配方中准备部分给出的步骤。
这些步骤包括编写和运行应用:
- 从终端或控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter6/storage
的新目录并导航到此目录。 - 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/storage
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter6/storage
- 从
~/projects/go-programming-cookbook-original/chapter6/storage
复制测试,或者将其作为练习来编写自己的代码! - 创建一个名为
storage.go
的文件,其内容如下:
package storage
import "context"
// Item represents an item at
// a shop
type Item struct {
Name string
Price int64
}
// Storage is our storage interface
// We'll implement it with Mongo
// storage
type Storage interface {
GetByName(context.Context, string) (*Item, error)
Put(context.Context, *Item) error
}
- 创建一个名为
mongoconfig.go
的文件,其内容如下:
package storage
import (
"context"
"time"
"github.com/mongodb/mongo-go-driver/mongo"
)
// MongoStorage implements our storage interface
type MongoStorage struct {
*mongo.Client
DB string
Collection string
}
// NewMongoStorage initializes a MongoStorage
func NewMongoStorage(ctx context.Context, connection, db, collection string) (*MongoStorage, error) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
client, err := mongo.Connect(ctx, "mongodb://localhost")
if err != nil {
return nil, err
}
ms := MongoStorage{
Client: client,
DB: db,
Collection: collection,
}
return &ms, nil
}
- 创建一个名为
mongointerface.go
的文件,其内容如下:
package storage
import (
"context"
"github.com/mongodb/mongo-go-driver/bson"
)
// GetByName queries mongodb for an item with
// the correct name
func (m *MongoStorage) GetByName(ctx context.Context, name string) (*Item, error) {
c := m.Client.Database(m.DB).Collection(m.Collection)
var i Item
if err := c.FindOne(ctx, bson.M{"name": name}).Decode(&i); err != nil {
return nil, err
}
return &i, nil
}
// Put adds an item to our mongo instance
func (m *MongoStorage) Put(ctx context.Context, i *Item) error {
c := m.Client.Database(m.DB).Collection(m.Collection)
_, err := c.InsertOne(ctx, i)
return err
}
- 创建一个名为
exec.go
的文件,其内容如下:
package storage
import (
"context"
"fmt"
)
// Exec initializes storage, then performs operations
// using the storage interface
func Exec() error {
ctx := context.Background()
m, err := NewMongoStorage(ctx, "localhost", "gocookbook", "items")
if err != nil {
return err
}
if err := PerformOperations(m); err != nil {
return err
}
if err := m.Client.Database(m.DB).Collection(m.Collection).Drop(ctx); err != nil {
return err
}
return nil
}
// PerformOperations creates a candle item
// then gets it
func PerformOperations(s Storage) error {
ctx := context.Background()
i := Item{Name: "candles", Price: 100}
if err := s.Put(ctx, &i); err != nil {
return err
}
candles, err := s.GetByName(ctx, "candles")
if err != nil {
return err
}
fmt.Printf("Result: %#v\n", candles)
return nil
}
- 导航到
example
。 - 创建一个名为
main.go
的文件,其内容如下:
package main
import "PacktPublishing/Go-Programming-Cookbook-Second-Edition/
go-cookbook/chapter6/storage"
func main() {
if err := storage.Exec(); err != nil {
panic(err)
}
}
- 运行
go run main.go
。 - 您还可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
Result: &storage.Item{Name:"candles", Price:100}
-
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶级配方目录中。 -
如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
演示此配方最重要的功能是PerformOperations
。此函数以Storage
的接口作为参数。这意味着我们可以动态地替换底层存储,而无需修改此函数。例如,将存储连接到一个单独的 API 以使用和修改它是很简单的。
我们使用这些接口的上下文来增加额外的灵活性,并允许接口处理超时。将应用逻辑与底层存储分离提供了多种好处,但很难选择正确的位置来划定边界,这将因应用而异。