Overview
There is no unifying abstraction over NoSQL in Go — each store has one canonical client, and
each client hands you the store's real semantics: BSON documents, tunable consistency levels,
conditional writes. Mapping is done with struct tags (bson, dynamodbav, cql), not
managed entities, so a document is just a struct you marshal — no identity map, no lazy
loading, no session. The craft is knowing each client's pooling and consistency knobs and
modeling documents so that structs stay honest about what is actually stored.
Key points
- MongoDB — official mongo-go-driver (
go.mongodb.org/mongo-driver/v2):Client→Database→Collection; every call takes acontext.Context. Mapping viabsonstruct tags (bson:"_id,omitempty"); queries arebson.D/bson.Mdocuments, so filters are data, not a DSL. Change streams (collection.Watch) give ordered, resumable event feeds on replica sets. - Mongo consistency surfaces per call: read preference (primary/secondary), read concern, and write concern are options on client, database, collection, or single operation — nothing is hidden behind a template default.
- Cassandra/ScyllaDB — gocql: session over a token-aware, prepared-statement-cached pool;
session.Query("...").Bind(...).Consistency(gocql.Quorum)puts the consistency level in your face on every query. ScyllaDB maintains a fork (github.com/scylladb/gocql) with shard-aware routing — drop-in, worth it on Scylla. scylladb/gocqlx adds struct binding and query building on top. - DynamoDB — aws-sdk-go-v2 (
service/dynamodb): request/response structs mirror the HTTP API; thefeature/dynamodb/attributevaluepackage marshals Go structs to attribute maps viadynamodbavtags. Expression building (conditions, updates) via theexpressionpackage.ConsistentRead: trueopts single reads out of eventual consistency; transactions exist (TransactWriteItems) with item-count limits. - Couchbase — gocb (and CouchDB via go-kivik): gocb gives KV ops, SQL++ (N1QL) queries, and durability levels per mutation. Brief mentions — reach for the deep docs when needed.
- Structs, not entities: no dirty checking means updates are explicit operations
(
$set, update expressions), and partial updates are something you write, not something a framework diffs for you. This kills the read-modify-write-everything anti-pattern early. - Schema drift is the tax: old documents missing new fields decode into zero values —
design with
omitempty, pointers for "absent vs zero", and explicit migration reads.
Details
Client-by-client mechanics
| Store | Client | Mapping | Consistency knob location |
|---|---|---|---|
| MongoDB | mongo-go-driver | bson tags |
read pref / read+write concern, per op |
| Cassandra/Scylla | gocql (+ scylla fork, gocqlx) | positional bind / gocqlx db tags |
.Consistency(...) per query |
| DynamoDB | aws-sdk-go-v2 + attributevalue | dynamodbav tags |
ConsistentRead, transact APIs |
| Couchbase | gocb | json tags |
durability level per mutation |
Patterns that recur
- Contexts everywhere: all four clients take
ctxper operation — timeouts and cancellation are uniform across stores, unlike the JVM's per-driver timeout settings. - Conditional writes replace transactions in Dynamo and often Mongo:
attribute_not_existsconditions,findOneAndUpdatewith filters — model invariants as single-item conditions where possible before reaching for multi-item transactions. - Change streams / CDC: Mongo change streams are the built-in outbox alternative; Dynamo has Streams (consumed via Lambda or the Kinesis adapter); Cassandra pushes you to CDC logs.
Examples
// MongoDB: bson tags, context, conditional upsert
type Order struct {
ID bson.ObjectID `bson:"_id,omitempty"`
Customer string `bson:"customer"`
Status string `bson:"status"`
Total int64 `bson:"total_cents"`
}
coll := client.Database("shop").Collection("orders")
_, err := coll.UpdateOne(ctx,
bson.M{"_id": id, "status": "open"}, // filter is data
bson.M{"$set": bson.M{"status": "paid"}}, // explicit partial update
)
// DynamoDB: attributevalue marshaling + conditional put
item, _ := attributevalue.MarshalMap(order)
_, err = ddb.PutItem(ctx, &dynamodb.PutItemInput{
TableName: aws.String("orders"),
Item: item,
ConditionExpression: aws.String("attribute_not_exists(pk)"), // create-only
})
Related
- Storage access from Go — the map — parent overview by storage kind.
- Relational databases from Go — the SQL-side contrast: where joins and real transactions live.
- Databases and other storage systems — partitioning, replication, and consistency models these knobs expose.
- Document and NoSQL stores from Java — the same stores through Spring Data's repository idiom instead of bare clients.