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

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

Go com MongoDB pelo driver oficial v2: bson.ObjectID no lugar do primitive da v1, structs com tags bson, filtros, $set, $inc e $push, upsert sem conflito com o $setOnInsert, inserção em massa que grava parte e devolve erro, agregação, índices e transações, que exigem replica set.
Golang

• • 18 min de leitura

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:8

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

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

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

type Produto struct {
    ID          bson.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"`
}

bson.ObjectID é o tipo Go para o _id do MongoDB — um identificador de 12 bytes: 4 de timestamp em segundos, 5 de um valor aleatório gerado uma vez por processo e 3 de um contador. Na versão 1 do driver, esse tipo ficava no pacote bson/primitive, que a versão 2 eliminou: código e tutoriais que importam primitive são da v1 e não compilam com o mongo-driver/v2. 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 = bson.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.(bson.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 = bson.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 := bson.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 errors.Is(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 := bson.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 := bson.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 := bson.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}

    // O $set lista os campos editáveis. Passar a struct inteira levaria junto
    // criado_em (zero), e o MongoDB recusa o mesmo caminho no $set e no
    // $setOnInsert: "Updating the path 'criado_em' would create a conflict".
    update := bson.M{
        "$set": bson.M{
            "descricao":     p.Descricao,
            "preco":         p.Preco,
            "categorias":    p.Categorias,
            "atributos":     p.Atributos,
            "estoque":       p.Estoque,
            "ativo":         p.Ativo,
            "atualizado_em": time.Now(),
        },
        // só na criação; o _id o próprio servidor gera
        "$setOnInsert": bson.M{"criado_em": time.Now()},
    }

    opts := options.UpdateOne().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 := bson.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
        {{Key: "$match", Value: bson.D{{Key: "ativo", Value: true}}}},

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

        // Estágio 3: agrupa por categoria
        {{Key: "$group", Value: 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
        {{Key: "$sort", Value: bson.D{{Key: "total_produtos", Value: -1}}}},

        // Estágio 5: formata a saída
        {{Key: "$project", Value: 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, mas só em replica sets e clusters fragmentados. Num servidor isolado, como o do docker run do início, a primeira operação da transação falha com Transaction numbers are only allowed on a replica set member or mongos. Para desenvolvimento, um replica set de um nó só basta: o mongod sobe com --replSet rs0 e recebe um rs.initiate() uma vez. O WithTransaction abre a transação, faz o commit se a função devolver nil e o abort se devolver erro, e repete a função diante de erros transitórios — por isso ela não deve ter efeitos fora do banco:

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

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

    sessao, err := cliente.StartSession()
    if err != nil {
        return fmt.Errorf("iniciar sessão: %w", err)
    }
    defer sessao.EndSession(ctx)

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

    // Cada operação usa o ctx recebido pela função, que carrega a sessão
    _, err = sessao.WithTransaction(ctx, func(ctx context.Context) (any, error) {
        // Verifica e reserva estoque
        var produto Produto
        err := colProdutos.FindOneAndUpdate(
            ctx,
            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 errors.Is(err, mongo.ErrNoDocuments) {
            return nil, fmt.Errorf("produto indisponível ou estoque insuficiente")
        }
        if err != nil {
            return nil, fmt.Errorf("reservar estoque: %w", err)
        }

        // Cria o pedido
        pedido := bson.M{
            "_id":          bson.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(),
        }
        if _, err := colPedidos.InsertOne(ctx, pedido); err != nil {
            return nil, fmt.Errorf("criar pedido: %w", err)
        }
        return nil, nil
    })
    return err
}

Tratando erros do MongoDB

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

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

    if errors.Is(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
}

O driver oficial traduz o modelo de documentos do MongoDB para structs com tags bson, e os operadores de atualização — $set, $inc, $push — fazem no servidor o que, num banco relacional, exigiria ler e regravar a linha. Os cuidados vêm do que o banco não impõe: o upsert não pode citar o mesmo campo no $set e no $setOnInsert, a inserção em massa não ordenada grava parte dos documentos mesmo quando devolve erro, o time.Time volta em milissegundos e em UTC, e transação só existe em replica set. E, como o driver mudou de versão, vale conferir o caminho de importação de qualquer exemplo: o pacote primitive é da v1.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Um desenvolvedor copia de um tutorial o import go.mongodb.org/mongo-driver/bson/primitive e, como o projeto usa a versão 2 do driver, acrescenta o /v2 no caminho. O go build falha. Qual é a mensagem, e o que mudou entre as versões?

Ver resposta

✓ Resposta: O pacote bson/primitive existia na versão 1 do driver e foi eliminado na versão 2, que moveu seus tipos para o próprio pacote bson. Num teste com o driver 2.9, o import de go.mongodb.org/mongo-driver/v2/bson/primitive falhou com no required module provides package go.mongodb.org/mongo-driver/v2/bson/primitive. A tradução é direta: primitive.ObjectID vira bson.ObjectID, primitive.NewObjectID vira bson.NewObjectID e primitive.ObjectIDFromHex vira bson.ObjectIDFromHex. Outras mudanças da v2 aparecem logo em seguida: mongo.Connect não recebe mais contexto, mongo.SessionContext deixou de existir, porque a sessão viaja dentro de um context.Context comum, e as opções de atualização passaram a ser separadas por operação, como options.UpdateOne(). Misturar o caminho da v2 com exemplos da v1 é a origem mais comum desses erros, e o guia de migração do driver lista as trocas uma a uma.

Exercício 2

A primeira versão do upsertProduto usava "$set": p, passando a struct inteira, junto com "$setOnInsert": bson.M{"criado_em": time.Now()}. Nenhum produto era gravado, nem os novos nem os existentes. Qual era o erro, e por que listar os campos no $set resolve?

Ver resposta

✓ Resposta: Ao serializar a struct inteira, o driver inclui todos os campos sem omitempty, e CriadoEm vai junto, com o valor zero. O servidor recebe o caminho criado_em tanto no $set quanto no $setOnInsert, e recusa a operação inteira, mesmo que o documento já exista e o $setOnInsert nem fosse ser aplicado. Num teste, a chamada devolveu Updating the path 'criado_em' would create a conflict at 'criado_em' em todas as execuções. Listar os campos editáveis no $set deixa claro o que a operação altera e tira o criado_em de lá; com essa versão, a mesma chamada criou o produto e, na segunda vez, atualizou o preço sem tocar a data de criação. Há ainda um defeito silencioso que a versão com a struct teria mesmo sem o conflito: cada atualização gravaria a data zero em criado_em, apagando a data verdadeira.

Exercício 3

O time sobe o MongoDB com o docker run do começo do artigo e chama processarPedido. Toda chamada falha antes de reservar o estoque, com o estoque intacto. Qual é a mensagem, e como preparar o ambiente de desenvolvimento?

Ver resposta

✓ Resposta: Transações exigem um replica set ou um cluster fragmentado, porque dependem do registro de operações que só existe nesses modos. O docker run do artigo sobe um servidor isolado. Num teste com o MongoDB 8, a primeira operação dentro da transação devolveu (IllegalOperation) Transaction numbers are only allowed on a replica set member or mongos, e nada foi gravado. Contra um replica set de um nó, a mesma função criou o pedido e descontou o estoque, e a segunda chamada, sem estoque suficiente, devolveu o erro de negócio, sem gravar pedido nenhum. Para desenvolvimento, basta subir o mongod com --replSet rs0, rodar rs.initiate() uma vez e acrescentar replicaSet=rs0 à URI; no Docker, o mesmo vale passando --replSet rs0 depois do nome da imagem. É importante que o ambiente dos testes seja igual ao de produção nesse ponto: um código que só é testado sem transação nunca exercita o caminho do abort.

Exercício 4

Um teste grava um produto com CriadoEm: time.Now(), lê de volta e compara com reflect.DeepEqual. Falha sempre, embora os dois horários pareçam iguais quando impressos. Com t.Equal, continua falhando. Por quê?

Ver resposta

✓ Resposta: O tipo de data do BSON guarda milissegundos desde a época Unix, sem fuso. Ao gravar, o driver descarta tudo abaixo do milissegundo e, ao ler, devolve o horário em UTC. Num teste, 12:00:00.123456789 -0300 voltou como 15:00:00.123 +0000 UTC. O reflect.DeepEqual falha pelo fuso e pela representação interna, e o Equal, que compara o instante, falha pelos 456789 nanossegundos perdidos. A correção é truncar antes de gravar, com time.Now().Truncate(time.Millisecond), e comparar com Equal, que ignora o fuso; para exibir, converte-se com In(loc) na saída. O mesmo cuidado vale para qualquer valor que passe pelo banco e seja comparado depois, como chaves de cache ou campos de concorrência otimista baseados em data.

Exercício 5

O inserirVariosProdutos recebe três produtos, e o segundo tem um nome que já existe, violando o índice único. A função devolve erro, o chamador avisa ao usuário que nada foi importado e ele tenta de novo. Na segunda tentativa, os outros dois também dão erro de duplicidade. O que aconteceu na primeira?

Ver resposta

✓ Resposta: Com SetOrdered(false), o servidor tenta todos os documentos e só reporta as falhas no fim. Num teste com três documentos e o segundo duplicado, o primeiro e o terceiro foram gravados, e o InsertMany devolveu ao mesmo tempo um resultado com dois IDs e um erro. O artigo descarta o resultado quando há erro, e a mensagem "nada foi importado" é falsa. No modo ordenado, o padrão, a inserção para no primeiro erro: no mesmo teste, só o primeiro documento entrou. Para informar o usuário corretamente, o erro pode ser examinado com errors.As(err, &bwe), sendo bwe um mongo.BulkWriteException: cada item de bwe.WriteErrors traz o Index do documento que falhou, e mongo.IsDuplicateKeyError(err) confirma a causa. Em importações que podem ser repetidas, a alternativa é tornar a operação idempotente, com upserts por uma chave natural, em vez de inserções.

Comentários

Mais em Golang

Composição vs herança: o jeito Go de reutilizar código
Composição vs herança: o jeito Go de reutilizar código

Composição em vez de herança em Go: embedding de structs e de interfaces…

Operadores, expressões e conversão de tipo
Operadores, expressões e conversão de tipo

Operadores em Go: aritméticos, de comparação, lógicos com curto-circuito e bit…

Design Patterns em Go: Repository, Factory e Strategy
Design Patterns em Go: Repository, Factory e Strategy

Design Patterns em Go medidos: Repository, Factory com registro que não perde…