Golang 大補帖:我那個「偶爾」會拿出來用的私藏工具與套件清單!(Part 3)

TL;DR
本篇文章將帶您深入了解 Go 語言應用程式中數據持久化與快取策略的實踐。我們將重點介紹 Go 社群中最受歡迎的 ORM (Object-Relational Mapping) 框架之一:GORM。透過 GORM,您將學會如何以 Go 語言的 Struct 定義數據模型,並透過簡潔的 API 輕鬆地執行數據庫的 CRUD (Create, Read, Update, Delete) 操作,告別繁瑣的 SQL 語句撰寫。
此外,我們也將探討如何利用輕量級的記憶體快取庫 gocache 來提升應用程式的性能。您將了解快取的基本概念、gocache 的使用方式,以及如何透過快取策略有效減少數據庫的負載,優化數據讀取速度。這兩者的結合,將幫助您構建出更高效、更具擴展性的 Go 語言應用程式。
GORM:Go 語言的強大 ORM 框架
在 Go 語言應用程式中處理數據庫操作時,通常有兩種主要方式:直接使用標準庫 database/sql
執行原生 SQL 語句,或者使用 ORM (Object-Relational Mapping) 框架。雖然原生 SQL 提供了最大的靈活性和性能控制,但在面對複雜的數據模型和大量的數據庫操作時,其程式碼會變得冗長且易於出錯。這時,ORM 框架的優勢便凸顯出來。
什麼是 ORM?
ORM (Object-Relational Mapping) 是一種程式設計技術,它用於實現物件導向程式語言裡的不同類型系統的數據之間的轉換。簡單來說,它將數據庫中的表和行映射成程式碼中的物件 (Struct) 和物件實例。開發者無需直接撰寫 SQL 語句,而是透過操作程式碼中的物件來間接實現對數據庫的增、刪、查、改 (CRUD) 操作。ORM 的主要優點包括:
- 提高開發效率:自動化 SQL 語句的生成,減少手動撰寫和調試 SQL 的時間。
- 降低錯誤率:將 SQL 語句的錯誤轉移到編譯時檢查 (例如 Struct 字段類型不匹配),而不是運行時。
- 提高程式碼可讀性與可維護性:以物件導向的方式操作數據庫,使業務邏輯更清晰。
- 數據庫遷移更方便:在某些情況下,切換數據庫類型(例如從 MySQL 換到 PostgreSQL)可能只需要修改配置和驅動,而無需大量修改業務邏輯程式碼。
GORM 支援的數據庫
GORM 是一個功能豐富、使用者友好的 Go 語言 ORM 庫,它支援多種主流的關係型數據庫。這使得 GORM 在不同的專案和部署環境中都具有很高的靈活性。目前,GORM 官方支援的數據庫包括:
- MySQL
- PostgreSQL
- SQLite
- SQL Server
- TiDB
- Clickhouse
此外,透過社區貢獻的驅動,GORM 也可能支援其他數據庫。
如何用 Struct 定義表結構
在 GORM 中,數據庫中的「表」會被映射為 Go 語言中的「Struct」。你只需要定義一個 Go Struct,GORM 就會根據其字段資訊自動推斷出數據庫表的結構。你可以使用 Struct Tag 來進一步定義字段的行為,例如指定列名、數據類型、約束、預設值、是否為主鍵等。
範例:定義用戶 (User) 和文章 (Article) 表結構
package main
import (
"fmt"
"time"
"gorm.io/gorm"
)
// User 定義用戶表結構
// GORM 會自動將 Struct 名稱的複數形式作為表名,例如 User -> users
type User struct {
// GORM 內建的 gorm.Model 包含了常用的字段:ID (主鍵), CreatedAt, UpdatedAt, DeletedAt (軟刪除)
gorm.Model
Name string `gorm:"type:varchar(100);not null;uniqueIndex:idx_name"` // 設定字段類型、非空、唯一索引
Email string `gorm:"type:varchar(100);unique;not null"` // 設定唯一約束
Age uint `gorm:"default:18"` // 設定預設值
Birthday *time.Time // 指針類型可以存儲 NULL
Active bool `gorm:"default:true"`
// 一對多關聯:一個用戶可以有多篇文章
// GORM 會自動處理關聯,但需要手動定義 Slice 字段
Articles []Article
}
// Article 定義文章表結構
type Article struct {
ID uint `gorm:"primaryKey"` // 明確指定主鍵
Title string `gorm:"type:varchar(255);not null"`
Content string `gorm:"type:text"`
UserID uint // GORM 會自動識別為 User 的外鍵
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"` // 軟刪除字段,會自動添加索引
// 多對一關聯:一篇文章屬於一個用戶
User User // 關聯到 User Struct
}
// TableName 方法 (可選): 如果你不想讓 GORM 使用預設的複數表名,可以自行指定
func (User) TableName() string {
return "my_users" // 將 User Struct 映射到 my_users 表
}
func main() {
// 這裡僅定義 Struct,實際的數據庫連接和遷移會在後續章節說明
fmt.Println("User and Article structs defined for GORM.")
}
在上述範例中:
gorm.Model
是一個內嵌 Struct,它包含了ID
(Primary Key)、CreatedAt
、UpdatedAt
、DeletedAt
四個常用字段,極大地方便了開發。- Struct Tag (
gorm:"..."
) 用於配置字段如何映射到數據庫列。你可以指定數據類型 (type
)、約束 (not null
,unique
)、索引 (index
,uniqueIndex
)、預設值 (default
) 等。 UserID
字段被 GORM 自動識別為User
表的外鍵,這是基於命名約定(關聯 Struct 名稱 + ID
)。- 定義
Articles []Article
和User User
字段則表明了 Struct 之間的關係,GORM 會在查詢時利用這些關係。
更多的範例可以參考:
數據庫操作:查詢 (Retrieve)、插入 (Create)、更新 (Update)、刪除 (Delete)
GORM 提供了直觀且強大的鏈式 API 來執行數據庫的 CRUD 操作。以下我們將透過程式碼範例來展示這些基本操作。
在開始 CRUD 操作之前,我們需要先建立數據庫連接並進行自動遷移 (Auto Migrate),這會根據你的 Struct 定義自動創建或更新數據庫表。
範例:連接數據庫與自動遷移
package main
import (
"fmt"
"log"
"os"
"time"
"gorm.io/driver/sqlite" // 導入 SQLite 驅動,你也可以替換為 mysql、postgres 等
"gorm.io/gorm"
"gorm.io/gorm/logger" // 導入 GORM 日誌功能
)
// User 定義用戶表結構
// GORM 會自動將 Struct 名稱的複數形式作為表名,例如 User -> users
type User struct {
// GORM 內建的 gorm.Model 包含了常用的字段:ID (主鍵), CreatedAt, UpdatedAt, DeletedAt (軟刪除)
gorm.Model
Name string `gorm:"type:varchar(100);not null;uniqueIndex:idx_name"` // 設定字段類型、非空、唯一索引
Email string `gorm:"type:varchar(100);unique;not null"` // 設定唯一約束
Age uint `gorm:"default:18"` // 設定預設值
Birthday *time.Time // 指針類型可以存儲 NULL
Active bool `gorm:"default:true"`
// 一對多關聯:一個用戶可以有多篇文章
// GORM 會自動處理關聯,但需要手動定義 Slice 字段
Articles []Article
}
// Article 定義文章表結構
type Article struct {
ID uint `gorm:"primaryKey"` // 明確指定主鍵
Title string `gorm:"type:varchar(255);not null"`
Content string `gorm:"type:text"`
UserID uint // GORM 會自動識別為 User 的外鍵
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"` // 軟刪除字段,會自動添加索引
// 多對一關聯:一篇文章屬於一個用戶
User User // 關聯到 User Struct
}
// TableName 方法 (可選): 如果你不想讓 GORM 使用預設的複數表名,可以自行指定
func (User) TableName() string {
return "my_users" // 將 User Struct 映射到 my_users 表
}
var DB *gorm.DB
func init() {
// 設定 GORM 日誌級別 (可選,用於調試)
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Info, // Log level: Silent, Error, Warn, Info
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
ParameterizedQueries: true, // Don't include params in the SQL log
Colorful: true, // Disable color
},
)
// 連接 SQLite 數據庫
// 對於其他數據庫,這裡需要替換為相應的驅動和連接字符串
// MySQL: dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
// PostgreSQL: dsn := "host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai"
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
Logger: newLogger, // 設置日誌
})
if err != nil {
log.Fatalf("無法連接到數據庫: %v", err)
}
// 自動遷移數據庫表 (如果表不存在則創建,如果字段有變化則更新)
if err := db.AutoMigrate(&User{}, &Article{}); err != nil {
log.Fatalf("數據庫遷移失敗: %v", err)
}
DB = db // 將數據庫實例賦值給全局變量
fmt.Println("數據庫連接成功並完成自動遷移!")
}
// 主函數僅作測試用
func main() {
// 所有 CRUD 範例都在 init() 之後執行
// 接下來的章節會詳細介紹這些操作
}
把程式跑起來後,可以看到 GORM 輸出的日誌裡,會自動生成資料庫建立表以及相關的 SQL並執行。

範例:插入 (Create) 數據
GORM 提供了 Create()
方法來插入單條或多條記錄。
// 插入單條數據
user1 := User{Name: "Alice", Email: "[email protected]", Age: 25}
result := DB.Create(&user1) // 傳入 Struct 指針
if result.Error != nil {
fmt.Printf("插入用戶失敗: %v\n", result.Error)
} else {
fmt.Printf("插入用戶成功,ID: %d, 影響行數: %d\n", user1.ID, result.RowsAffected)
}
// 插入多條數據 (批量插入)
users := []User{
{Name: "Bob", Email: "[email protected]", Age: 30},
{Name: "Charlie", Email: "[email protected]", Age: 28},
}
result = DB.Create(&users) // 傳入 Struct Slice 指針
if result.Error != nil {
fmt.Printf("批量插入用戶失敗: %v\n", result.Error)
} else {
fmt.Printf("批量插入用戶成功,影響行數: %d\n", result.RowsAffected)
for _, u := range users {
fmt.Printf(" 插入用戶 ID: %d\n", u.ID)
}
}
將以上的程式碼複製到 main 函數內執行後,可以觀察到 GORM 一樣自動生成對應數據插入 SQL 並自動執行。

範例:查詢數據
GORM 提供了多種查詢方法,包括 First()
(查詢第一條)、Last()
(查詢最後一條)、Find()
(查詢多條)、Take()
(查詢一條不排序的記錄) 以及各種條件查詢。
// 查詢單條數據
var user User
// First() 按照主鍵排序後取第一條
result = DB.First(&user, user1.ID) // 根據主鍵 ID 查詢
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
fmt.Println("未找到用戶")
} else {
fmt.Printf("查詢用戶失敗: %v\n", result.Error)
}
} else {
fmt.Printf("查詢到用戶: %+v\n", user)
}
// 根據條件查詢
var oldUser User
DB.Where("age > ?", 20).First(&oldUser)
fmt.Printf("查詢到年齡大於 20 的用戶: %+v\n", oldUser)
// 查詢多條數據
users = []User{}
DB.Where("age >= ?", 28).Find(&users)
fmt.Printf("查詢到年齡大於等於 28 的用戶數量: %d\n", len(users))
for _, u := range users {
fmt.Printf(" - %+v\n", u)
}
// Select 指定查詢字段
var names []string
DB.Model(&User{}).Select("name").Where("age < ?", 30).Scan(&names) // Scan 到切片
fmt.Printf("查詢到年齡小於 30 的用戶名稱: %v\n", names)
// 排序和限制
var limitedUsers []User
DB.Order("age desc").Limit(2).Find(&limitedUsers)
fmt.Printf("按年齡倒序排列前兩位用戶: %+v\n", limitedUsers)
執行結果,可以觀察 GORM 生成的 SQL 以及附帶的查詢條件。

範例:更新 (Update) 數據
GORM 提供了多種更新方法,包括 Save()
(更新所有字段,如果主鍵存在則更新,不存在則創建)、Update()
(更新單個字段)、Updates()
(更新多個字段)。
// 更新單個字段
var userToUpdate User
DB.First(&userToUpdate, user1.ID)
DB.Model(&userToUpdate).Update("Age", 26) // 更新 Age 字段
fmt.Printf("更新後用戶 %d 的年齡: %d\n", userToUpdate.ID, userToUpdate.Age)
// 更新多個字段
DB.Model(&userToUpdate).Updates(User{Name: "Alice Smith", Email: "[email protected]"}) // 使用 Struct 更新
// 或者使用 Map 更新
DB.Model(&userToUpdate).Updates(map[string]interface{}{"Age": 27, "Active": false})
fmt.Printf("更新後用戶 %d 的信息: %+v\n", userToUpdate.ID, userToUpdate)
// 批量更新
// 將所有年齡大於 25 的用戶的 Active 狀態設為 false
result = DB.Model(&User{}).Where("age > ?", 25).Update("Active", false)
fmt.Printf("批量更新影響行數: %d\n", result.RowsAffected)

範例:刪除 (Delete) 數據
GORM 支援兩種刪除方式:物理刪除和軟刪除。如果 Struct 中包含 gorm.DeletedAt
字段,GORM 預設會執行軟刪除(將 deleted_at
字段設置為當前時間,而不是真正從數據庫中刪除記錄)。如果沒有 gorm.DeletedAt
字段,則執行物理刪除。
// 軟刪除單條數據
var userToDelete User
DB.First(&userToDelete, user1.ID)
DB.Delete(&userToDelete) // 執行軟刪除,將 deleted_at 設置為當前時間
fmt.Printf("用戶 %d 已被軟刪除。\n", userToDelete.ID)
// 查詢被軟刪除的數據
var deletedUser User
DB.Unscoped().First(&deletedUser, user1.ID) // 使用 Unscoped 才能查詢到軟刪除的數據
fmt.Printf("查詢到已軟刪除的用戶: %+v\n", deletedUser)
// 物理刪除單條數據
// 假設我們有一個沒有 DeletedAt 字段的 Article 表
// 或者明確使用 DB.Unscoped().Delete(&userToDelete) 來進行物理刪除已軟刪除的數據
// 這裡我們直接示範一個新的 Article
article1 := Article{Title: "Golang Basics", Content: "...", UserID: user1.ID}
DB.Create(&article1)
fmt.Printf("插入文章 ID: %d\n", article1.ID)
DB.Unscoped().Delete(&article1) // 物理刪除 article1
fmt.Printf("文章 %d 已被物理刪除。\n", article1.ID)
// 批量刪除
// 軟刪除所有年齡小於 30 且 Active 為 true 的用戶
result = DB.Where("age < ? AND active = ?", 30, true).Delete(&User{})
fmt.Printf("批量軟刪除影響行數: %d\n", result.RowsAffected)

數據的關聯 (Associations)
GORM 提供了強大的關聯功能,能夠輕鬆處理數據庫表之間的關係,例如一對一 (One-to-One)、一對多 (One-to-Many)、多對一 (Many-to-One) 和多對多 (Many-to-Many) 關係。
- 宣告 (Declare):在 Struct 中透過定義相關 Struct 類型的字段來宣告關聯。
- 預載入 (Preload):在查詢主體數據的同時,自動加載其關聯數據,避免 N+1 查詢問題。
我們之前在 Struct 定義中已經宣告了 User 和 Article 之間的一對多/多對一關係:
type User struct {
// ... 其他字段
Articles []Article // 一個用戶有多篇文章 (一對多)
}
type Article struct {
// ... 其他字段
UserID uint // 外鍵
User User // 一篇文章屬於一個用戶 (多對一)
}
範例:關聯數據的創建與預載入
// 創建帶有關聯的數據
userWithArticles := User{
Name: "David",
Email: "[email protected]",
Age: 40,
Articles: []Article{ // 在創建用戶時同時創建關聯的文章
{Title: "GORM Advanced", Content: "...", CreatedAt: time.Now(), UpdatedAt: time.Now()},
{Title: "Concurrency in Go", Content: "...", CreatedAt: time.Now(), UpdatedAt: time.Now()},
},
}
DB.Create(&userWithArticles)
fmt.Printf("創建帶文章的用戶: %+v\n", userWithArticles.Name)
for _, article := range userWithArticles.Articles {
fmt.Printf(" - 文章: %s (ID: %d)\n", article.Title, article.ID)
}
// 預載入 (Preload) 關聯數據
// 查詢用戶時同時加載他們的文章
var usersWithArticles []User
DB.Preload("Articles").Find(&usersWithArticles) // Preload "Articles" 字段
fmt.Println("\n查詢所有用戶及其文章:")
for _, user := range usersWithArticles {
fmt.Printf("用戶: %s (ID: %d)\n", user.Name, user.ID)
for _, article := range user.Articles {
fmt.Printf(" - 文章: %s (ID: %d)\n", article.Title, article.ID)
}
}
// 預載入指定條件的關聯數據
var specificUser User
DB.Where("name = ?", "David").Preload("Articles", "title LIKE ?", "%GORM%").First(&specificUser)
fmt.Printf("\n查詢用戶 'David' 及標題包含 'GORM' 的文章:\n")
fmt.Printf("用戶: %s\n", specificUser.Name)
for _, article := range specificUser.Articles {
fmt.Printf(" - 文章: %s\n", article.Title)
}
// 查詢文章時預載入用戶數據
var articlesWithUser []Article
DB.Preload("User").Find(&articlesWithUser)
fmt.Println("\n查詢所有文章及其作者:")
for _, article := range articlesWithUser {
fmt.Printf("文章: %s, 作者: %s\n", article.Title, article.User.Name)
}

Preload()
方法是解決 N+1 查詢問題的關鍵,它會一次性載入所有關聯的數據,而不是為每條主體數據單獨發送一個查詢。
讀寫分離 (Read/Write Splitting) 與連接池 (Connection Pool)
在大型應用程式中,數據庫的讀寫分離和連接池是優化性能和高可用性的重要手段。GORM 雖然本身不是一個數據庫連接池管理庫,但它基於 Go 標準庫的 database/sql
,因此可以充分利用 database/sql
提供的連接池管理能力。同時,GORM 也支援讀寫分離的配置。
範例:連接池配置
GORM 允許你透過 gorm.Config
和底層的 sql.DB
實例來配置連接池的行為,例如最大連接數、最大空閒連接數、連接生命週期等。
package main
import (
"fmt"
"log"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// User 和 Article Struct 定義同上
var DB *gorm.DB
func init() {
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: time.Second,
LogLevel: logger.Info,
Colorful: true,
},
)
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
Logger: newLogger,
})
if err != nil {
log.Fatalf("無法連接到數據庫: %v", err)
}
// 獲取底層的 *sql.DB 實例
sqlDB, err := db.DB()
if err != nil {
log.Fatalf("無法獲取底層數據庫實例: %v", err)
}
// 配置連接池
sqlDB.SetMaxIdleConns(10) // 設定最大空閒連接數
sqlDB.SetMaxOpenConns(100) // 設定數據庫的最大開啟連接數
sqlDB.SetConnMaxLifetime(time.Hour) // 設定連接可重用的最長時間
DB = db
fmt.Println("數據庫連接成功並完成自動遷移!連接池已配置。")
}
func main() {
// ... CRUD 操作等
}
合理配置連接池參數對於應用程式的性能至關重要。SetMaxIdleConns
控制空閒連接的數量,過低會導致連接頻繁創建和關閉,過高則可能佔用過多數據庫資源。SetMaxOpenConns
限制最大連接數,防止數據庫被過度負載。SetConnMaxLifetime
定期回收連接,有助於防止連接老化問題。
範例:讀寫分離 (Read/Write Splitting)
讀寫分離是一種常見的數據庫優化策略,它將讀操作分發到一個或多個讀取副本 (Read Replica),而寫操作則發送到主數據庫 (Master)。這可以提高數據庫的讀取吞吐量和可用性。GORM 透過 DB.Use(gorm.ReadWriteReplica())
插件來支援讀寫分離。
import (
"gorm.io/gorm"
"gorm.io/plugin/dbresolver"
"gorm.io/driver/mysql"
)
db, err := gorm.Open(mysql.Open("db1_dsn"), &gorm.Config{})
db.Use(dbresolver.Register(dbresolver.Config{
// use `db2` as sources, `db3`, `db4` as replicas
Sources: []gorm.Dialector{mysql.Open("db2_dsn")},
Replicas: []gorm.Dialector{mysql.Open("db3_dsn"), mysql.Open("db4_dsn")},
// sources/replicas load balancing policy
Policy: dbresolver.RandomPolicy{},
// print sources/replicas mode in logger
TraceResolverMode: true,
}).Register(dbresolver.Config{
// use `db1` as sources (DB's default connection), `db5` as replicas for `User`, `Address`
Replicas: []gorm.Dialector{mysql.Open("db5_dsn")},
}, &User{}, &Address{}).Register(dbresolver.Config{
// use `db6`, `db7` as sources, `db8` as replicas for `orders`, `Product`
Sources: []gorm.Dialector{mysql.Open("db6_dsn"), mysql.Open("db7_dsn")},
Replicas: []gorm.Dialector{mysql.Open("db8_dsn")},
}, "orders", &Product{}, "secondary"))
// 使用 Write 模式:从 sources db `db1` 读取 user
db.Clauses(dbresolver.Write).First(&user)
// 指定 Resolver:从 `secondary` 的 replicas db `db8` 读取 user
db.Clauses(dbresolver.Use("secondary")).First(&user)
// 指定 Resolver 和 Write 模式:从 `secondary` 的 sources db `db6` 或 `db7` 读取 user
db.Clauses(dbresolver.Use("secondary"), dbresolver.Write).First(&user)
至此,我們已經詳細介紹了 GORM 的基礎知識、CRUD 操作、數據關聯以及如何配置連接池和讀寫分離。GORM 的強大功能和靈活的 API 設計,使其成為 Go 語言開發者進行數據庫操作的首選框架之一。
Golang 記憶體快取:eko/gocache
在處理數據庫或其他慢速數據源時,快取 (Cache) 是一個不可或缺的優化手段。它透過將經常訪問的數據儲存在快速訪問的存儲介質(如記憶體或專用快取服務器)中,來減少對原始數據源的請求次數,從而顯著提升應用程式的響應速度和數據庫的負載能力。
eko/gocache
簡介
eko/gocache
是一個 Go 語言的統一快取庫,其核心優勢在於提供了一個通用的快取介面 (Unified Interface)。這意味著無論你底層使用的是什麼快取實現(例如記憶體、Redis、Memcached 等),你都可以使用相同的 gocache
API 來進行快取操作,無需因為切換底層存儲而修改你的業務邏輯程式碼。這極大地提高了程式碼的可維護性和靈活性。
gocache
不僅提供了通用的快取介面,還支援以下功能:
- 多種快取實作:開箱即用支援多種主流的快取解決方案。
- 靈活的快取策略:支援 TTL (Time To Live)、淘汰策略等。
- 內建 Prometheus Metrics 匯出:方便監控快取的運行狀況。
- 快取層疊 (Chaining):可以將多個快取實作層疊起來,例如先從記憶體快取讀取,如果沒有再讀取 Redis。
gocache
支援的快取實作
gocache
透過不同的「儲存 (Store)」實現來支援多種快取後端。以下是一些常見的儲存實作及其程式碼範例:
範例:記憶體快取 (Go-Cache)
這是最簡單也是最快的快取形式,數據直接儲存在應用程式的記憶體中。gocache
使用 patrickmn/go-cache
作為其內建的記憶體儲存。
package main
import (
"context"
"fmt"
"time"
"github.com/eko/gocache/lib/v4/cache"
gocache_store "github.com/eko/gocache/store/go_cache/v4"
gocache "github.com/patrickmn/go-cache"
)
func main() {
ctx := context.Background()
// 建立 go-cache 客戶端
// 參數:預設過期時間 5 分鐘,清理間隔 10 分鐘
gocacheClient := gocache.New(5*time.Minute, 10*time.Minute)
// 使用 go-cache 客戶端建立 gocache store
gocacheStore := gocache_store.NewGoCache(gocacheClient)
// 建立快取管理器,指定儲存的資料型別為 []byte
cacheManager := cache.New[[]byte](gocacheStore)
// 設定快取鍵值對:鍵為 "my-key",值為 "my-value" 的位元組陣列
err := cacheManager.Set(ctx, "my-key", []byte("my-value"))
if err != nil {
panic(err)
}
// 從快取中取得值
value, err := cacheManager.Get(ctx, "my-key")
if err != nil {
panic(err)
}
// 印出取得的值
fmt.Printf("%s", value)
}
範例:Redis 快取
Redis 是一個高性能的鍵值對數據庫,常被用作分佈式快取。gocache
透過 github.com/redis/go-redis
庫來實現 Redis 儲存。
package main
import (
"context"
"fmt"
"time"
"github.com/eko/gocache/lib/v4/cache"
"github.com/eko/gocache/lib/v4/store"
redis_store "github.com/eko/gocache/store/redis/v4"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
// 建立 Redis 存儲實例
// 連接到本地 Redis 服務器 (127.0.0.1:6379)
redisStore := redis_store.NewRedis(redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379", // Redis 服務器地址
}))
// 建立快取管理器,指定儲存的值類型為 string
cacheManager := cache.New[string](redisStore)
// 設定快取值
// 鍵:"my-key",值:"my-value",過期時間:15秒
err := cacheManager.Set(ctx, "my-key", "my-value", store.WithExpiration(15*time.Second))
if err != nil {
// 如果設定失敗,程式將會 panic
panic(err)
}
// 從快取中取得值
value, err := cacheManager.Get(ctx, "my-key")
// 根據不同的錯誤情況進行處理
switch err {
case nil:
// 成功取得值的情況
fmt.Printf("Get the key '%s' from the redis cache. Result: %s", "my-key", value)
case redis.Nil:
// 鍵不存在的情況(可能已過期或從未設定)
fmt.Printf("Failed to find the key '%s' from the redis cache.", "my-key")
default:
// 其他錯誤情況(如網路錯誤、連接問題等)
fmt.Printf("Failed to get the value from the redis cache with key '%s': %v", "my-key", err)
}
}
範例:Memcache 快取
Memcached 是一個高性能的分佈式記憶體對象快取系統。gocache 是透過 github.com/bradfitz/gomemcache
來實現連線。
package main
import (
"context"
"fmt"
"time"
"github.com/bradfitz/gomemcache/memcache"
"github.com/eko/gocache/lib/v4/cache"
"github.com/eko/gocache/lib/v4/store"
memcache_store "github.com/eko/gocache/store/memcache/v4"
)
func main() {
ctx := context.Background()
// 建立 Memcache 儲存實例
// 連接到本地的 Memcache 服務器 (localhost:11211)
// 設定預設過期時間為 10 秒
memcacheStore := memcache_store.NewMemcache(
memcache.New("localhost:11211"),
store.WithExpiration(10*time.Second),
)
// 建立快取管理器,指定資料型別為 []byte
cacheManager := cache.New[[]byte](memcacheStore)
// 設定快取值
// 鍵值:my-key
// 數值:my-value (轉換為 byte 陣列)
// 過期時間:15 秒 (覆蓋預設的 10 秒)
err := cacheManager.Set(ctx, "my-key", []byte("my-value"),
store.WithExpiration(15*time.Second),
)
if err != nil {
panic(err)
}
// 從快取中獲取資料
value, err := cacheManager.Get(ctx, "my-key")
if err != nil {
panic(err)
}
// 將 byte 陣列轉換為字串並輸出
fmt.Println(string(value))
// 刪除指定的快取項目
cacheManager.Delete(ctx, "my-key")
// 清空所有快取項目
cacheManager.Clear(ctx)
}
統一的介面 (Unified Interface) 優勢
從上述範例中,您會發現無論底層使用的是記憶體、Redis、Redis Cluster、Memcache 還是 Ristretto,所有的快取操作(Set
、Get
、Delete
)都透過 cache.New[any](store)
返回的 cache.Cache[any]
實例來完成。這正是 gocache
最核心的優勢:統一的介面。
這帶來了以下好處:
- 易於切換快取後端:在開發初期,您可以使用簡單的記憶體快取進行開發和測試。當應用程式規模擴大,需要分佈式快取時,只需修改少量的配置程式碼(替換
store.NewGoCache
為store.NewRedis
或其他),而無需改動核心業務邏輯中與快取互動的程式碼。這大大降低了系統重構的成本。 - 提高程式碼可讀性和可維護性:開發者無需關心底層快取實作的具體細節,只需專注於如何使用
gocache
的統一 API。 - 促進測試:在單元測試或集成測試中,可以輕鬆地替換為記憶體快取或其他模擬實現,提高測試的效率和隔離性。
- 未來擴展性:如果未來出現新的快取技術,
gocache
只需要實現新的store
介面,您的應用程式就可以無縫切換過去。
內建支援 Prometheus Metrics
對於任何生產環境的應用程式來說,監控是不可或缺的一部分。gocache
開箱即用支援匯出 Prometheus 指標,這使得監控快取的運行狀況變得非常方便。你可以監控快取的命中率 (Hit Rate)、錯失率 (Miss Rate)、操作總數、錯誤數等關鍵指標。
要啟用 Prometheus 指標,你需要引入 github.com/eko/gocache/lib/v4/metrics
包,並將其與你的 gocache
實例連接起來。
package main
import (
"context"
"fmt"
"net/http"
"time"
"github.com/eko/gocache/lib/v4/cache"
"github.com/eko/gocache/lib/v4/metrics"
gocache_store "github.com/eko/gocache/store/go_cache/v4"
gocache "github.com/patrickmn/go-cache"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
ctx := context.Background()
// 建立 go-cache 客戶端
// 參數:預設過期時間 5 分鐘,清理間隔 10 分鐘
gocacheClient := gocache.New(5*time.Minute, 10*time.Minute)
// 使用 go-cache 客戶端建立 gocache store
gocacheStore := gocache_store.NewGoCache(gocacheClient)
promMetrics := metrics.NewPrometheus("my-test-app")
// 建立快取管理器,指定儲存的資料型別為 []byte
cacheManager := cache.NewMetric[[]byte](
promMetrics,
cache.New[[]byte](gocacheStore),
)
// 設定快取鍵值對:鍵為 "my-key",值為 "my-value" 的位元組陣列
err := cacheManager.Set(ctx, "my-key", []byte("my-value"))
if err != nil {
panic(err)
}
// 從快取中取得值
value, err := cacheManager.Get(ctx, "my-key")
if err != nil {
panic(err)
}
// 印出取得的值
fmt.Printf("%s", value)
// 6. 啟動一個 HTTP 服務器來暴露 Prometheus metrics 接口
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":2112", nil)
}
運行上述程式碼後,你可以訪問 http://localhost:2112/metrics
,會看到類似以下的 Prometheus 指標輸出:
# HELP cache_collector This represent the number of items in cache
# TYPE cache_collector gauge
cache_collector{metric="delete_error",service="my-test-app",store="go-cache"} 0
cache_collector{metric="delete_success",service="my-test-app",store="go-cache"} 0
cache_collector{metric="hit_count",service="my-test-app",store="go-cache"} 1
cache_collector{metric="invalidate_error",service="my-test-app",store="go-cache"} 0
cache_collector{metric="invalidate_success",service="my-test-app",store="go-cache"} 0
cache_collector{metric="miss_count",service="my-test-app",store="go-cache"} 0
cache_collector{metric="set_error",service="my-test-app",store="go-cache"} 0
cache_collector{metric="set_success",service="my-test-app",store="go-cache"} 1
這些指標數據可以被 Prometheus 伺服器抓取,並在 Grafana 等工具中進行可視化,幫助你全面了解快取的運行狀況,及早發現潛在的性能問題。
總結來說,eko/gocache
是一個非常實用且強大的 Go 語言快取庫。它透過統一的介面、多種底層儲存支援和內建的監控功能,為 Go 應用程式的快取管理提供了高效且靈活的解決方案。
下集待續
在未來的文章中,我們將繼續深入 Go 語言的開發世界,為您介紹更多打造高品質軟體的關鍵工具與技術。我們將會探討:
- 依賴注入 (Dependency Injection) 框架: 如何使用uber-go/fx來管理應用程式的組件依賴,讓您的程式碼更具模組化、可測試性及可維護性。
- 測試框架與工具: 我們會介紹 Go 語言中常用的測試工具,包括:
- stretchr/testify:提供豐富的斷言和 Mocking 功能,簡化測試撰寫。
- smartystreets/goconvey:提供語義化的測試語法和互動式網頁報告。
- onsi/ginkgo:一個行為驅動開發 (BDD) 測試框架。
- Mock 工具: 學習如何使用 Mocking 技術隔離測試單元,提高測試效率,包括:
- jarcoal/httpmock:用於模擬 HTTP 請求與回應。
- DATA-DOG/go-sqlmock:用於模擬 SQL 數據庫操作。
- uber-go/mock:一個強大的 Mock 代碼生成工具。
這些工具的結合,將幫助您在 Go 專案中實踐自動化測試、提升程式碼品質,並最終打造出更穩定、可靠的軟體產品。敬請期待!