Golang

Go com MongoDB: operações CRUD com o driver oficial Já leu

11 min de leitura

Go com MongoDB: operações CRUD com o driver oficial
MongoDB armazena dados como documentos BSON — uma representação binária de JSON. Em vez de linhas em tabelas com esquema fixo, cada documento pode ter estrutura diferente dentro da mesma coleção. Isso o torna especialmen

MongoDB armazena dados como documentos BSON — uma representação binária de JSON. Em vez de linhas em tabelas com esquema fixo, cada documento pode ter estrutura diferente dentro da mesma coleção. Isso o torna especialmente adequado para dados com estrutura variável, hierarquias naturais, catálogos de produtos com atributos heterogêneos e cenários onde o esquema evolui rapidamente.

A decisão de usar MongoDB versus um banco relacional não é sobre qual é melhor — é sobre qual modelo de dados representa mais naturalmente o problema em questão. Aplicações que naturalmente pensam em documentos aninhados se beneficiam do MongoDB. Aplicações com relacionamentos complexos e integridade referencial forte se beneficiam do modelo relacional.


Instalação e configuração

# Driver oficial Go para MongoDB
go get go.mongodb.org/mongo-driver/v2/mongo

# MongoDB com Docker
docker run --name mongodb-dev \
    -e MONGO_INITDB_ROOT_USERNAME=admin \
    -e MONGO_INITDB_ROOT_PASSWORD=senha \
    -p 27017:27017 \
    -d mongo:7

Conexão e configuração do cliente

package main

import (
    "context"
    "fmt"
    "log"
    "time"

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

func conectarMongoDB(ctx context.Context, uri string) (*mongo.Client, error) {
    opts := options.Client().
        ApplyURI(uri).
        SetMaxPoolSize(25).
        SetMinPoolSize(5).
        SetMaxConnIdleTime(2 * time.Minute).
        SetConnectTimeout(10 * time.Second).
        SetServerSelectionTimeout(5 * time.Second)

    cliente, err := mongo.Connect(opts)
    if err != nil {
        return nil, fmt.Errorf("conectar MongoDB: %w", err)
    }

    // Verifica conectividade com o servidor primário
    pingCtx, cancelar := context.WithTimeout(ctx, 5*time.Second)
    defer cancelar()

    if err := cliente.Ping(pingCtx, readpref.Primary()); err != nil {
        cliente.Disconnect(ctx)
        return nil, fmt.Errorf("verificar conexão: %w", err)
    }

    log.Println("MongoDB conectado com sucesso")
    return cliente, nil
}

func main() {
    ctx := context.Background()

    uri := "mongodb://admin:senha@localhost:27017/?authSource=admin"
    cliente, err := conectarMongoDB(ctx, uri)
    if err != nil {
        log.Fatal(err)
    }
    defer cliente.Disconnect(ctx)

    // Referências ao banco e coleção
    db := cliente.Database("loja")
    colecao := db.Collection("produtos")
    fmt.Println("Coleção pronta:", colecao.Name())
}

Tipos e mapeamento com BSON

O MongoDB usa BSON internamente. O driver Go mapeia structs para BSON através de tags bson:

import (
    "time"
    "go.mongodb.org/mongo-driver/v2/bson"
    "go.mongodb.org/mongo-driver/v2/bson/primitive"
)

type Endereco struct {
    Rua    string `bson:"rua"`
    Cidade string `bson:"cidade"`
    Estado string `bson:"estado"`
    CEP    string `bson:"cep"`
}

type Avaliacao struct {
    UsuarioID primitive.ObjectID `bson:"usuario_id"`
    Nota      float64            `bson:"nota"`
    Comentario string            `bson:"comentario,omitempty"`
    Data      time.Time          `bson:"data"`
}

type Produto struct {
    ID          primitive.ObjectID `bson:"_id,omitempty"`
    Nome        string             `bson:"nome"`
    Descricao   string             `bson:"descricao,omitempty"`
    Preco       float64            `bson:"preco"`
    Categorias  []string           `bson:"categorias"`
    Atributos   bson.M             `bson:"atributos,omitempty"` // mapa flexível
    Estoque     int                `bson:"estoque"`
    Ativo       bool               `bson:"ativo"`
    Avaliacoes  []Avaliacao        `bson:"avaliacoes,omitempty"`
    CriadoEm   time.Time          `bson:"criado_em"`
    AtualizadoEm time.Time        `bson:"atualizado_em"`
}

primitive.ObjectID é o tipo Go para o _id do MongoDB — um identificador de 12 bytes que inclui timestamp, identificador de máquina e contador sequencial. bson.M é um map[string]any para documentos sem estrutura fixa.


Insert: inserindo documentos

func inserirProduto(ctx context.Context, col *mongo.Collection, p *Produto) error {
    if p.ID.IsZero() {
        p.ID = primitive.NewObjectID()
    }
    p.CriadoEm = time.Now()
    p.AtualizadoEm = time.Now()

    resultado, err := col.InsertOne(ctx, p)
    if err != nil {
        return fmt.Errorf("inserir produto: %w", err)
    }

    // O ID inserido é retornado como interface{}
    id, ok := resultado.InsertedID.(primitive.ObjectID)
    if ok {
        p.ID = id
    }

    fmt.Printf("Produto inserido: %s\n", p.ID.Hex())
    return nil
}

func inserirVariosProdutos(ctx context.Context, col *mongo.Collection, produtos []Produto) error {
    docs := make([]any, len(produtos))
    for i := range produtos {
        produtos[i].ID = primitive.NewObjectID()
        produtos[i].CriadoEm = time.Now()
        produtos[i].AtualizadoEm = time.Now()
        docs[i] = produtos[i]
    }

    opts := options.InsertMany().SetOrdered(false) // continua mesmo se um falhar
    resultado, err := col.InsertMany(ctx, docs, opts)
    if err != nil {
        return fmt.Errorf("inserir produtos: %w", err)
    }

    fmt.Printf("%d produtos inseridos\n", len(resultado.InsertedIDs))
    return nil
}

FindOne e Find: buscando documentos

func buscarPorID(ctx context.Context, col *mongo.Collection, id string) (*Produto, error) {
    oid, err := primitive.ObjectIDFromHex(id)
    if err != nil {
        return nil, fmt.Errorf("ID inválido: %w", err)
    }

    filtro := bson.M{"_id": oid}

    var produto Produto
    err = col.FindOne(ctx, filtro).Decode(&produto)
    if err == mongo.ErrNoDocuments {
        return nil, fmt.Errorf("produto %s não encontrado", id)
    }
    if err != nil {
        return nil, fmt.Errorf("buscar produto: %w", err)
    }

    return &produto, nil
}

type FiltroProduto struct {
    Categoria  string
    PrecoMin   float64
    PrecoMax   float64
    ApenasAtivo bool
    Limite     int64
    Pagina     int64
}

func listarProdutos(ctx context.Context, col *mongo.Collection, f FiltroProduto) ([]Produto, error) {
    // Construção dinâmica do filtro
    filtro := bson.D{}

    if f.Categoria != "" {
        filtro = append(filtro, bson.E{Key: "categorias", Value: f.Categoria})
    }

    if f.ApenasAtivo {
        filtro = append(filtro, bson.E{Key: "ativo", Value: true})
    }

    if f.PrecoMin > 0 || f.PrecoMax > 0 {
        preco := bson.D{}
        if f.PrecoMin > 0 {
            preco = append(preco, bson.E{Key: "$gte", Value: f.PrecoMin})
        }
        if f.PrecoMax > 0 {
            preco = append(preco, bson.E{Key: "$lte", Value: f.PrecoMax})
        }
        filtro = append(filtro, bson.E{Key: "preco", Value: preco})
    }

    limite := f.Limite
    if limite <= 0 {
        limite = 20
    }

    skip := (f.Pagina - 1) * limite
    if skip < 0 {
        skip = 0
    }

    opts := options.Find().
        SetSort(bson.D{{Key: "nome", Value: 1}}).
        SetLimit(limite).
        SetSkip(skip)

    cursor, err := col.Find(ctx, filtro, opts)
    if err != nil {
        return nil, fmt.Errorf("listar produtos: %w", err)
    }
    defer cursor.Close(ctx)

    var produtos []Produto
    if err := cursor.All(ctx, &produtos); err != nil {
        return nil, fmt.Errorf("decodificar produtos: %w", err)
    }

    return produtos, nil
}

bson.D é uma slice ordenada de pares chave-valor — útil quando a ordem dos campos importa. bson.M é um mapa não ordenado — mais conveniente para filtros simples.


Update: atualizando documentos

func atualizarPreco(ctx context.Context, col *mongo.Collection, id string, novoPreco float64) error {
    oid, err := primitive.ObjectIDFromHex(id)
    if err != nil {
        return fmt.Errorf("ID inválido: %w", err)
    }

    filtro := bson.M{"_id": oid}
    update := bson.M{
        "$set": bson.M{
            "preco":         novoPreco,
            "atualizado_em": time.Now(),
        },
    }

    resultado, err := col.UpdateOne(ctx, filtro, update)
    if err != nil {
        return fmt.Errorf("atualizar preço: %w", err)
    }

    if resultado.MatchedCount == 0 {
        return fmt.Errorf("produto %s não encontrado", id)
    }

    return nil
}

func adicionarAvaliacao(ctx context.Context, col *mongo.Collection,
    produtoID string, av Avaliacao) error {

    oid, err := primitive.ObjectIDFromHex(produtoID)
    if err != nil {
        return fmt.Errorf("ID inválido: %w", err)
    }

    av.Data = time.Now()

    filtro := bson.M{"_id": oid}
    update := bson.M{
        "$push": bson.M{"avaliacoes": av},
        "$set":  bson.M{"atualizado_em": time.Now()},
    }

    _, err = col.UpdateOne(ctx, filtro, update)
    return err
}

func decrementarEstoque(ctx context.Context, col *mongo.Collection,
    produtoID string, quantidade int) error {

    oid, err := primitive.ObjectIDFromHex(produtoID)
    if err != nil {
        return fmt.Errorf("ID inválido: %w", err)
    }

    // $inc incrementa ou decrementa atomicamente
    filtro := bson.M{
        "_id":     oid,
        "estoque": bson.M{"$gte": quantidade}, // garante estoque suficiente
    }
    update := bson.M{
        "$inc": bson.M{"estoque": -quantidade},
        "$set": bson.M{"atualizado_em": time.Now()},
    }

    resultado, err := col.UpdateOne(ctx, filtro, update)
    if err != nil {
        return fmt.Errorf("decrementar estoque: %w", err)
    }

    if resultado.MatchedCount == 0 {
        return fmt.Errorf("produto não encontrado ou estoque insuficiente")
    }

    return nil
}

Upsert: inserir ou atualizar

func upsertProduto(ctx context.Context, col *mongo.Collection, p *Produto) error {
    filtro := bson.M{"nome": p.Nome}

    p.AtualizadoEm = time.Now()

    update := bson.M{
        "$set": p,
        "$setOnInsert": bson.M{
            "criado_em": time.Now(),
            "_id":       primitive.NewObjectID(),
        },
    }

    opts := options.Update().SetUpsert(true)
    resultado, err := col.UpdateOne(ctx, filtro, update, opts)
    if err != nil {
        return fmt.Errorf("upsert produto: %w", err)
    }

    if resultado.UpsertedCount > 0 {
        fmt.Println("Produto criado (upsert)")
    } else {
        fmt.Println("Produto atualizado (upsert)")
    }

    return nil
}

Delete: removendo documentos

func deletarProduto(ctx context.Context, col *mongo.Collection, id string) error {
    oid, err := primitive.ObjectIDFromHex(id)
    if err != nil {
        return fmt.Errorf("ID inválido: %w", err)
    }

    resultado, err := col.DeleteOne(ctx, bson.M{"_id": oid})
    if err != nil {
        return fmt.Errorf("deletar produto: %w", err)
    }

    if resultado.DeletedCount == 0 {
        return fmt.Errorf("produto %s não encontrado", id)
    }

    return nil
}

func deletarProdutosInativos(ctx context.Context, col *mongo.Collection) (int64, error) {
    resultado, err := col.DeleteMany(ctx, bson.M{"ativo": false})
    if err != nil {
        return 0, fmt.Errorf("deletar inativos: %w", err)
    }
    return resultado.DeletedCount, nil
}

Aggregation Pipeline

O aggregation pipeline é um dos recursos mais poderosos do MongoDB — permite transformar e analisar dados em múltiplos estágios:

func estatisticasPorCategoria(ctx context.Context, col *mongo.Collection) ([]bson.M, error) {
    pipeline := mongo.Pipeline{
        // Estágio 1: filtra apenas produtos ativos
        {{"$match", bson.D{{Key: "ativo", Value: true}}}},

        // Estágio 2: desaninha o array de categorias
        {{"$unwind", "$categorias"}},

        // Estágio 3: agrupa por categoria
        {{"$group", bson.D{
            {Key: "_id", Value: "$categorias"},
            {Key: "total_produtos", Value: bson.D{{Key: "$sum", Value: 1}}},
            {Key: "preco_medio", Value: bson.D{{Key: "$avg", Value: "$preco"}}},
            {Key: "preco_min", Value: bson.D{{Key: "$min", Value: "$preco"}}},
            {Key: "preco_max", Value: bson.D{{Key: "$max", Value: "$preco"}}},
            {Key: "estoque_total", Value: bson.D{{Key: "$sum", Value: "$estoque"}}},
        }}},

        // Estágio 4: ordena por total de produtos decrescente
        {{"$sort", bson.D{{Key: "total_produtos", Value: -1}}}},

        // Estágio 5: formata a saída
        {{"$project", bson.D{
            {Key: "categoria", Value: "$_id"},
            {Key: "total_produtos", Value: 1},
            {Key: "preco_medio", Value: bson.D{{Key: "$round", Value: bson.A{"$preco_medio", 2}}}},
            {Key: "preco_min", Value: 1},
            {Key: "preco_max", Value: 1},
            {Key: "estoque_total", Value: 1},
            {Key: "_id", Value: 0},
        }}},
    }

    cursor, err := col.Aggregate(ctx, pipeline)
    if err != nil {
        return nil, fmt.Errorf("aggregate: %w", err)
    }
    defer cursor.Close(ctx)

    var resultados []bson.M
    if err := cursor.All(ctx, &resultados); err != nil {
        return nil, fmt.Errorf("decodificar resultados: %w", err)
    }

    return resultados, nil
}

Índices: essenciais para performance

func criarIndices(ctx context.Context, col *mongo.Collection) error {
    indices := []mongo.IndexModel{
        // Índice único no campo nome
        {
            Keys:    bson.D{{Key: "nome", Value: 1}},
            Options: options.Index().SetUnique(true),
        },
        // Índice composto para filtragem por categoria e preço
        {
            Keys: bson.D{
                {Key: "categorias", Value: 1},
                {Key: "preco", Value: 1},
            },
        },
        // Índice para produtos ativos
        {
            Keys: bson.D{
                {Key: "ativo", Value: 1},
                {Key: "criado_em", Value: -1},
            },
        },
        // Índice de texto para busca
        {
            Keys: bson.D{
                {Key: "nome", Value: "text"},
                {Key: "descricao", Value: "text"},
            },
            Options: options.Index().
                SetDefaultLanguage("portuguese").
                SetWeights(bson.D{
                    {Key: "nome", Value: 10},
                    {Key: "descricao", Value: 1},
                }),
        },
    }

    _, err := col.Indexes().CreateMany(ctx, indices)
    if err != nil {
        return fmt.Errorf("criar índices: %w", err)
    }

    fmt.Println("Índices criados com sucesso")
    return nil
}

Transações multi-documento

MongoDB suporta transações ACID desde a versão 4.0 em replica sets:

func processarPedido(ctx context.Context, cliente *mongo.Client,
    produtoID string, quantidade int, clienteNome string) error {

    return cliente.UseSession(ctx, func(sessao mongo.SessionContext) error {
        err := sessao.StartTransaction()
        if err != nil {
            return fmt.Errorf("iniciar transação: %w", err)
        }

        db := cliente.Database("loja")
        colProdutos := db.Collection("produtos")
        colPedidos := db.Collection("pedidos")

        // Verifica e reserva estoque
        oid, _ := primitive.ObjectIDFromHex(produtoID)

        var produto Produto
        err = colProdutos.FindOneAndUpdate(
            sessao,
            bson.M{
                "_id":     oid,
                "estoque": bson.M{"$gte": quantidade},
                "ativo":   true,
            },
            bson.M{
                "$inc": bson.M{"estoque": -quantidade},
                "$set": bson.M{"atualizado_em": time.Now()},
            },
            options.FindOneAndUpdate().SetReturnDocument(options.After),
        ).Decode(&produto)

        if err == mongo.ErrNoDocuments {
            sessao.AbortTransaction(sessao)
            return fmt.Errorf("produto indisponível ou estoque insuficiente")
        }
        if err != nil {
            sessao.AbortTransaction(sessao)
            return fmt.Errorf("reservar estoque: %w", err)
        }

        // Cria o pedido
        pedido := bson.M{
            "_id":          primitive.NewObjectID(),
            "cliente_nome": clienteNome,
            "produto_id":   oid,
            "produto_nome": produto.Nome,
            "quantidade":   quantidade,
            "preco_unit":   produto.Preco,
            "total":        produto.Preco * float64(quantidade),
            "status":       "confirmado",
            "criado_em":    time.Now(),
        }

        _, err = colPedidos.InsertOne(sessao, pedido)
        if err != nil {
            sessao.AbortTransaction(sessao)
            return fmt.Errorf("criar pedido: %w", err)
        }

        return sessao.CommitTransaction(sessao)
    })
}

Tratando erros do MongoDB

import "go.mongodb.org/mongo-driver/v2/mongo"

func tratarErroMongo(err error) error {
    if err == nil {
        return nil
    }

    if err == mongo.ErrNoDocuments {
        return fmt.Errorf("documento não encontrado")
    }

    if mongo.IsDuplicateKeyError(err) {
        return fmt.Errorf("registro duplicado: %w", err)
    }

    if mongo.IsNetworkError(err) {
        return fmt.Errorf("erro de rede ao conectar ao MongoDB: %w", err)
    }

    if mongo.IsTimeout(err) {
        return fmt.Errorf("timeout na operação MongoDB: %w", err)
    }

    return err
}

Resumo do que foi coberto

Este artigo apresentou MongoDB com Go usando o driver oficial em profundidade: conexão com pgxpool, mapeamento de structs com tags bson, tipos nativos como primitive.ObjectID e bson.M, operações de insert individual e em massa, Find com filtros dinâmicos e paginação, Update com operadores como $set, $push e $inc, upsert, delete, aggregation pipeline para análise de dados, criação de índices incluindo texto e transações multi-documento. Com este artigo o módulo de banco de dados com os principais bancos está completo.


Referências e leituras complementares

  • Driver oficial MongoDB para Go — Documentação e exemplos oficiais. https://www.mongodb.com/docs/drivers/go/current/

  • Documentação MongoDB — Referência completa de operadores e aggregation. https://www.mongodb.com/docs/manual/

  • pkg.go.dev — mongo-driver — Referência da API do driver Go. https://pkg.go.dev/go.mongodb.org/mongo-driver/v2/mongo

  • MongoDB University — Cursos gratuitos oficiais incluindo Go. https://learn.mongodb.com

  • BSON specification — Especificação do formato de dados do MongoDB. https://bsonspec.org

  • MongoDB Compass — GUI oficial para visualizar e gerenciar dados MongoDB. https://www.mongodb.com/products/tools/compass

Comentários

Mais em Golang

Maps: criação, iteração e boas práticas
Maps: criação, iteração e boas práticas

Se slices s&atilde;o a espinha dorsal das sequ&ecirc;ncias em Go, maps s&atil...

Select, timeouts e cancelamento de operações
Select, timeouts e cancelamento de operações

O artigo anterior introduziu o select como mecanismo para aguardar múltiplos...

Artigo 43 — Profiling e otimização de performance em Go
Artigo 43 — Profiling e otimização de performance em Go

Artigo 43 — Profiling e otimização de performance em Go Otimizar sem medir é...