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
-
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
-
Guia de migração para o driver 2.0 — As mudanças da v1 para a v2, como o fim do pacote
primitivee doSessionContext. https://github.com/mongodb/mongo-go-driver/blob/master/docs/migration-2.0.md
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.