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

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 框架

GORM
The fantastic ORM library for Golang aims to be developer friendly.

在 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 官方支持的数据库类型有:MySQL, PostgreSQL, SQLite, SQL Server 和 TiDB MySQLimport ( “gorm.io/driver/mysql” “gorm.io/gorm”)func main() { // 参考 https://github.com/go-sql-driver/mysql

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)、CreatedAtUpdatedAtDeletedAt 四個常用字段,極大地方便了開發。
  • Struct Tag (gorm:"...") 用於配置字段如何映射到數據庫列。你可以指定數據類型 (type)、約束 (not null, unique)、索引 (index, uniqueIndex)、預設值 (default) 等。
  • UserID 字段被 GORM 自動識別為 User 表的外鍵,這是基於命名約定(關聯 Struct 名稱 + ID)。
  • 定義 Articles []ArticleUser User 字段則表明了 Struct 之間的關係,GORM 會在查詢時利用這些關係。

更多的範例可以參考:

模型定义
GORM 通过将 Go 结构体(Go structs) 映射到数据库表来简化数据库交互。 了解如何在GORM中定义模型,是充分利用GORM全部功能的基础。 模型定义模型是使用普通结构体定义的。 这些结构体可以包含具有基本Go类型、指针或这些类型的别名,甚至是自定义类型(只需要实现 database/sql 包中的Scanner和Valuer接口)。 考虑以下 user 模型的示例: type Us

數據庫操作:查詢 (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() 方法來插入單條或多條記錄。

创建
创建记录user := User{Name: “Jinzhu”, Age: 18, Birthday: time.Now()}result := db.Create(&user) // 通过数据的指针来创建user.ID // 返回插入数据的主键result.Error // 返回 errorresult.Row
// 插入單條數據
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() (查詢一條不排序的記錄) 以及各種條件查詢。

查询
检索单个对象GORM 提供了 First、Take、Last 方法,以便从数据库中检索单个对象。当查询数据库时它添加了 LIMIT 1 条件,且没有找到记录时,它会返回 ErrRecordNotFound 错误 // 获取第一条记录(主键升序)db.First(&user)// SELECT * FROM users ORDER BY id LIMIT 1;// 获取一条记录,没有指定排序
高级查询
智能选择字段在 GORM 中,您可以使用 Select 方法有效地选择特定字段。 这在Model字段较多但只需要其中部分的时候尤其有用,比如编写API响应。 type User struct { ID uint Name string Age int Gender string // 很多很多字段}type APIUser struct {
// 查詢單條數據
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() (更新多個字段)。

更新
保存所有字段Save 会保存所有的字段,即使字段是零值 db.First(&user)user.Name = “jinzhu 2”user.Age = 100db.Save(&user)// UPDATE users SET name=&#x27;jinzhu 2&#x27;, age=100, birthday=&#x27;2016-01-01&#x27;,
// 更新單個字段
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 字段,則執行物理刪除。

删除
删除一条记录删除一条记录时,删除对象需要指定主键,否则会触发 批量删除,例如: // Email 的 ID 是 `10`db.Delete(&email)// DELETE from emails where id = 10;// 带额外条件的删除db.Where(“name = ?”, “jinzhu”).Delete(&email)//
// 軟刪除單條數據
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) 關係。

实体关联
自动创建、更新GORM在创建或更新记录时会自动地保存其关联和引用,主要使用upsert技术来更新现有关联的外键引用。 在创建时自动保存关联当你创建一条新的记录时,GORM会自动保存它的关联数据。 这个过程包括向关联表插入数据以及维护外键引用。 user := User&#123; Name: “jinzhu”, BillingAddress: Add
  • 宣告 (Declare):在 Struct 中透過定義相關 Struct 類型的字段來宣告關聯。
  • 預載入 (Preload):在查詢主體數據的同時,自動加載其關聯數據,避免 N+1 查詢問題。

我們之前在 Struct 定義中已經宣告了 User 和 Article 之間的一對多/多對一關係:

type User struct {
	// ... 其他字段
	Articles []Article // 一個用戶有多篇文章 (一對多)
}

type Article struct {
	// ... 其他字段
	UserID uint // 外鍵
	User   User // 一篇文章屬於一個用戶 (多對一)
}
範例:關聯數據的創建與預載入
预加载
预加载示例GORM允许使用 Preload通过多个SQL中来直接加载关系, 例如: type User struct &#123; gorm.Model Username string Orders []Order&#125;type Order struct &#123; gorm.Model UserID uint Price float64&#125;// 查找 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)
DBResolver
DBResolver 为 GORM 提供了多个数据库支持,支持以下功能: 支持多个 sources、replicas 读写分离 根据工作表、struct 自动切换连接 手动切换连接 Sources/Replicas 负载均衡 适用于原生 SQL 事务 https://github.com/go-gorm/dbresolver 用法import ( “gorm.io/gor

至此,我們已經詳細介紹了 GORM 的基礎知識、CRUD 操作、數據關聯以及如何配置連接池和讀寫分離。GORM 的強大功能和靈活的 API 設計,使其成為 Go 語言開發者進行數據庫操作的首選框架之一。


Golang 記憶體快取:eko/gocache

GitHub - eko/gocache: ☔️ A complete Go cache library that brings you multiple ways of managing your caches
☔️ A complete Go cache library that brings you multiple ways of managing your caches - 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,所有的快取操作(SetGetDelete)都透過 cache.New[any](store) 返回的 cache.Cache[any] 實例來完成。這正是 gocache 最核心的優勢:統一的介面

這帶來了以下好處:

  • 易於切換快取後端:在開發初期,您可以使用簡單的記憶體快取進行開發和測試。當應用程式規模擴大,需要分佈式快取時,只需修改少量的配置程式碼(替換 store.NewGoCachestore.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 語言中常用的測試工具,包括:
  • Mock 工具: 學習如何使用 Mocking 技術隔離測試單元,提高測試效率,包括:

這些工具的結合,將幫助您在 Go 專案中實踐自動化測試、提升程式碼品質,並最終打造出更穩定、可靠的軟體產品。敬請期待!