如何在MongoDB中使用Go

488 阅读7分钟
在这篇文章中

在Twitter上分享

  • 发表于2021年8月21日

如何在MongoDB中使用Go

本文最初于2020年4月8日发表于DigitalOcean博客。一些小的细节已被修改,以使其符合最新的要求。

在依靠社区开发的解决方案多年后,MongoDB宣布他们正在为Go开发一个官方驱动程序。2019年3月,这个新驱动随着v1.0.0版本的发布达到了生产就绪的状态,并从那时起不断地更新。

与其他MongoDB官方驱动一样,Go驱动对Go编程语言来说是习以为常的,并提供了一种简单的方法来使用MongoDB作为Go程序的数据库解决方案。它与MongoDB的API完全集成,并公开了API的所有查询、索引和聚合功能,以及其他高级功能。与第三方库不同的是,它将得到MongoDB工程师的全面支持,因此你可以确信它将得到持续的发展和维护。

在本教程中,你将开始使用官方的MongoDB Go驱动。你将安装该驱动,连接到MongoDB数据库,并执行若干CRUD操作。在此过程中,你将创建一个简单的任务管理器程序,通过命令行管理任务。

前提条件

对于本教程,你需要在机器上安装最新版本的GoMongoDB,MongoDB 2.6或更高版本是MongoDB Go驱动支持的最低版本。本指南中显示的命令和代码是用Go v1.14.1和MongoDB v3.6.3测试的。

安装 MongoDB Go 驱动程序

在这一步中,你将安装MongoDB的Go驱动包,并将其导入你的项目中。你还将连接到你的MongoDB数据库并检查连接的状态。

继续,在你的文件系统中为这个教程创建一个新的目录。

$ mkdir tasker

一旦你的项目目录设置好了,就用下面的命令改成它。

$ cd tasker

接下来,用一个go.mod 文件来初始化Go项目。这个文件定义了项目要求,并将依赖关系锁定为正确的版本。

$ go mod init

如果你的项目目录在$GOPATH 之外,你需要为你的模块指定导入路径,如下所示。

$ go mod init github.com/<your_username>/tasker

这时,你的go.mod 文件将看起来像这样。

go.mod

module github.com/<your_username>/tasker

go 1.14

使用下面的命令将MongoDB Go驱动作为你项目的依赖项。

$ go get go.mongodb.org/mongo-driver

你会看到如下的输出。

输出

go: downloading go.mongodb.org/mongo-driver v1.3.2
go: go.mongodb.org/mongo-driver upgrade => v1.3.2

在这一点上,你的go.mod 文件将看起来像这样。

go.mod

module github.com/<your_username>/tasker

go 1.14

require go.mongodb.org/mongo-driver v1.3.1 // indirect

接下来,在你的项目根目录下创建一个main.go 文件,并在你的文本编辑器中打开它。

$ nano main.go

为了开始使用驱动程序,请将以下软件包导入你的main.go 文件。

main.go

package main

import (
	"context"
	"log"

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

在这里,你添加了 mongooptions包,这是MongoDB Go驱动提供的。

接下来,根据你的导入,创建一个新的MongoDB客户端,并连接到你正在运行的MongoDB服务器。

main.go

. . .
var collection *mongo.Collection
var ctx = context.TODO()

func init() {
	clientOptions := options.Client().ApplyURI("mongodb://localhost:27017/")
	client, err := mongo.Connect(ctx, clientOptions)
	if err != nil {
		log.Fatal(err)
	}
}

mongo.Connect() 接受一个 和一个 对象,该对象用于设置连接字符串和其他驱动程序设置。你可以访问Context options.ClientOptions options包的文档,看看有哪些配置选项可用。

上下文就像一个超时或最后期限,表明一个操作应该何时停止运行并返回。它有助于防止生产系统在特定操作运行缓慢时出现性能下降。在这段代码中,你通过context.TODO() ,表示你现在不确定要使用什么上下文,但你计划在将来添加一个。

接下来,让我们确保你的MongoDB服务器被找到并使用Ping 方法成功连接。在init 函数内添加以下代码。

main.go

. . .
    log.Fatal(err)
  }

  err = client.Ping(ctx, nil)
  if err != nil {
    log.Fatal(err)
  }
}

如果在连接数据库时有任何错误,程序应该崩溃,同时你要尝试解决这个问题,因为在没有活跃的数据库连接的情况下保持程序运行是没有意义的。

添加以下代码来创建一个数据库。

main.go

. . .
  err = client.Ping(ctx, nil)
  if err != nil {
    log.Fatal(err)
  }

  collection = client.Database("tasker").Collection("tasks")
}

你创建了一个tasker 数据库和一个task 集合来存储你将要创建的任务。你还设置了collection 作为包级变量,这样你就可以在整个包中重复使用数据库连接。

保存并退出该文件。此时完整的main.go ,如下所示。

main.go

package main

import (
	"context"
	"log"

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

var collection *mongo.Collection
var ctx = context.TODO()

func init() {
	clientOptions := options.Client().ApplyURI("mongodb://localhost:27017/")
	client, err := mongo.Connect(ctx, clientOptions)
	if err != nil {
		log.Fatal(err)
	}

	err = client.Ping(ctx, nil)
	if err != nil {
		log.Fatal(err)
	}

	collection = client.Database("tasker").Collection("tasks")
}

你已经设置了你的程序,以使用Go驱动连接到你的MongoDB服务器。在接下来的步骤中,你将继续创建你的任务管理器程序。

创建一个CLI程序

在这一步,你将安装著名的 cli包来帮助开发你的任务管理器程序。它提供了一个接口,你可以利用它来快速创建现代命令行工具。例如,这个软件包提供了为你的程序定义子命令的能力,以获得更类似于git的命令行体验。

运行下面的命令,将软件包作为一个依赖项添加。

$ go get github.com/urfave/cli/v2

接下来,再次打开你的main.go 文件。

$ nano main.go

在你的main.go 文件中添加以下突出显示的代码。

main.go

package main

import (
	"context"
	"log"
	"os"

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

如前所述,你导入了cli 包。你还导入了os 包,你将用它来向你的程序传递命令行参数。

在你的init 函数之后添加以下代码,以创建你的CLI程序,并使你的代码得到编译。

main.go

. . .
func main() {
	app := &cli.App{
		Name:     "tasker",
		Usage:    "A simple CLI program to manage your tasks",
		Commands: []*cli.Command{},
	}

	err := app.Run(os.Args)
	if err != nil {
		log.Fatal(err)
	}
}

这个片段创建了一个名为tasker 的CLI程序,并添加了一个简短的使用说明,当你运行该程序时将会打印出来。Commands 片段是你为你的程序添加命令的地方。Run 命令将参数分片解析为适当的命令。

保存并退出你的文件。这里是你需要建立和运行程序的命令。

$ go run main.go

你会看到下面的输出。

输出

NAME:
   tasker - A simple CLI program to manage your tasks

USAGE:
   main [global options] command [command options] [arguments...]

COMMANDS:
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h     show help (default: false)

程序运行并显示帮助文本,这对了解程序能做什么,以及如何使用它很方便。在接下来的步骤中,你将通过添加子命令来帮助管理你在MongoDB中的任务,从而提高程序的效用。

创建一个任务

在这一步中,你将使用cli 包向你的CLI程序添加一个子命令。在本节结束时,你将能够通过在你的CLI程序中使用新的add 命令向你的MongoDB数据库添加一个新任务。

首先,打开你的main.go 文件。

$ nano main.go

接下来,导入 go.mongodb.org/mongo-driver/bson/primitive, timeerrors包。

main.go

package main

import (
	"context"
	"errors"
	"log"
	"os"
	"time"

	"github.com/urfave/cli/v2"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

然后创建一个新的结构来代表数据库中的一个任务,并将其插入到main 函数的前面。

main.go

. . .
type Task struct {
	ID        primitive.ObjectID `bson:"_id"`
	CreatedAt time.Time          `bson:"created_at"`
	UpdatedAt time.Time          `bson:"updated_at"`
	Text      string             `bson:"text"`
	Completed bool               `bson:"completed"`
}
. . .

你使用primitive 包来设置每个任务的ID的类型,因为MongoDB默认使用ObjectIDs来设置_id 字段。MongoDB的另一个默认行为是,在序列化时,小写的字段名被用作每个导出字段的键,但这可以使用bson 结构标记来改变。

接下来,创建一个函数,接收Task 的实例并将其保存在数据库中。在main 函数后面添加这个片段。

main.go

. . .
func createTask(task *Task) error {
	_, err := collection.InsertOne(ctx, task)
  return err
}
. . .

collection.InsertOne() 方法将提供的任务插入数据库集合中,并返回被插入的文档的ID。由于你不需要这个ID,你通过赋值给下划线操作符来抛弃它。

下一步是在你的任务管理器程序中添加一个新命令,用于创建新任务。让我们把它叫做add

main.go

. . .
func main() {
	app := &cli.App{
		Name:  "tasker",
		Usage: "A simple CLI program to manage your tasks",
		Commands: []*cli.Command{
			{
				Name:    "add",
				Aliases: []string{"a"},
				Usage:   "add a task to the list",
				Action: func(c *cli.Context) error {
					str := c.Args().First()
					if str == "" {
						return errors.New("Cannot add an empty task")
					}

					task := &Task{
						ID:        primitive.NewObjectID(),
						CreatedAt: time.Now(),
						UpdatedAt: time.Now(),
						Text:      str,
						Completed: false,
					}

					return createTask(task)
				},
			},
		},
	}

	err := app.Run(os.Args)
	if err != nil {
		log.Fatal(err)
	}
}

每一个被添加到你的CLI程序中的新命令都被放在Commands slice里面。每个命令都包括一个名称、使用说明和动作。这是命令执行时要运行的代码。

在这段代码中,你收集了add 的第一个参数,并使用它来设置一个新的Task 实例的Text 属性,同时为其他属性分配了适当的默认值。新的任务随后被传递给createTask ,它将任务插入到数据库中,如果一切顺利,则返回nil ,使命令退出。

保存并退出你的文件。通过使用add 命令添加一些任务来测试它。如果成功,你会看到屏幕上没有打印出错误。

$ go run main.go add "Learn Go"
$ go run main.go add "Read a book"

现在你可以成功地添加任务,让我们实现一种方法来显示你已经添加到数据库的所有任务。

列出所有任务

列出一个集合中的文件可以用collection.Find() 方法来完成,该方法需要一个过滤器以及一个指向结果可以被解码的值的指针。它的返回值是一个Cursor,它提供了一个文件流,可以被迭代并逐一解码。一旦游标被用完,它就被关闭。

打开你的main.go 文件。

$ nano main.go

请确保导入 bson包。

main.go

package main

import (
	"context"
	"errors"
	"log"
	"os"
	"time"

	"github.com/urfave/cli/v2"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)
. . .

然后在createTask 之后立即创建以下函数。

main.go

. . .
func getAll() ([]*Task, error) {
	// passing bson.D{{}} matches all documents in the collection
	filter := bson.D{{}}
	return filterTasks(filter)
}

func filterTasks(filter interface{}) ([]*Task, error) {
	// A slice of tasks for storing the decoded documents
	var tasks []*Task

	cur, err := collection.Find(ctx, filter)
	if err != nil {
		return tasks, err
	}

	for cur.Next(ctx) {
		var t Task
		err := cur.Decode(&t)
		if err != nil {
			return tasks, err
		}

		tasks = append(tasks, &t)
	}

	if err := cur.Err(); err != nil {
		return tasks, err
	}

	// once exhausted, close the cursor
	cur.Close(ctx)

	if len(tasks) == 0 {
		return tasks, mongo.ErrNoDocuments
	}

	return tasks, nil
}

BSON(二进制编码的JSON)是文件在MongoDB数据库中的表示方式,而bson 包就是帮助我们在Go中处理BSON对象。getAll() 函数中使用的bson.D 类型代表了一个 BSON 文档,它被用于属性顺序重要的地方。通过将bson.D{{}} 作为过滤器传递给filterTasks() ,你表明你想匹配集合中的所有文档。

filterTasks() 函数中,你遍历由collection.Find() 方法返回的 Cursor,并将每个文档解码为Task 的一个实例。然后每个Task 被附加到函数开始时创建的任务片上。一旦游标用完,它就被关闭,tasks slice被返回。

在你创建一个列出所有任务的命令之前,让我们创建一个辅助函数,该函数获取tasks ,并打印到标准输出。你将使用 color包来对输出进行着色。

在你使用这个包之前,请先安装它。

$ go get gopkg.in/gookit/color.v1
go: downloading gopkg.in/gookit/color.v1 v1.1.6
go: gopkg.in/gookit/color.v1 upgrade => v1.1.6

并将其与fmt 包一起导入到你的main.go 文件中。

main.go

package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/urfave/cli/v2"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"gopkg.in/gookit/color.v1"
)
. . .

接下来,在你的main 函数之后创建一个新的printTasks 函数。

main.go

. . .
func printTasks(tasks []*Task) {
	for i, v := range tasks {
		if v.Completed {
			color.Green.Printf("%d: %s\n", i+1, v.Text)
		} else {
			color.Yellow.Printf("%d: %s\n", i+1, v.Text)
		}
	}
}
. . .

这个printTasks 函数获取tasks 的一个片断,对每个片断进行迭代,并将其打印到标准输出,用绿色表示已完成的任务,黄色表示未完成的任务。

继续添加以下突出显示的行,在Commands slice中创建一个新的all 命令。这个命令将把所有添加的任务打印到标准输出。

main.go

. . .
func main() {
	app := &cli.App{
		Name:  "tasker",
		Usage: "A simple CLI program to manage your tasks",
		Commands: []*cli.Command{
			{
				Name:    "add",
				Aliases: []string{"a"},
				Usage:   "add a task to the list",
				Action: func(c *cli.Context) error {
					str := c.Args().First()
					if str == "" {
						return errors.New("Cannot add an empty task")
					}

					task := &Task{
						ID:        primitive.NewObjectID(),
						CreatedAt: time.Now(),
						UpdatedAt: time.Now(),
						Text:      str,
						Completed: false,
					}

					return createTask(task)
				},
			},
			{
				Name:    "all",
				Aliases: []string{"l"},
				Usage:   "list all tasks",
				Action: func(c *cli.Context) error {
					tasks, err := getAll()
					if err != nil {
						if err == mongo.ErrNoDocuments {
							fmt.Print("Nothing to see here.\nRun `add 'task'` to add a task")
							return nil
						}

						return err
					}

					printTasks(tasks)
					return nil
				},
			},
		},
	}

	err := app.Run(os.Args)
	if err != nil {
		log.Fatal(err)
	}
}

. . .

all 命令检索数据库中存在的所有任务,并将它们打印到标准输出。如果没有任务存在,则打印出一个添加新任务的提示。

保存并退出你的文件。用all 命令建立并运行你的程序。

$ go run main.go all

它将列出到目前为止你所添加的所有任务。

main.go

1: Learn Go
2: Read a book

现在你可以查看数据库中的所有任务了,让我们在下一步中添加将任务标记为已完成的功能。

完成一项任务

在这一步中,你将创建一个新的子命令,名为done ,它将允许你把数据库中的现有任务标记为已完成。要把一项任务标记为完成,你可以使用collection.FindOneAndUpdate() 方法。它允许你在一个集合中找到一个文件,并更新其部分或全部属性。这个方法需要一个过滤器来定位文档,以及一个更新文档来描述操作。这两个都是使用bson.D 类型建立的。

首先打开你的main.go 文件。

$ nano main.go

在你的filterTasks() 函数后面插入以下片段。

main.go

. . .
func completeTask(text string) error {
	filter := bson.D{primitive.E{Key: "text", Value: text}}

	update := bson.D{primitive.E{Key: "$set", Value: bson.D{
		primitive.E{Key: "completed", Value: true},
	}}}

	t := &Task{}
	return collection.FindOneAndUpdate(ctx, filter, update).Decode(t)
}
. . .

该函数匹配第一个文本属性等于text 参数的文件。update 文件指定将completed 属性设置为true 。如果在FindOneAndUpdate() 操作中出现错误,将由completeTask() 返回。否则就会返回nil

接下来,让我们在你的CLI程序中添加一个新的done 命令,将一个任务标记为完成。

main.go

. . .
func main() {
	app := &cli.App{
		Name:  "tasker",
		Usage: "A simple CLI program to manage your tasks",
		Commands: []*cli.Command{
			{
				Name:    "add",
				Aliases: []string{"a"},
				Usage:   "add a task to the list",
				Action: func(c *cli.Context) error {
					str := c.Args().First()
					if str == "" {
						return errors.New("Cannot add an empty task")
					}

					task := &Task{
						ID:        primitive.NewObjectID(),
						CreatedAt: time.Now(),
						UpdatedAt: time.Now(),
						Text:      str,
						Completed: false,
					}

					return createTask(task)
				},
			},
			{
				Name:    "all",
				Aliases: []string{"l"},
				Usage:   "list all tasks",
				Action: func(c *cli.Context) error {
					tasks, err := getAll()
					if err != nil {
						if err == mongo.ErrNoDocuments {
							fmt.Print("Nothing to see here.\nRun `add 'task'` to add a task")
							return nil
						}

						return err
					}

					printTasks(tasks)
					return nil
				},
			},
			{
				Name:    "done",
				Aliases: []string{"d"},
				Usage:   "complete a task on the list",
				Action: func(c *cli.Context) error {
					text := c.Args().First()
					return completeTask(text)
				},
			},
		},
	}

	err := app.Run(os.Args)
	if err != nil {
		log.Fatal(err)
	}
}

. . .

你使用传递给done 命令的参数来寻找第一个属性与text 匹配的文档。如果找到了,该文档上的completed 属性就被设置为true

保存并退出你的文件。然后用done 命令运行你的程序。

$ go run main.go done "Learn Go"

如果你再次使用all 命令,你会注意到被标记为完成的任务现在被打印成了绿色。

$ go run main.go all

Screenshot of terminal output after completing a task

有时,你只想查看尚未完成的任务。我们接下来会增加这个功能。

只显示未完成的任务

在这一步,你将加入代码,使用MongoDB驱动从数据库中检索待办任务。挂起的任务是那些completed 属性被设置为false

让我们添加一个新函数,以检索尚未完成的任务。打开你的main.go 文件。

$ nano main.go

然后在completeTask 函数后面添加这个片段。

main.go

. . .
func getPending() ([]*Task, error) {
	filter := bson.D{
		primitive.E{Key: "completed", Value: false},
	}

	return filterTasks(filter)
}
. . .

你使用MongoDB驱动中的bsonprimitive 包创建了一个过滤器,它将匹配那些completed 属性被设置为false 的文档。然后,待处理任务的片断被返回给调用者。

与其创建一个新的命令来列出待办任务,不如让它成为在没有任何命令的情况下运行程序时的默认动作。你可以通过给程序添加一个Action 属性来做到这一点,如下所示。

main.go

. . .
func main() {
	app := &cli.App{
		Name:  "tasker",
		Usage: "A simple CLI program to manage your tasks",
		Action: func(c *cli.Context) error {
			tasks, err := getPending()
			if err != nil {
				if err == mongo.ErrNoDocuments {
					fmt.Print("Nothing to see here.\nRun `add 'task'` to add a task")
					return nil
				}

				return err
			}

			printTasks(tasks)
			return nil
		},
		Commands: []*cli.Command{
			{
				Name:    "add",
				Aliases: []string{"a"},
				Usage:   "add a task to the list",
				Action: func(c *cli.Context) error {
					str := c.Args().First()
					if str == "" {
						return errors.New("Cannot add an empty task")
					}

					task := &Task{
						ID:        primitive.NewObjectID(),
						CreatedAt: time.Now(),
						UpdatedAt: time.Now(),
						Text:      str,
						Completed: false,
					}

					return createTask(task)
				},
			},

当程序在没有任何子命令的情况下执行时,Action 属性会执行一个默认动作。这是将列出待办任务的逻辑放在这里。getPending() 函数被调用,结果任务被打印到标准输出,使用printTasks() 。如果没有悬而未决的任务,就会显示一个提示,鼓励用户使用add 命令添加一个新的任务。

保存并退出你的文件。现在运行程序,不添加任何命令,将列出数据库中所有待处理的任务。

$ go run main.go

你会看到下面的输出。

输出

1: Read a book

现在你可以列出未完成的任务了,让我们再添加一条命令,只允许你查看已完成的任务。

显示已完成的任务

在这一步,你将添加一个新的finished 子命令,从数据库中获取已完成的任务并在屏幕上显示。这涉及到过滤和返回那些completed 属性被设置为true 的任务。

打开你的main.go 文件。

$ nano main.go

然后在你的文件末尾添加以下代码。

main.go

. . .
func getFinished() ([]*Task, error) {
	filter := bson.D{
		primitive.E{Key: "completed", Value: true},
	}

	return filterTasks(filter)
}
. . .

类似于getPending() 函数,你已经添加了一个getFinished() 函数,返回一个已完成任务的切片。在这种情况下,过滤器的completed 属性设置为true ,所以只有符合这个条件的文件才会被返回。

接下来,创建一个finished 命令,打印所有完成的任务。

main.go

. . .
func main() {
	app := &cli.App{
		Name:  "tasker",
		Usage: "A simple CLI program to manage your tasks",
		Action: func(c *cli.Context) error {
			tasks, err := getPending()
			if err != nil {
				if err == mongo.ErrNoDocuments {
					fmt.Print("Nothing to see here.\nRun `add 'task'` to add a task")
					return nil
				}

				return err
			}

			printTasks(tasks)
			return nil
		},
		Commands: []*cli.Command{
			{
				Name:    "add",
				Aliases: []string{"a"},
				Usage:   "add a task to the list",
				Action: func(c *cli.Context) error {
					str := c.Args().First()
					if str == "" {
						return errors.New("Cannot add an empty task")
					}

					task := &Task{
						ID:        primitive.NewObjectID(),
						CreatedAt: time.Now(),
						UpdatedAt: time.Now(),
						Text:      str,
						Completed: false,
					}

					return createTask(task)
				},
			},
			{
				Name:    "all",
				Aliases: []string{"l"},
				Usage:   "list all tasks",
				Action: func(c *cli.Context) error {
					tasks, err := getAll()
					if err != nil {
						if err == mongo.ErrNoDocuments {
							fmt.Print("Nothing to see here.\nRun `add 'task'` to add a task")
							return nil
						}

						return err
					}

					printTasks(tasks)
					return nil
				},
			},
			{
				Name:    "done",
				Aliases: []string{"d"},
				Usage:   "complete a task on the list",
				Action: func(c *cli.Context) error {
					text := c.Args().First()
					return completeTask(text)
				},
			},
			{
				Name:    "finished",
				Aliases: []string{"f"},
				Usage:   "list completed tasks",
				Action: func(c *cli.Context) error {
					tasks, err := getFinished()
					if err != nil {
						if err == mongo.ErrNoDocuments {
							fmt.Print("Nothing to see here.\nRun `done 'task'` to complete a task")
							return nil
						}

						return err
					}

					printTasks(tasks)
					return nil
				},
			},
		}
	}

	err := app.Run(os.Args)
	if err != nil {
		log.Fatal(err)
	}
}

finished 命令通过这里创建的getFinished() 函数检索那些completed 属性被设置为true 的任务。然后,它将其传递给printTasks 函数,以便将它们打印到标准输出。

保存并退出你的文件。运行下面的命令。

$ go run main.go finished

你会看到下面的输出。

输出

1: Learn Go

在最后一步,你将让用户选择从数据库中删除任务。

删除一个任务

在这一步,你将添加一个新的delete 子命令,允许用户从数据库中删除一个任务。要删除一个任务,你将使用MongoDB驱动的collection.DeleteOne() 方法。它也依赖于一个过滤器来匹配要删除的文件。

再一次打开你的main.go 文件。

$ nano main.go

在你的getFinished 函数之后直接添加这个deleteTask 函数来从数据库中删除任务。

main.go

. . .
func deleteTask(text string) error {
	filter := bson.D{primitive.E{Key: "text", Value: text}}

	res, err := collection.DeleteOne(ctx, filter)
	if err != nil {
		return err
	}

	if res.DeletedCount == 0 {
		return errors.New("No tasks were deleted")
	}

	return nil
}
. . .

这个deleteTask 方法接受一个字符串参数,代表要删除的任务项目。一个过滤器被构造出来以匹配任务项,其text 属性被设置为字符串参数。你把过滤器传递给DeleteOne() 方法,该方法匹配集合中的项目并删除它。

你可以检查DeleteOne 方法的结果上的DeletedCount 属性,以确认是否有任何文件被删除。如果过滤器无法匹配要删除的文档,DeletedCount ,在这种情况下你可以返回一个错误。

现在添加一个新的rm 命令,如高亮所示。

main.go

. . .
func main() {
	app := &cli.App{
		Name:  "tasker",
		Usage: "A simple CLI program to manage your tasks",
		Action: func(c *cli.Context) error {
			tasks, err := getPending()
			if err != nil {
				if err == mongo.ErrNoDocuments {
					fmt.Print("Nothing to see here.\nRun `add 'task'` to add a task")
					return nil
				}

				return err
			}

			printTasks(tasks)
			return nil
		},
		Commands: []*cli.Command{
			{
				Name:    "add",
				Aliases: []string{"a"},
				Usage:   "add a task to the list",
				Action: func(c *cli.Context) error {
					str := c.Args().First()
					if str == "" {
						return errors.New("Cannot add an empty task")
					}

					task := &Task{
						ID:        primitive.NewObjectID(),
						CreatedAt: time.Now(),
						UpdatedAt: time.Now(),
						Text:      str,
						Completed: false,
					}

					return createTask(task)
				},
			},
			{
				Name:    "all",
				Aliases: []string{"l"},
				Usage:   "list all tasks",
				Action: func(c *cli.Context) error {
					tasks, err := getAll()
					if err != nil {
						if err == mongo.ErrNoDocuments {
							fmt.Print("Nothing to see here.\nRun `add 'task'` to add a task")
							return nil
						}

						return err
					}

					printTasks(tasks)
					return nil
				},
			},
			{
				Name:    "done",
				Aliases: []string{"d"},
				Usage:   "complete a task on the list",
				Action: func(c *cli.Context) error {
					text := c.Args().First()
					return completeTask(text)
				},
			},
			{
				Name:    "finished",
				Aliases: []string{"f"},
				Usage:   "list completed tasks",
				Action: func(c *cli.Context) error {
					tasks, err := getFinished()
					if err != nil {
						if err == mongo.ErrNoDocuments {
							fmt.Print("Nothing to see here.\nRun `done 'task'` to complete a task")
							return nil
						}

						return err
					}

					printTasks(tasks)
					return nil
				},
			},
			{
				Name:  "rm",
				Usage: "deletes a task on the list",
				Action: func(c *cli.Context) error {
					text := c.Args().First()
					err := deleteTask(text)
					if err != nil {
						return err
					}

					return nil
				},
			},
		}
	}

	err := app.Run(os.Args)
	if err != nil {
		log.Fatal(err)
	}
}
. . .

就像之前添加的所有其他子命令一样,rm 命令使用其第一个参数来匹配数据库中的任务并删除它。

保存并退出你的文件。你可以通过运行你的程序而不传递任何子命令来列出待定任务。

$ go run main.go

输出

1: Read a book

"Read a book" 任务运行rm 子命令将从数据库中删除它。

$ go run main.go rm "Read a book"

如果你再次列出所有待办任务,你会注意到"Read a book" 任务不再出现,而是显示了一个添加新任务的提示。

$ go run main.go

输出

Nothing to see here
Run `add 'task'` to add a task

在这一步,你添加了一个从数据库中删除任务的功能。

总结

你已经成功地创建了一个任务管理器命令行程序,并在此过程中学习了使用MongoDB Go驱动的基本原理。