Tutorial

Cómo usar Go con MongoDB utilizando el controlador de Go de MongoDB

MongoDBGoDatabases

El autor seleccionó la Free Software Foundation para recibir una donación como parte del programa Write for DOnations.

Introducción

Después de muchos años de basarse en soluciones desarrolladas por la comunidad, MongoDB anunció que estaban trabajando en un controlador oficial para Go.  Este nuevo controlador estuvo listo para la producción en marzo de 2019 con el lanzamiento de la versión v1.0.0 y, desde entonces, se ha estado actualizando de forma continua.

Al igual que los demás controladores oficiales de MongoDB, el controlador de Go es característico del lenguaje de programación Go y ofrece una manera sencilla de utilizar MongoDB como solución de base de datos para programas de Go. Está completamente integrado con la API de MongoDB y presenta todas las funciones de consulta, indexación y agregación de la API, así como otras funciones avanzadas. A diferencia de las bibliotecas de terceros, los ingenieros de MongoDB lo respaldarán por completo, por lo que puede tener la seguridad de que se seguirá desarrollando y manteniendo.

En este tutorial, empezará a utilizar el controlador de oficial de Go de MongoDB. Instalará el controlador, establecerá conexión con una base de datos de MongoDB y realizará varias operaciones CRUD. En el proceso, creará un programa de administración de tareas para gestionar tareas a través de la línea de comandos.

Requisitos previos

Para este tutorial, necesitará lo siguiente:

  • Go instalado en su máquina y un espacio de trabajo de Go configurado conforme a Cómo instalar Go y configurar un entorno de programación local. En este tutorial, el proyecto se denominará tasker. Necesitará tener instalada la versión 1.11 o superior de Go en su máquina y los módulos de Go habilitados.
  • MongoDB instalada para su sistema operativo conforme a Cómo instalar MongoDB. MongoDB 2.6 o superior, que es la versión mínima que admite el controlador de Go de MongoDB.

Si utiliza Go v1.11 o 1.12, asegúrese de que los módulos de Go estén habilitados fijando la variable de entorno GO111MODULE en on como se indica a continuación:

  • export GO111MODULE="on"

Para obtener más información sobre la implementación de variables de entorno, consulte el tutorial Cómo leer y establecer variables de entorno y shell.

Los comandos y el código que se muestran en esta guía se probaron con Go v1.14.1 y MongoDB v3.6.3.

Paso 1: Instalar el controlador de Go de MongoDB

En este paso, instalará el paquete del controlador de Go para MongoDB y lo importará en su proyecto. También establecerá conexión con su base de datos de MongoDB y verificará el estado de la conexión.

Proceda a crear un directorio nuevo para este tutorial en su sistema de archivos:

  • mkdir tasker

Una vez que haya establecido el directorio de su proyecto, posiciónese en él con el siguiente comando:

  • cd tasker

A continuación, inicie el proyecto de Go con un archivo go.mod. Este archivo define los requisitos del proyecto y bloquea las dependencias en sus versiones correctas:

  • go mod init

Si el directorio de su proyecto está fuera de $GOPATH, debe especificar la ruta de importación de su módulo de la siguiente manera:

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

En este punto, su archivo go.mod tendrá el siguiente aspecto:

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

go 1.14

Agregue el controlador de Go de MongoDB como dependencia de su proyecto utilizando el siguiente comando:

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

Verá un resultado como el siguiente:

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

En este punto, su archivo go.mod tendrá el siguiente aspecto:

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

go 1.14

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

A continuación, cree un archivo main.go en el root de su proyecto y ábralo en su editor de texto:

  • nano main.go

Para comenzar a usar el controlador, importe los siguientes paquetes en su archivo main.go:

main.go
package main

import (
    "context"
    "log"

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

Aquí, añade los paquetes mongo y options, que proporciona el controlador de Go de MongoDB.

A continuación, después de realizar sus importaciones, cree un cliente de MongoDB nuevo y establezca conexión con su servidor de MongoDB en ejecución:

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() acepta los objetos Context y options.ClientOptions, que se utiliza para establecer la cadena de conexión y otros ajustes del controlador. Puede consultar las opciones de configuración disponibles en la documentación del paquete de opciones.

El contexto es como un tiempo de espera o un plazo que indica cuándo una operación debe dejar de funcionar y reanudarse. Ayuda a prevenir la degradación del rendimiento en los sistemas de producción cuando determinadas operaciones se ejecutan con lentitud. En este código, está pasando context.TODO() para indicar que no está seguro de qué contexto usar en este momento, pero que planea añadir uno en el futuro.

A continuación, vamos a asegurarnos de que su servidor de MongoDB se haya encontrado y conectado con éxito utilizando el método Ping.  Añada el siguiente código en la función init:

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

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

Si hay algún error al establecer conexión con la base de datos, el programa debe bloquearse mientras intenta solucionar el problema, dado que no tiene sentido mantenerlo en ejecución sin una conexión activa con la base de datos.

Añada el siguiente código para crear una base de datos:

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

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

Creó una base de datos tasker y una colección task para almacenar las tareas que creará. También estableció collection como variable de nivel de paquete para poder reutilizar la conexión a la base de datos en todo el paquete.

Guarde el archivo y ciérrelo.

En este punto, todo main.go tiene la siguiente estructura:

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

Configuró su programa para establecer conexión con su servidor de MongoDB usando el controlador de Go. En el siguiente paso, procederá a crear su programa de administración de tareas.

Paso 2: Crear un programa de CLI

En este paso, instalará el famoso paquete cli para ayudar a desarrollar su programa de administración de tareas. Ofrece una interfaz que puede aprovechar para crear rápidamente herramientas de línea de comandos modernas. Por ejemplo, este paquete proporciona la capacidad de definir subcomandos para su programa con el fin de obtener una experiencia de línea de comandos más similar a git.

Ejecute el siguiente comando para añadir el paquete como dependencia:

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

Luego, vuelva a abrir su archivo main.go:

  • nano main.go

Añada el siguiente código resaltado a su archivo 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"
)
. . .

Importó el paquete cli como se indicó. También importó el paquete os, que usará para pasar argumentos de línea de comandos a su programa:

Añada el siguiente código después de la función init para crear su programa de CLI y hacer que su código se compile:

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 fragmento crea un programa de CLI denominado tasker y añade una descripción de uso breve que se imprimirá al ejecutar el programa. El segmento de Commands es donde agregará comandos para su programa. El comando Run redistribuye el segmento de argumentos al comando correspondiente.

Guarde y cierre su archivo.

Este es el comando que necesita para compilar y ejecutar el programa:

  • go run main.go

Verá el siguiente 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)

El programa ejecuta y muestra texto de ayuda, que es útil para aprender sobre lo que puede hacer el programa y cómo usarlo.

En los siguientes pasos, mejorará la utilidad de su programa añadiendo subcomandos para ayudar a gestionar sus tareas en MongoDB.

Paso 3: Crear una tarea

En este paso, agregará un subcomando a su programa de CLI utilizando el paquete cli. Al final de esta sección, podrá añadir una tarea nueva a la base de datos de MongoDB utilizando un comando add nuevo en su programa de CLI.

Comience por abrir su archivo main.go:

  • nano main.go

Luego, importe los paquetes go.mongodb.org/mongo-driver/bson/primitive, time y 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"
)
. . .

Luego, cree una nueva estructura para representar una sola tarea en la base de datos e insértela inmediatamente antes de la función 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"`
}
. . .

Utilizó el paquete primitive para establecer el tipo de ID de cada tarea, dado que MongoDB utiliza ObjectID para el campo _id por defecto. Otro comportamiento predeterminado de MongoDB es que el nombre de campo en minúsculas se utiliza como la clave de cada campo exportado cuando se está serializado, pero esto se puede modificar utilizando las etiquetas de estructura bson.

A continuación, cree una función que reciba una instancia de Task y la guarde en la base de datos. Añada este fragmento después de la función main:

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

El método collection.InsertOne() inserta la tarea proporcionada en la colección de la base de datos y devuelve la ID del documento que se insertó. Como no necesita esta ID, la descarta al asignar al operador de guion bajo.

El siguiente paso es añadir un comando nuevo a su programa de administración de tareas para crear tareas nuevas. Lo llamaremos 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)
    }
}

Todos los comandos nuevos que se añaden a su programa de CLI se colocan dentro del segmento de Commands. Cada uno consta de un nombre, una descripción de uso y una acción. Este es el código que se ejecutará al ejecutar el comando.

Con este código, se recoge el primer argumento en add y se utiliza para establecer la propiedad Text de una nueva instancia de Task a la vez que se asignan los valores predeterminados correspondientes de las demás propiedades. La tarea nueva se pasa posteriormente a createTask, que inserta la tarea en la base de datos y devuelve nil si todo está bien, lo que provoca que el comando se cierre.

Guarde y cierre su archivo.

Pruébelo al añadir algunas tareas utilizando el comando add. Si todo es correcto, no verá errores en la pantalla:

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

Ahora que puede añadir tareas correctamente, implementaremos una manera de mostrar todas las tareas que añadió a la base de datos.

Paso 4: Enumerar todas las tareas

Los documentos de una colección se pueden enumerar utilizando el método collection.Find(), que espera un filtro y un indicador a un valor en el que se pueda decodificar el resultado.  El valor que devuelve es un cursor, que proporciona un flujo de documentos que se pueden iterar y decodificar de a uno a la vez. El cursor se cierra una vez que se agota.

Abra su archivo main.go:

  • nano main.go

Asegúrese de importar el paquete 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"
)
. . .

Luego, cree las siguientes funciones inmediatamente después de 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 con codificado binario) es la forma en que los documentos se representan en una base de datos de MongoDB, y el paquete bson es lo que nos ayuda a trabajar con los objetos de BSON en Go. El tipo bson.D que se utiliza en la función getAll() representa un documento de BSON y se utiliza cuando el orden de las propiedades es importante. Al pasar bson.D{{}} como su filtro a filterTasks(), indica que desea hacer coincidir todos los documentos de la colección.

En la función filterTasks(), itera sobre el cursor que devuelve el método collection.Find() y decodifica cada documento en una instancia de Task. Luego, cada Task se anexa al segmento de tareas creadas al principio de la función. Una vez que el cursor se agota, se cierra y se devuelve el segmento de tasks.

Antes de crear un comando para enumerar todas las tareas, crearemos una función de ayuda que tome un segmento de tasks e imprima en la salida estándar. Utilizará el paquete color para dar color al resultado.

Para poder utilizar este paquete, instálelo con lo siguiente:

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

Verá el siguiente resultado:

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

Impórtelo en su archivo main.go junto con el paquete 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"
)
. . .

Luego, cree una función printTasks nueva después de su función 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)
        }
    }
}
. . .

Esta función printTasks toma un segmento de tasks, itera sobre cada una de ellas y las imprime en la salida estándar utilizando el color verde para indicar las tareas completadas y el amarillo para las tareas incompletas.

Proceda a agregar las siguientes líneas resaltadas para crear un comando all nuevo en el segmento de Commands. Este comando imprimirá todas las tareas añadidas a la salida estándar:

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

. . .

El comando all obtiene todas las tareas presentes en la base de datos y las imprime en la salida estándar. Si no hay tareas presentes, en su lugar, se imprime una línea de comandos para añadir una tarea nueva.

Guarde y cierre su archivo.

Compile y ejecute su programa con el comando all:

  • go run main.go all

Enumerará todas las tareas que añadió hasta ahora:

Output
1: Learn Go 2: Read a book

Ahora que puede ver todas las tareas de la base de datos, añadiremos la capacidad de marcar una tarea como completa en el siguiente paso.

Paso 5: Completar una tarea

En este paso, creará un subcomando nuevo denominado done que le permitirá marcar una tarea existente en la base de datos como completada. Para marcar una tarea como completada, puede utilizar el método collection.FindOneAndUpdate(). Le permite localizar un documento en una colección y actualizar todas sus propiedades o algunas de ellas. Este método requiere un filtro para localizar el documento y un documento de actualización para describir la operación. Los dos se crean utilizando tipos bson.D.

Comience por abrir su archivo main.go:

  • nano main.go

A continuación, inserte el siguiente fragmento después de la función de 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)
}
. . .

La función coincide con el primer documento en el que la propiedad de texto es igual al parámetro text. El documento update especifica que la propiedad completed se establezca en true. Si hay un error en la operación FindOneAndUpdate(), se devolverá mediante completeTask(). De lo contrario, se devuelve nil.

A continuación, añadiremos un comando done a su programa de CLI que marca una tarea como completada:

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

. . .

Utiliza el argumento que se pasó al comando done para encontrar el primer documento cuya propiedad text coincida. Si se encuentra, la propiedad completed del documento se establece en true.

Guarde y cierre su archivo.

Luego, ejecute su programa con el comando done:

  • go run main.go done "Learn Go"

Si vuelve a utilizar el comando all, observará que la tarea que se marcó como completada, ahora, se imprime en color verde.

  • go run main.go all

Captura de pantalla del resultado de la terminal después de completar una tarea

A veces, solo desea ver las tareas que todavía no se completaron. Añadiremos esa función a continuación.

Paso 6: Mostrar únicamente tareas pendientes

En este paso, incorporará código para obtener las tareas pendientes de la base de datos utilizando el controlador de MongoDB. Las tareas pendientes son las que tienen la propiedad completed establecida en false.

Vamos a añadir una función nueva para obtener las tareas que todavía no se completaron. Abra su archivo main.go:

  • nano main.go

Luego, añada este fragmento después de la función completeTask:

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

    return filterTasks(filter)
}
. . .

Creó un filtro utilizando los paquetes bson y primitive del controlador de MongoDB, que devolverá los documentos que tengan la propiedad completed establecida en false. Luego, el segmento de tareas pendientes se devuelve al autor de la llamada.

En lugar de crear un comando nuevo para enumerar las tareas pendientes, vamos a hacer que sea la acción predeterminada cuando se ejecute el programa sin comandos. Para hacerlo, vamos a añadir una propiedad Action al programa de la siguiente manera:

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

La propiedad Action realiza una acción predeterminada cuando el programa se ejecuta sin ningún subcomando. Aquí es donde se aplica la lógica para enumerar las tareas pendientes. Se invoca la función get getPending() y las tareas resultantes se imprimen en la salida estándar utilizando printTasks(). Si no hay tareas pendientes, se muestra un aviso en su lugar, que anima al usuario a añadir una nueva tarea usando el comando add.

Guarde y cierre su archivo.

Ahora, al ejecutar el programa sin añadir ningún comando, se enumerarán todas las tareas pendientes en la base de datos:

  • go run main.go

Verá el siguiente resultado:

Output
1: Read a book

Ahora que puede enumerar tareas incompletas, vamos a añadir otro comando que permite ver únicamente las tareas completadas.

Paso 7: Mostrar tareas completadas

En este paso, agregará un subcomando finished nuevo que obtiene las tareas completadas de la base de datos y las muestra en la pantalla. Esto incluye filtrar y devolver las tareas que tengan la propiedad completed establecida en true.

Abra su archivo main.go:

  • nano main.go

Luego, añada el siguiente código al final de su archivo:

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

    return filterTasks(filter)
}
. . .

De forma similar a lo que hizo con la función getPending(), añadió una función getFinished() que devuelve un segmento de tareas completadas. En este caso, el filtro tiene la propiedad completed establecida en true, por lo tanto, solo se devolverán los documentos que coincidan con esta condición.

A continuación, cree un comando finished que imprima todas las tareas completadas:

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

El comando finished obtiene las tareas que tienen la propiedad completed establecida en true a través de la función getFinished() creada aquí. Luego, las pasa a la función printTasks para que se impriman en la salida estándar.

Guarde y cierre su archivo.

Ejecute el siguiente comando:

  • go run main.go finished

Verá el siguiente resultado:

Output
1: Learn Go

En el paso final, proporcionará a los usuarios la opción de eliminar tareas de la base de datos.

Paso 8: Eliminar una tarea

En este paso, agregará un subcomando delete para permitir que los usuarios puedan eliminar tareas de la base de datos. Para eliminar una tarea, utilizará el método collection.DeleteOne() del controlador de MongoDB. También utiliza un filtro para buscar el documento que se eliminará.

Vuelva a abrir su archivo main.go una vez más:

  • nano main.go

Añada esta función deleteTask para eliminar tareas de la base de datos directamente después de su función 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
}
. . .

Este método deleteTask toma un argumento de cadena que representa el elemento de tarea que se va a eliminar. Se crea un filtro para buscar el elemento de tarea que tenga la propiedad text establecida en el argumento de cadena. Pasa el filtro al método DeleteOne() que coincide con el elemento de la colección y lo elimina.

Puede verificar la propiedad DeletedCount en el resultado del método DeleteOne para verificar si se eliminaron documentos. Si el filtro no encuentra un documento para eliminar, DeletedCount será cero y, en ese caso, puede devolver un error.

Ahora, añada un comando rm nuevo como se indica en la sección resaltada:

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

Al igual que con todos los demás subcomandos añadidos anteriormente, el comando rm utiliza su primer argumento para buscar una tarea en la base de datos y la elimina.

Guarde y cierre su archivo.

Puede enumerar tareas pendientes ejecutando su programa sin pasar ningún subcomando de la siguiente manera:

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

Al ejecutar el subcomando rm en la tarea de "Read a book", se eliminará de la base de datos:

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

Si vuelve a enumerar todas las tareas pendientes, observará que la tarea "Read a book" ya no aparece y, en su lugar, se muestra un aviso para añadir una nueva tarea:

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

En este paso, añadió una función para eliminar tareas de la base de datos.

Conclusión

Creó correctamente un programa de línea de comandos de administración de tareas y aprendió los conceptos básicos del uso del controlador de Go de MongoDB en el proceso.

Asegúrese de consultar la documentación completa del controlador de Go de MongoDB en GoDoc para obtener más información sobre las funciones que proporciona su utilización. La documentación que describe el uso de agregaciones o transacciones puede resultarle particularmente interesante.

Puede ver el código final de este tutorial en este repositorio de GitHub.

Creative Commons License