Tutorial

Como usar Go com o MongoDB usando o driver Go do MongoDB

MongoDBGoDatabases

O autor selecionou a Free Software Foundation para receber uma doação como parte do programa Write for DOnations.

Introdução

Após contar com soluções desenvolvidas pela comunidade por muitos anos, o MongoDB anunciou que estava trabalhando em um driver oficial para Go. Em março 2019, esse novo driver atingiu um status de pronto para a produção com o lançamento da versão v1.0.0 e tem sido atualizado continuamente desde então.

Assim como os outros drivers oficiais do MongoDB, o driver Go compartilha a linguagem de programação Go e fornece uma maneira fácil de usar o MongoDB como a solução de banco de dados para um programa Go. Ele está totalmente integrado com a API do MongoDB e expõe todas as consultas, indexações e recursos de agregação da API, além de outros recursos avançados. Ao contrário das bibliotecas de terceiros, ele receberá total suporte dos engenheiros do MongoDB para que você possa ter certeza de sua continuidade de desenvolvimento e manutenção.

Neste tutorial, você começará a usar o driver Go oficial do MongoDB. Você instalará o driver, se conectará a um banco de dados do MongoDB e executará várias operações CRUD. No processo, você criará um programa gerenciador de tarefas para gerenciar tarefas através da linha de comando.

Pré-requisitos

Para este tutorial, você precisará do seguinte:

  • O Go instalado em sua máquina e um espaço de trabalho de Go configurado seguindo Como instalar o Go e configurar um ambiente de programação local. Neste tutorial, o projeto se chamará tasker. Você precisará da versão do Go v1.11 ou mais recente instalada em sua máquina com o Go Modules habilitado.
  • O MongoDB instalado para seu sistema operacional seguindo Como instalar o MongoDB. O MongoDB 2.6 ou superior é a versão mínima suportada pelo driver Go do MongoDB.

Se você estiver usando o Go v1.11 ou 1.12, certifique-se de que o Go Modules esteja habilitado definindo a variável de ambiente GO111MODULE para on, como mostrado a seguir:

  • export GO111MODULE="on"

Para obter mais informações sobre a implementação das variáveis de ambiente, leia este tutorial sobre Como ler e configurar variáveis de ambiente e de shell.

O código e os comandos mostrados neste guia foram testados com o Go v1.14.1 e o MongoDB v3.6.3.

Passo 1 — Instalando o driver Go do MongoDB

Neste passo, você instalará o pacote Go Driver para o MongoDB e o importará para seu projeto. Você também se conectará ao seu banco de dados MongoDB e verificará o status da conexão.

Vá em frente e crie um novo diretório para este tutorial em seu sistema de arquivos:

  • mkdir tasker

Assim que seu diretório do projeto estiver configurado, vá até ele com o seguinte comando:

  • cd tasker

Em seguida, inicialize o projeto Go com um arquivo go.mod. Este arquivo define requisitos de projeto e fixa dependências para suas versões corretas:

  • go mod init

Se seu diretório do projeto estiver fora de $GOPATH, você precisa especificar o caminho de importação para o seu módulo como mostrado:

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

Neste ponto, seu arquivo go.mod se parecerá com este:

go.mod
module github.com/<your_username>/tasker

go 1.14

Adicione o driver Go do MongoDB ao seu projeto com o seguinte comando:

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

Você verá uma saída como a seguinte:

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

Neste ponto, seu arquivo go.mod se parecerá com este:

go.mod
module github.com/<your_username>/tasker

go 1.14

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

Em seguida, crie um arquivo main.go na raiz de projeto e abra-o em seu editor de texto:

  • nano main.go

Para começar a usar o driver, importe os seguintes pacotes para seu arquivo main.go:

main.go
package main

import (
    "context"
    "log"

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

Aqui, você adiciona os pacotes mongo e options, que o driver Go do MongoDB oferece.

Em seguida, após suas importações, crie um novo cliente MongoDB e se conecte ao seu servidor MongoDB em execução:

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

O mongo.Connect() aceita um Context e um objeto options.ClientOptions, que é usado para definir a string de conexão e outras configurações do driver. Você pode visitar a documentação de pacotes de opções para ver quais opções de configuração estão disponíveis.

O Context é como um tempo limite ou prazo que indica quando uma operação deve parar de ser executada e retornar. Ele ajuda a evitar a degradação de desempenho em sistemas de produção quando operações específicas apresentam lentidão. Neste código, você está passando o context.TODO() para indicar que você não tem certeza sobre qual contexto usar agora, mas planeja adicionar um no futuro.

Em seguida, vamos garantir que seu servidor MongoDB tenha sido encontrado e que você tenha se conectado a ele com sucesso usando o método Ping. Adicione o código a seguir dentro da função init:

main.go
. . .
    log.Fatal(err)
  }

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

Se houver erros ao se conectar ao banco de dados, o programa deve falhar enquanto você tenta corrigir o problema, pois não há motivos para manter o programa em execução sem uma conexão com o banco de dados ativa.

Adicione o código a seguir para criar um banco de dados:

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

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

Cria-se um banco de dados tasker e uma coleção task para armazenar as tarefas que você estará criando. Define-se também uma collection (coleção) como uma variável de nível de pacote, para que você possa reutilizar a conexão com o banco de dados por todo o pacote.

Salve e saia do arquivo.

O main.go completo neste ponto é o seguinte:

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

Você configurou seu programa para se conectar ao seu servidor MongoDB usando o driver Go. No próximo passo, você prosseguirá com a criação do seu programa gerenciador de tarefas.

Passo 2 — Criando um programa CLI

Neste passo, você instalará o famoso pacote cli para ajudar com o desenvolvimento do seu programa gerenciador de tarefas. Ele oferece uma interface que você pode aproveitar para criar rapidamente ferramentas modernas de linha de comando. Por exemplo, esse pacote dá a capacidade de definir subcomandos para o seu programa, resultando em uma experiência de linha de comando semelhante ao git.

Execute o seguinte comando para adicionar o pacote como uma dependência:

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

Em seguida, abra seu arquivo main.go novamente:

  • nano main.go

Adicione o código destacado ao seu arquivo 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"
)
. . .

Importa-se o pacote cli como mencionado. Além disso, importa-se também o pacote os, que você usará para passar os argumentos de linha de comando para o seu programa:

Adicione o código a seguir após sua função init para criar seu programa CLI e fazer com que seu código seja compilado:

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

Este trecho de código cria um programa CLI chamado tasker e adiciona uma descrição curta de uso que será impressa ao executar o programa. A fatia Commands é onde você adicionará os comandos para o seu programa. O comando Run analisa a fatia de argumentos para o comando apropriado.

Salve e saia do seu arquivo.

Aqui está o comando que você precisa para compilar e executar o programa:

  • go run main.go

Você verá o seguinte resultado:

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

O programa funciona e mostra texto de ajuda, que é útil para aprender sobre o que o programa pode fazer e como usá-lo.

Nos próximos passos, você melhorará o utilitário do seu programa, adicionando subcomandos para ajudar no gerenciamento de suas tarefas no MongoDB.

Passo 3 — Criando uma tarefa

Neste passo, você adicionará um subcomando ao seu programa CLI usando o pacote cli. Ao final desta seção, você será capaz de adicionar uma nova tarefa ao seu banco de dados MongoDB , usando um novo comando add em seu programa CLI.

Comece abrindo seu arquivo main.go:

  • nano main.go

Em seguida, importe os pacotes go.mongodb.org/mongo-driver/bson/primitive, time e errors:

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"
)
. . .

Então, crie uma nova estrutura para representar uma única tarefa no banco de dados e insira-a imediatamente antes da função 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"`
}
. . .

Usa-se o pacote primitive para definir o tipo de ID de cada tarefa, uma vez que o MongoDB usa os ObjectIDs para o campo _id por padrão. Outro comportamento padrão do MongoDB é que o nome em letra minúscula do campo é usado como a chave para cada campo exportado quanto está sendo serializado. No entanto, isso pode ser alterado usando sinalizadores struct bson.

Em seguida, crie uma função que recebe uma instância de Task e salve-a no banco de dados. Adicione este trecho de código após a função main:

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

O método collection.InsertOne() insere a tarefa fornecida na coleção do banco de dados e retorna o ID do documento que foi inserido. Como você não precisa desse ID, descarte-o atribuindo-o ao operador underline.

O próximo passo é adicionar um novo comando ao seu programa gerenciador de tarefas para a criação de novas tarefas. Vamos chamá-lo de 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)
    }
}

Todo novo comando que é adicionado ao seu programa CLI é colocado dentro da fatia Commands. Cada um consiste em um nome, descrição de uso e ação. Esse é o código que será executado após a execução de comando.

Nesse código, você pega o primeiro argumento em add e usa-o para definir a propriedade Text de uma nova instância de Task, ao mesmo tempo em que atribui os padrões apropriados para as outras propriedades. A nova tarefa é subsequentemente passada ao createTask, que insere a tarefa no banco de dados e retorna nil se tudo correr bem. Isso faz com o o comando seja finalizado.

Salve e saia do seu arquivo.

Teste-o adicionando algumas tarefas com o comando add. Se o teste for bem-sucedido, você não verá erros impressos em sua tela:

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

Agora que você pode adicionar tarefas com sucesso, vamos implementar uma maneira de exibir todas as tarefas que você já adicionou ao banco de dados.

Passo 4 — Listando todas as tarefas

Listar os documentos em uma coleção pode ser feito usando o método collection.Find(), que espera receber um filtro, bem como um ponteiro para um valor no qual o resultado pode ser decodificado. Seu valor de retorno é um Cursor, que fornece um fluxo de documentos que podem ser iterados e decodificados, um de cada vez. Em seguida, o Cursor é fechado depois de esgotado.

Abra o seu arquivo main.go:

  • nano main.go

Certifique-se de importar o pacote 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"
)
. . .

Em seguida, crie as seguintes funções imediatamente após o 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
}

O BSON (JSON codificado em binário) é como os documentos são representados em um banco de dados MongoDB e o pacote bson é o que nos ajuda a trabalhar com os objetos BSON em Go. O tipo bson.D usado na função getAll() representa um documento BSON e é usado onde a ordem das propriedades importa. Ao passar o bson.D{}} como seu filtro para o filterTasks(), você está indicando que deseja combinar todos os documentos na coleção.

Na função filterTasks(), você itera sobre o Cursor retornado pelo método collection.Find() e decodifica cada documento em uma instância de Task. Em seguida, cada Task é anexada à fatia de tarefas criada no início da função. Assim que o Cursor é esgotado, ele é fechado e a fatia tasks é retornada.

Antes de você criar um comando para listar todas as tarefas, vamos criar uma função auxiliar que recebe uma fatia de tasks e imprime na saída padrão. Você usará o pacote color para colorir a saída.

Antes de poder usar esse pacote, instale-o com:

  • go get gopkg.in/gookit/color.v1

Você verá o seguinte resultado:

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

E importe-o para seu arquivo main.go, junto com o pacote fmt:

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"
)
. . .

Em seguida, crie uma nova função printTasks após sua função main:

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

Essa função printTasks recebe uma fatia de tasks, itera sobre cada uma e imprime-as na saída padrão usando a cor verde para indicar tarefas concluídas, e amarelo para tarefas incompletas.

Vá em frente e adicione as linhas destacadas a seguir para criar um novo comando all para a fatia Commands. Este comando imprimirá todas as tarefas adicionadas à saída padrão:

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

. . .

O comando all recupera todas as tarefas presentes no banco de dados e os imprime-as na saída padrão. Se nenhuma tarefa estiver presente, ao invés disso, um aviso para adicionar uma nova tarefa é impresso.

Salve e saia do seu arquivo.

Compile e execute seu programa com o comando all:

  • go run main.go all

Ele listará todas as tarefas que você adicionou até agora:

Output
1: Learn Go 2: Read a book

Agora que você pode visualizar todas as tarefas no banco de dados, vamos adicionar a capacidade de marcar uma tarefa como concluída no próximo passo.

Passo 5 — Concluindo uma tarefa

Neste passo, você criará um novo subcomando chamado done, que permitirá que você marque uma tarefa existente no banco de dados como concluída. Para marcar uma tarefa como concluída, você pode usar o método collection.FindOneAndUpdate(). Ele permite que você localize um documento em uma coleção e atualize algumas ou todas as suas propriedades. Esse método exige um filtro para localizar o documento e um documento de atualização para descrever a operação. Ambos são construídos usando tipos bson.D.

Comece abrindo seu arquivo main.go:

  • nano main.go

Insira o trecho de código após sua função 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)
}
. . .

A função corresponde ao primeiro documento onde a propriedade de texto é igual ao parâmetro text. O documento update (atualizar) especifica que a propriedade completed (concluído) está definida como true (verdadeiro). Se houver um erro na operação FindOneAndUpdate(), ele será retornado pelo completeTask(). Caso contrário, o nil é retornado.

Em seguida, vamos adicionar ao seu programa CLI um novo comando done, que marca uma tarefa como concluída:

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

. . .

Usa-se o argumento passado ao comando done para encontrar o primeiro documento cuja propriedade text é correspondente. Caso seja encontrado, a propriedade completed no documento é definida como true.

Salve e saia do seu arquivo.

Em seguida, execute seu programa com o comando done:

  • go run main.go done "Learn Go"

Se usar o comando all novamente, você notará que a tarefa que foi marcada como concluída está agora impressa em verde.

  • go run main.go all

Captura de tela da saída do terminal após completar uma tarefa

Às vezes, você deseja visualizar apenas tarefas que ainda não foram feitas. Vamos adicionar esse recurso a seguir.

Passo 6 — Exibindo somente tarefas pendentes

Neste passo, você incorporará um código para recuperar tarefas pendentes a partir do banco de dados usando o driver MongoDB. As tarefas pendentes são aquelas nas quais a propriedade completed está definida como false.

Vamos adicionar uma nova função que recupera tarefas que ainda não foram concluídas. Abra o seu arquivo main.go:

  • nano main.go

Adicione este trecho de código após a função completeTask:

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

    return filterTasks(filter)
}
. . .

Cria-se um filtro usando os pacotes bson e primitive do driver do MongoDB, que agruparão documentos cuja propriedade completed está definida como false. Em seguida, a fatia das tarefas pendentes é retornada ao autor da chamada.

Em vez de criar um novo comando para listar as tarefas pendentes, vamos tornar isso a ação padrão ao executar o programa sem nenhum comando. Faça isso adicionando uma propriedade Action ao programa, como mostrado a seguir:

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

A propriedade Action executa uma ação padrão quando o programa é executado sem nenhum subcomando. É aqui que a lógica de listar as tarefas pendentes é colocada. A função getPending() é chamada e as tarefas resultantes são impressas na saída padrão usando o printTasks(). Se não houver tarefas pendentes, ao invés disso, um aviso é exibido, encorajando o usuário a adicionar uma nova tarefa usando o comando add.

Salve e saia do seu arquivo.

Executar o programa agora sem adicionar nenhum comando listará todas as tarefas pendentes no banco de dados:

  • go run main.go

Você verá o seguinte resultado:

Output
1: Read a book

Agora que você pode listar as tarefas incompletas, vamos adicionar outro comando que permite que você veja apenas tarefas concluídas.

Passo 7 — Exibindo tarefas finalizadas

Neste passo, você adicionará um novo subcomando finished (finalizado) que busca as tarefas concluídas no banco de dados e as exibe na tela. Isso envolve a filtragem e a devolução de tarefas cuja propriedade completed estiver definida como true.

Abra o seu arquivo main.go:

  • nano main.go

Então, adicione ao código a seguir ao final do seu arquivo:

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

    return filterTasks(filter)
}
. . .

De maneira similar à função getPending(), você adicionou uma função getFinished(), que retorna uma parte das tarefas concluídas. Neste caso, o filtro tem a propriedade completed definida como true, de modo que apenas os documentos que correspondem a essa condição serão retornados.

Em seguida, crie um comando finished que imprime todas as tarefas concluídas:

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

O comando finished recupera as tarefas cuja propriedade completed está definida como true através da função getFinished() aqui criada. Em seguida, ele passa-as para a função printTasks, de modo que elas sejam impressas na saída padrão.

Salve e saia do seu arquivo.

Execute o seguinte comando:

  • go run main.go finished

Você verá o seguinte resultado:

Output
1: Learn Go

No passo final, você dará aos usuários a opção de excluir tarefas do banco de dados.

Passo 8 — Excluindo uma tarefa

Neste passo, você adicionará um novo subcomando delete (excluir) para permitir que os usuários excluam uma tarefa do banco de dados. Para excluir uma única tarefa, você usará o método collection.DeleteOne() do driver do MongoDB. Ele também depende de um filtro que corresponda ao documento a ser excluído.

Abra seu arquivo main.go mais uma vez:

  • nano main.go

Adicione essa função deleteTask para excluir tarefas do banco de dados, logo após sua função getFinished:

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

Esse método deleteTask recebe um argumento em string que representa o item da tarefa a ser excluído. Um filtro é construído para que corresponda ao item de tarefa cuja propriedade text está definida como o argumento em string. Passa-se o filtro ao método DeleteOne(), que encontra o item na coleção e o exclui.

Você pode verificar a propriedade DeletedCount no resultado do método DeleteOne para confirmar se algum documento foi excluído. Se o filtro não corresponder a um documento que será excluído, o DeletedCount será zero. Neste caso, retorne um erro.

Agora, adicione um novo comando rm, como destacado:

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

Assim como em todos os outros subcomandos adicionados anteriormente, o comando rm usa seu primeiro argumento para encontrar uma tarefa no banco de dados e a exclui.

Salve e saia do seu arquivo.

Você pode listar as tarefas pendentes executando seu programa sem passar nenhum subcomando:

  • go run main.go
Output
1: Read a book

Executar o subcomando rm na tarefa "Read a book" a excluirá do banco de dados:

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

Se listar todas as tarefas pendentes novamente, você notará que a tarefa "Read a book" não aparece mais e, ao invés disso, um prompt para adicionar uma nova tarefa é mostrado:

  • go run main.go
Output
Nothing to see here Run `add 'task'` to add a task

Neste passo, você adicionou uma função para excluir tarefas do banco de dados.

Conclusão

Você criou um programa de linha de comando gerenciador de tarefas e aprendeu os fundamentos para usar o driver Go do MongoDB no processo.

Certifique-se de verificar a documentação completa para o driver Go do MongoDB no GoDoc para aprender mais sobre os recursos oferecidos pelo driver. A documentação que descreve o uso de agregações ou transações pode despertar seu interesse.

O código final para este tutorial pode ser visualizado neste repositório do GitHub.

0 Comments

Creative Commons License