Golang 資料庫操作 – NoSQL 資料庫(MongoDB、Redis)
簡介
在現代的 Web 與微服務架構中,NoSQL 資料庫 已成為不可或缺的組件。相較於傳統的關聯式資料庫,MongoDB 與 Redis 提供了彈性的資料模型與極佳的效能,特別適合處理大量的非結構化資料、快取需求或即時訊息傳遞。
對於使用 Golang 開發的後端服務而言,掌握如何在程式中正確、有效率地操作這兩種 NoSQL 資料庫,能大幅提升系統的可擴展性與開發速度。本篇文章將從核心概念出發,搭配實作範例,說明在 Go 中與 MongoDB、Redis 互動的最佳方式,並分享常見陷阱與實務應用情境,讓初學者與中階開發者都能快速上手。
核心概念
1. MongoDB 基礎
MongoDB 是一個文件導向的資料庫,資料以 BSON(類 JSON)格式存放於 collection 中。Go 官方提供的驅動 go.mongodb.org/mongo-driver 支援完整的 CRUD、索引、聚合等功能。
1.1 連線與設定
package main
import (
"context"
"log"
"time"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func main() {
// 建立 10 秒的 timeout context,避免連線卡住
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// MongoDB URI,建議使用環境變數或密碼管理服務
clientOpts := options.Client().ApplyURI("mongodb://localhost:27017")
client, err := mongo.Connect(ctx, clientOpts)
if err != nil {
log.Fatalf("MongoDB 連線失敗: %v", err)
}
// 確認連線狀態
if err = client.Ping(ctx, nil); err != nil {
log.Fatalf("MongoDB Ping 失敗: %v", err)
}
log.Println("MongoDB 連線成功")
// 使用完畢記得關閉連線
defer client.Disconnect(context.Background())
}
重點:使用
context來控制連線與操作的逾時,避免因網路問題造成程式卡死。
1.2 基本 CRUD
package main
import (
"context"
"log"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type User struct {
ID primitive.ObjectID `bson:"_id,omitempty"` // 自動產生的 ObjectID
Name string `bson:"name"`
Age int `bson:"age"`
CreatedAt time.Time `bson:"created_at"`
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
client, _ := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017"))
coll := client.Database("demo").Collection("users")
// --- Insert ---
newUser := User{Name: "Alice", Age: 28, CreatedAt: time.Now()}
insertRes, err := coll.InsertOne(ctx, newUser)
if err != nil {
log.Fatalf("Insert 錯誤: %v", err)
}
log.Printf("插入文件 ID: %v\n", insertRes.InsertedID)
// --- Find ---
var result User
filter := bson.M{"name": "Alice"}
if err = coll.FindOne(ctx, filter).Decode(&result); err != nil {
log.Fatalf("Find 錯誤: %v", err)
}
log.Printf("查詢結果: %+v\n", result)
// --- Update ---
update := bson.M{"$set": bson.M{"age": 29}}
updateRes, _ := coll.UpdateOne(ctx, filter, update)
log.Printf("更新筆數: %d\n", updateRes.ModifiedCount)
// --- Delete ---
deleteRes, _ := coll.DeleteOne(ctx, filter)
log.Printf("刪除筆數: %d\n", deleteRes.DeletedCount)
}
說明:
bson.M為 map 型別的快捷寫法,用於建構查詢或更新條件。InsertOne、FindOne、UpdateOne、DeleteOne為最常用的單筆操作;若需批次處理,可改用InsertMany、UpdateMany等。
1.3 索引與效能
在大量資料的查詢情境下,索引是提升效能的關鍵。以下示範在 age 欄位建立升冪索引,並說明如何檢查索引建立結果。
indexModel := mongo.IndexModel{
Keys: bson.D{{Key: "age", Value: 1}}, // 1 為升冪,-1 為降冪
Options: options.Index().SetName("idx_age"),
}
if _, err := coll.Indexes().CreateOne(ctx, indexModel); err != nil {
log.Fatalf("建立索引失敗: %v", err)
}
log.Println("索引 idx_age 建立完成")
最佳實踐:對於頻繁使用的查詢條件(如
user_id、
2. Redis 基礎
Redis 是一個 鍵值快取 資料庫,支援字串、哈希、列表、集合、有序集合等多種資料結構,且提供 Pub/Sub、Lua 腳本、事務等高階功能。Go 中最常用的官方客戶端是 github.com/go-redis/redis/v9。
2.1 連線與設定
package main
import (
"context"
"log"
"time"
"github.com/go-redis/redis/v9"
)
func main() {
// 建立一個 5 秒的 timeout context
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379", // Redis 伺服器位址
Password: "", // 若有密碼請填寫
DB: 0, // 使用的 DB 編號,預設 0
})
// Ping 測試連線
if err := rdb.Ping(ctx).Err(); err != nil {
log.Fatalf("Redis 連線失敗: %v", err)
}
log.Println("Redis 連線成功")
}
提示:在生產環境建議使用 連線池(Redis 客戶端內建)以及 TLS 加密,確保安全與效能。
2.2 常見資料結構操作
package main
import (
"context"
"fmt"
"log"
"github.com/go-redis/redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
// --- String ---
if err := rdb.Set(ctx, "username", "bob", 0).Err(); err != nil {
log.Fatalf("Set 錯誤: %v", err)
}
name, _ := rdb.Get(ctx, "username").Result()
fmt.Printf("username = %s\n", name)
// --- Hash ---
userKey := "user:1001"
rdb.HSet(ctx, userKey, "name", "Alice", "age", 30)
age, _ := rdb.HGet(ctx, userKey, "age").Int()
fmt.Printf("user age = %d\n", age)
// --- List (Queue) ---
queueKey := "task:queue"
rdb.RPush(ctx, queueKey, "task1", "task2", "task3")
task, _ := rdb.LPop(ctx, queueKey).Result()
fmt.Printf("取出任務: %s\n", task)
// --- Set ---
rdb.SAdd(ctx, "tags", "golang", "redis", "mongodb")
members, _ := rdb.SMembers(ctx, "tags").Result()
fmt.Printf("Set 成員: %v\n", members)
// --- Pub/Sub ---
go func() {
sub := rdb.Subscribe(ctx, "news")
ch := sub.Channel()
for msg := range ch {
fmt.Printf("收到訊息: %s\n", msg.Payload)
}
}()
// 發布訊息
if err := rdb.Publish(ctx, "news", "Hello Redis!").Err(); err != nil {
log.Fatalf("Publish 錯誤: %v", err)
}
}
說明:
Set、Get為最基本的字串操作。HSet、HGet用於儲存結構化資料(類似關聯式資料庫的一筆記錄)。RPush、LPop可實作 FIFO 工作佇列。Publish/Subscribe為即時訊息傳遞的典型應用。
2.3 使用 Redis 作為快取層
以下示範 Cache-Aside(先查快取,若未命中再查資料庫)的典型流程,適用於查詢頻繁、更新較少的資料。
func GetUserProfile(ctx context.Context, rdb *redis.Client, db *mongo.Collection, userID string) (*User, error) {
cacheKey := fmt.Sprintf("user:profile:%s", userID)
// 1. 嘗試從 Redis 讀取
if data, err := rdb.Get(ctx, cacheKey).Result(); err == nil {
var user User
if err := json.Unmarshal([]byte(data), &user); err == nil {
// 命中快取
return &user, nil
}
}
// 2. 快取未命中 → 從 MongoDB 讀取
var user User
filter := bson.M{"_id": userID}
if err := db.FindOne(ctx, filter).Decode(&user); err != nil {
return nil, err
}
// 3. 把結果寫回 Redis,設定過期時間(例如 5 分鐘)
if bytes, err := json.Marshal(user); err == nil {
rdb.Set(ctx, cacheKey, bytes, 5*time.Minute)
}
return &user, nil
}
關鍵:快取的 TTL(Time‑To‑Live) 必須根據資料變動頻率調整,避免「陳舊資料」問題。
常見陷阱與最佳實踐
| 類別 | 常見問題 | 解決方案 / 最佳實踐 |
|---|---|---|
| MongoDB | 忘記在 context 中設定 timeout,導致程式卡死。 |
使用 context.WithTimeout,並在每次 DB 操作前傳入。 |
| 大量寫入時未建立批次寫入(bulk)或交易,效能低下。 | 使用 mongo.Collection.BulkWrite 或 session.StartTransaction。 |
|
| 索引未同步,查詢仍走全表掃描。 | explain 查詢計畫,確保使用索引;定期檢查 db.collection.getIndexes()。 |
|
| Redis | 直接使用 Set 存放大量資料,導致記憶體飽和。 |
設定合理的 TTL,或使用 LRU/LFU 策略(maxmemory-policy)。 |
| Pub/Sub 訊息遺失(消費者斷線)。 | 考慮使用 Redis Streams(XADD、XREADGROUP)提供持久化與消費者組。 |
|
| 在高併發環境下忘記使用 pipeline,造成大量往返 RTT。 | 使用 client.Pipeline() 或 client.TxPipeline() 合併多個指令。 |
|
| 共通 | 直接把敏感連線資訊硬編碼在程式碼。 | 使用環境變數、Vault、或 Kubernetes Secret 管理機密。 |
| 忽略錯誤回傳,導致隱蔽的失敗。 | 所有 DB/Cache 呼叫務必檢查 err,並適當記錄或回傳。 |
實際應用場景
| 場景 | 為何選擇 MongoDB | 為何選擇 Redis |
|---|---|---|
| 使用者檔案 | 資料結構多變(可擴充的 JSON),需要支援複雜查詢與聚合。 | 讀取頻繁、更新較少,可使用快取提升讀取效能。 |
| 即時排行榜 | 需要聚合大量交易紀錄,使用 Aggregation Pipeline 計算分數。 | 有序集合(ZSET)提供 O(log N) 的排序與範圍查詢,適合即時排行榜。 |
| 工作佇列 | 任務資料較大、需要持久化,且可能需要失敗重試。 | 使用 List 或 Streams 作為輕量級佇列,結合 BRPOP 或 XREADGROUP。 |
| 會話管理 | 需要儲存使用者的多筆關聯資料(如購物車、偏好設定)。 | Session ID → Hash 結構,配合 TTL 自動過期。 |
| 即時聊天 | 訊息儲存與搜尋(關鍵字、時間範圍)適合 MongoDB。 | Pub/Sub 或 Streams 用於即時訊息推送。 |
總結
- MongoDB 提供彈性的文件模型與強大的聚合功能,適合儲存結構多變、需要複雜查詢的資料。使用 Go 時,務必搭配
context、適當的索引與批次寫入,以取得最佳效能。 - Redis 則是高速的鍵值快取與即時訊息平台,支援多種資料結構。透過
pipeline、TTL、以及適當的資料結構選擇(String、Hash、ZSET、Stream),可以大幅降低延遲、提升系統吞吐。
在實務開發中,兩者往往結合使用:MongoDB 作為主要的永續存儲,Redis 作為快取層或即時通訊管道。只要遵循上述的最佳實踐與注意常見陷阱,就能在 Golang 生態系中構建出高效、可擴展的 NoSQL 解決方案。祝開發順利,期待看到你把這些技巧運用到自己的專案中! 🚀