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

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

TL;DR

我又來了各位,在我現在的工作裡,最常寫的就是 Golang。所以,這篇文章想跟大家分享一下,我在日常專案中覺得超好用、能讓我快速上手,而且省下很多重複寫基礎功能時間的實用套件開發工具

本系列文章投影片
Amazingly Simple Graphic Design Software – Canva
Amazingly Simple Graphic Design Software – Canva

學習資源

學習 Golang,建議從以下資源循序漸進地打下基礎;對於進階開發者,也有豐富的資源可以持續精進。

基本入門

進階學習與實踐

  • Effective Go
    這份官方文檔提供了編寫清晰、符合 Go 慣用語法程式碼的技巧。它深入探討 Go 語言的設計理念和最佳實踐,是每個 Go 開發者都應仔細閱讀的進階指南。例如 GitHub 上的《Effective Go》中英雙語版是不錯的參考。
  • Common Mistakes
    理解常見程式錯誤是提升開發品質的關鍵。這類資源會列出 Go 程式設計中容易犯的錯誤,例如錯誤處理不當、Goroutine 的使用陷阱或切片 (slice) 的誤用等,幫助你避免重蹈覆轍。
  • uber-go/guide 的繁體中文翻譯
    Uber 的 Go 語言編碼規範,為大型專案提供了詳細的撰寫準則,涵蓋命名、錯誤處理、併發模式等,有助於提升程式碼品質和團隊協作效率。
  • Go standard library documentation (Go 標準庫文檔)
    深入理解 Go 語言的強大之處在於其豐富且穩定的標準庫。這是查詢每個 API 具體使用方式的必備手冊,無論是處理檔案、網路通訊還是資料結構,標準庫都能提供基礎且高效的解決方案。
  • Go Patterns / Design Patterns in Go
    雖然 Go 語言的設計哲學鼓勵簡潔,但瞭解設計模式仍能幫助你解決複雜的軟體設計問題。這些資源會介紹如何在 Go 語言中應用常見的設計模式。
    警語:不要設計模式上癮,設計模式設計太多就會變成設計同事。
  • Awesome Go
    一個精心整理的 Go 語言資源集合,包含各種高品質的 Go 語言工具、框架、函式庫、軟體和教學。無論是尋找特定工具還是探索 Go 生態系統的廣度,這都是一個很好的起點。

組態管理

在專案開發中,良好的組態管理至關重要。Golang 生態系提供多種工具來簡化這個過程。

go-envconfig

GitHub - sethvargo/go-envconfig: A Go library for parsing struct tags from environment variables.
A Go library for parsing struct tags from environment variables. - sethvargo/go-envconfig

如果你主要依賴環境變數來管理組態,go-envconfig 是一個輕量級且實用的選擇,它能自動將環境變數對應到 Go Struct。

範例:Struct 映射
package main

import (
	"context"
	"log"

	"github.com/sethvargo/go-envconfig"
)

// 定義一個 struct,使用 `env` tag 來指定對應的環境變數名稱
type MyConfig struct {
	Port string `env:"PORT"`
	User string `env:"USER"`
}

func main() {
	ctx := context.Background()

	var c MyConfig // 宣告設定 struct

	if err := envconfig.Process(ctx, &c); err != nil {
		log.Fatal(err) // 處理錯誤
	}

	// 假設已設定環境變數:
	// export PORT=5555
	// export USER=yoyo
	// 此時 c.Port 為 "5555",c.User 為 "yoyo"
	log.Printf("Port: %s, User: %s", c.Port, c.User)
}
範例:如何設定預設值

當環境變數未設定時,你可以使用 default tag 為 struct 欄位提供預設值。

package main

import (
	"context"
	"log"
	"time"
	"github.com/sethvargo/go-envconfig"
)

type AppConfigWithDefaults struct {
	// 如果 APP_PORT 環境變數未設定,預設為 "8080"
	Port     string        `env:"APP_PORT,default=8080"`
	// 如果 LOG_LEVEL 未設定,預設為 "info"
	LogLevel string        `env:"LOG_LEVEL,default=info"`
	// 可設定時間格式的預設值,例如預設超時時間為 10 秒
	Timeout  time.Duration `env:"TIMEOUT,default=10s"`
	// 預設值可引用其他環境變數,例如 DB_USER 未設定時,預設為當前使用者
	DBUser   string        `env:"DB_USER,default=$USER"`
	// 注意:如果預設值包含 "$",需要使用 "\\" 進行跳脫,例如 `env:"MONEY,default=\\\\$100"`
}

func main() {
	ctx := context.Background()
	var config AppConfigWithDefaults

	if err := envconfig.Process(ctx, &config); err != nil {
		log.Fatalf("載入設定失敗: %v", err)
	}

	log.Printf("應用程式連接埠: %s", config.Port)
	log.Printf("日誌等級: %s", config.LogLevel)
	log.Printf("超時時間: %s", config.Timeout)
	log.Printf("資料庫使用者: %s", config.DBUser)

	// 測試:
	// 不設定任何環境變數,將使用所有預設值。
	// 若設定 export APP_PORT=9000,則 Port 將為 "9000"。
}
範例:如何檢查格式以及是否必填、格式檢查 (透過自定義解碼器)

go-envconfig 支援標記必填欄位,並可透過 Go 的型別系統處理基礎格式檢查。對於複雜的格式驗證,你可以實現 envconfig.Decoder 介面。
go-envconfig 會自動轉換 Go 的基本型別。若需更複雜的格式驗證(如 IP 地址、URL 等),可以實現 envconfig.Decoder 介面。

package main

import (
	"context"
	"fmt"
	"log"
	"net"

	"github.com/sethvargo/go-envconfig"
)

// 自定義 IPAddress 型別,並實現 envconfig.Decoder 介面
type IPAddress net.IP

func (ip *IPAddress) EnvDecode(val string) error {
	parsedIP := net.ParseIP(val)
	if parsedIP == nil {
		return fmt.Errorf("無效的 IP 地址格式: %s", val)
	}
	*ip = IPAddress(parsedIP)
	return nil
}

type FormattedConfig struct {
	// DB_HOST 會自動嘗試解析為 IP 地址
	DBHost IPAddress `env:"DB_HOST,required"`
}

func main() {
	ctx := context.Background()
	var config FormattedConfig

	// 測試範例:
	// export DB_HOST="192.168.1.100" (正確格式)
	// export DB_HOST="invalid-ip" (錯誤格式會報錯)
	if err := envconfig.Process(ctx, &config); err != nil {
		log.Fatalf("讀取設定失敗: %s", err)
	}

	log.Printf("資料庫主機 IP: %s", net.IP(config.DBHost).String())
}
範例:必填 (required)

在 tag 中加入 required 即可將欄位標記為必填。若對應的環境變數未設定,envconfig.Process 將回傳錯誤。

package main

import (
	"context"
	"log"

	"github.com/sethvargo/go-envconfig"
)

type RequiredConfig struct {
	// SERVICE_HOST 是必填欄位
	ServiceHost string `env:"SERVICE_HOST,required"`
	// DATABASE_URL 也是必填欄位
	DatabaseURL string `env:"DATABASE_URL,required"`
	// 注意:必填和預設值不能同時設定,否則會報錯。
}

func main() {
	ctx := context.Background()
	var config RequiredConfig

	// 若未設定 SERVICE_HOST 和 DATABASE_URL,Process 會報錯
	// 例如:export DATABASE_URL="postgresql://user:pass@host:port/db"
	if err := envconfig.Process(ctx, &config); err != nil {
		log.Fatalf("讀取設定失敗,部分必填環境變數缺失: %v", err)
	}

	log.Printf("服務主機: %s", config.ServiceHost)
	log.Printf("資料庫URL: %s", config.DatabaseURL)
}
範例:處理複雜型別 Map

預設情況下,以逗號 , 分隔的 key:value 對會被解析為 Map。你可以使用 separator 自定義鍵值分隔符號。

package main

import (
	"context"
	"log"

	"github.com/sethvargo/go-envconfig"
)

type MapConfig struct {
	// 例如:export SERVICE_ENDPOINTS="users:http://user-svc,products:http://prod-svc"
	// 預設以冒號 `:` 分隔鍵值,逗號 `,` 分隔項目
	ServiceEndpoints map[string]string `env:"SERVICE_ENDPOINTS"`
	// 可自定義分隔符號,例如用 "|" 分隔鍵值,用 ";" 分隔項目
	// 例如:export CUSTOM_SETTINGS="color|red;size|large;theme|dark"
	CustomSettings map[string]string `env:"CUSTOM_SETTINGS,delimiter=;,separator=|"`
}

func main() {
	ctx := context.Background()
	var config MapConfig

	// 測試範例:
	// export SERVICE_ENDPOINTS="auth:http://auth-api,logging:http://log-api"
	// export CUSTOM_SETTINGS="env|prod;region|us-east-1"
	if err := envconfig.Process(ctx, &config); err != nil {
		log.Fatalf("載入 Map 設定失敗: %v", err)
	}

	log.Printf("服務端點 Map: %v", config.ServiceEndpoints)
	log.Printf("自定義設定 Map: %v", config.CustomSettings)
}
範例:處理複雜型別 Slice

預設情況下,以逗號 , 分隔的字串會被解析為 Slice。你也可以使用 delimiter 來自定義分隔符號。

package main

import (
	"context"
	"log"

	"github.com/sethvargo/go-envconfig"
)

type ListConfig struct {
	// 若未設定,Ports 預設為 [8000, 9000]
	Ports []int `env:"APP_PORTS,default=8000,9000"`
	// 使用分號 ";" 作為分隔符號,例如 export FEATURES="featA;featB;featC"
	Features []string `env:"APP_FEATURES,delimiter=;"`
}

func main() {
	ctx := context.Background()
	var config ListConfig

	// 測試範例:
	// export APP_PORTS="80,443,8080"
	// export APP_FEATURES="darkMode;newUI;betaTest"
	if err := envconfig.Process(ctx, &config); err != nil {
		log.Fatalf("載入列表設定失敗: %v", err)
	}

	log.Printf("應用程式連接埠列表: %v", config.Ports)
	log.Printf("啟用功能列表: %v", config.Features)
}

Viper

GitHub - spf13/viper: Go configuration with fangs
Go configuration with fangs. Contribute to spf13/viper development by creating an account on GitHub.

功能強大且靈活的組態解決方案,支援多種組態檔案格式(如 JSON、YAML、TOML、dotenv 等),也能讀取環境變數和命令列參數。

Viper 組態來源範例:多種情境的靈活應用

Viper 的強大之處在於它能從多種來源讀取組態,並能智能地進行合併,讓你根據不同部署環境選擇最適合的組態方式。

範例:組態檔案 (Config File) 多格式支援

Viper 支援多種常見的組態檔案格式,如 JSON, TOML, YAML, HCL, INI, envfile 或 Java properties。你只需指定檔案名(不含副檔名)和檔案類型,Viper 就能自動解析。

假設你有一個 config.yaml ,檔案內容如下:

app:
  name: MyAwesomeApp
  port: 8080
database:
  host: localhost
  port: 5432
  user: admin

程式碼

package main

import (
	"fmt"
	"log"

	"github.com/spf13/viper"
)

func main() {
	viper.SetConfigName("config") // 設定檔名為 "config"
	viper.SetConfigType("yaml")   // 設定檔案類型為 YAML
	viper.AddConfigPath(".")      // 加入當前目錄為組態檔搜尋路徑

	if err := viper.ReadInConfig(); err != nil {
		log.Fatalf("無法讀取組態檔: %v", err)
	}

	fmt.Println("從 config.yaml 讀取組態:")
	fmt.Printf("應用程式名稱: %s\n", viper.GetString("app.name"))
	fmt.Printf("應用程式連接埠: %d\n", viper.GetInt("app.port"))
	fmt.Printf("資料庫主機: %s\n", viper.GetString("database.host"))
}

Viper 會依照 AddConfigPath 的順序尋找組態檔,若找到多個同名組態檔,它會使用第一個找到的。

範例:環境變數 (Environment Variable)

Viper 能夠自動讀取環境變數,並可將其對應到你的組態結構中。這對於部署到容器化環境(如 Docker、Kubernetes)非常有用。

package main

import (
	"fmt"

	"github.com/spf13/viper"
)

func main() {
	// 設定環境變數 (在執行程式前,你也可以在終端機中設定)
	// export APP_ENV_PORT=9090
	// export APP_DB_USER=envuser

	viper.AutomaticEnv()                          // 自動讀取所有環境變數
	viper.SetEnvPrefix("APP")                     // 設定環境變數前綴,Viper 會將 APP_XXX 對應到 app.XXX
	viper.BindEnv("app.env_port", "APP_ENV_PORT") // 明確綁定指定環境變數到 Viper key

	// 也可以設定預設值,當環境變數和組態檔都沒找到時使用
	viper.SetDefault("app.name", "DefaultApp")
	viper.SetDefault("database.user", "defaultuser")

	// 這裡即使沒有組態檔,也能讀取環境變數和預設值
	fmt.Println("\n從環境變數和預設值讀取組態:")
	fmt.Printf("應用程式名稱: %s\n", viper.GetString("app.name"))            // 來自預設值
	fmt.Printf("環境變數連接埠: %d\n", viper.GetInt("app.env_port"))          // 來自 APP_ENV_PORT
	fmt.Printf("資料庫使用者 (環境變數): %s\n", viper.GetString("DB_USER"))      // 直接讀取 DB_USER
	fmt.Printf("資料庫使用者 (預設值): %s\n", viper.GetString("database.user")) // 來自預設值
}

透過 AutomaticEnv(),Viper 會自動匹配環境變數名(例如 APP_NAME)與你的組態鍵(app.name),通常會將 _ 轉換為 .。而 SetEnvPrefix 則能讓 Viper 專注於帶有特定前綴的環境變數。

範例:命令列參數 (Command Line Arguments)

Viper 可以與 Go 的 flag 套件或第三方如 pflag 套件整合,將命令列參數作為組態來源。

package main

import (
	"fmt"
	"log"

	"github.com/spf13/pflag"
	"github.com/spf13/viper"
)

func main() {
	// 定義命令列參數
	_ = pflag.Int("port", 0, "應用程式運行連接埠")
	_ = pflag.Bool("debug", false, "是否啟用調試模式")
	pflag.Parse() // 解析命令列參數

	// 將 flag 綁定到 Viper
	if err := viper.BindPFlag("app.port", pflag.CommandLine.Lookup("port")); err != nil {
		log.Printf("綁定 port flag 失敗: %v", err)
	}
	if err := viper.BindPFlag("app.debug", pflag.CommandLine.Lookup("debug")); err != nil {
		log.Printf("綁定 debug flag 失敗: %v", err)
	}

	// 設定組態檔 (若有,優先級會低於命令列參數)
	viper.SetConfigName("config")
	viper.SetConfigType("yaml")
	viper.AddConfigPath(".")
	if err := viper.ReadInConfig(); err != nil {
		log.Printf("未能讀取組態檔 (這可能是預期行為): %v", err)
	}

	fmt.Println("\n從命令列參數讀取組態:")
	// 測試:go run main.go --port 8088 --debug
	fmt.Printf("應用程式連接埠: %d\n", viper.GetInt("app.port"))
	fmt.Printf("調試模式: %t\n", viper.GetBool("app.debug"))
}

命令列參數的優先級通常最高,會覆蓋組態檔案和環境變數中的相同設定。

範例:遠端儲存 (Remote Storage)

Viper 支援從遠端組態系統讀取組態,例如 etcd 或 Consul,並且能夠監控這些遠端組態的變化。這對於分散式系統中的動態組態非常有用。要使用遠端儲存功能,你需要匯入額外的驅動。

由於我不常使用這功能,就不提供範例,有興趣的可以看官方 GitHub 說明:

GitHub - spf13/viper: Go configuration with fangs
Go configuration with fangs. Contribute to spf13/viper development by creating an account on GitHub.
範例:熱重載 (Hot Reload)

Viper 支援應用程式運行時動態讀取組態檔案的變更,無需重新啟動服務。這可以透過 WatchConfig() 方法實現,並可搭配 OnConfigChange() 註冊一個回調函數,在組態變化時執行特定邏輯。

package main

import (
	"fmt"
	"log"

	"github.com/fsnotify/fsnotify" // 檔案系統事件監聽
	"github.com/spf13/viper"
)

func main() {
	viper.SetConfigName("config_live") // 使用不同的檔名以示區別
	viper.SetConfigType("yaml")
	viper.AddConfigPath(".")

	// 初始化讀取組態
	if err := viper.ReadInConfig(); err != nil {
		log.Fatalf("無法讀取初始組態檔: %v", err)
	}

	fmt.Println("初始組態:")
	fmt.Printf("應用程式名稱: %s\n", viper.GetString("app.name"))
	fmt.Printf("應用程式連接埠: %d\n", viper.GetInt("app.port"))

	// 註冊組態變更的回調函數
	viper.OnConfigChange(func(e fsnotify.Event) {
		fmt.Printf("\n組態檔案變更 detected: %s, 操作: %s\n", e.Name, e.Op.String())
		fmt.Println("新組態:")
		fmt.Printf("應用程式名稱: %s\n", viper.GetString("app.name"))
		fmt.Printf("應用程式連接埠: %d\n", viper.GetInt("app.port"))
	})

	// 啟動監聽組態變化
	viper.WatchConfig()

	fmt.Println("\n程式正在運行中,請嘗試修改或保存 'config_live.yaml' 檔案...")
	fmt.Println("例如,將 app.port 從 8080 改為 8081,然後保存。")

	// 保持程式運行,以便觀察組態變化
	select {} // 無限期等待,直到程式被終止
}

// 為了測試,請創建一個 config_live.yaml 檔案在程式執行目錄下:
/*
# config_live.yaml
app:
  name: LiveApp
  port: 8080
*/

在運行此範例時,你可以修改 config_live.yaml 檔案並保存,程式會即時捕獲變更並輸出新的組態值,而無需重新啟動。

Viper 的多種組態來源支援,讓它成為 Golang 專案中一個非常全面且靈活的組態管理工具。


Cobra

GitHub - spf13/cobra: A Commander for modern Go CLI interactions
A Commander for modern Go CLI interactions. Contribute to spf13/cobra development by creating an account on GitHub.

一個強大的庫,用於構建現代化的、基於命令列介面 (CLI) 的應用程式。它是許多流行的 Golang 專案(如 Kubernetes、Hugo、Gatling 等)的基石。

命令列框架 (Command Line Framework)

Cobra 提供了一套結構化的方式來定義命令、子命令、標誌 (flags) 和參數,讓你的 CLI 應用程式不僅易於開發,也更具用戶友好性。它自動處理解析輸入、錯誤處理和生成幫助資訊等繁瑣任務,讓開發者能專注於業務邏輯。

子命令 (Subcommands)

Cobra 的核心概念之一是子命令。它允許你將複雜的應用程式功能組織成一系列層次化的命令,例如:

yourcli command subcommand --flag argument

這種結構使得 CLI 工具的功能更清晰,更易於理解和使用。例如,一個 git 工具會有 git addgit commitgit push 等子命令。

Cobra 與 Viper 整合範例:讀取命令列參數

Cobra 本身使用 pflag 套件來處理命令列標誌,而 Viperpflag 有很好的整合。這表示你可以透過 Cobra 定義你的命令列參數,然後讓 Viper 讀取這些參數作為組態的一部分,實現命令列、環境變數和組態檔案的統一管理。

範例:一個帶有子命令並與 Viper 整合的 CLI 應用

透過這個範例,你可以看到 Cobra 如何幫助你構建結構化的 CLI 應用,而 Viper 則能無縫地讀取 Cobra 定義的命令列參數,並將其與其他組態來源(如檔案、環境變數)進行智能合併,大大簡化了組態管理的複雜性。

日誌管理

在 Golang 中,日誌是應用程式可觀察性的重要一環。選擇一個功能豐富且高效的日誌框架,能夠幫助我們更好地追蹤程式行為、診斷問題。一個好用的日誌框架通常應具備以下核心功能:

  1. 日誌元數據 (Log Metadata)
    自動或手動包含日誌的重要資訊,例如:
    • 日誌級別
      區分日誌的緊急程度,如 Debug, Info, Warn, Error, Fatal, Panic
    • 時間戳
      記錄日誌發生的精確時間。
    • 呼叫者資訊
      指明日誌是由程式碼的哪個檔案、哪一行產生。
  2. 日誌格式 (Log Format)
    支援多種輸出格式,以適應不同的應用場景,例如:
    • Console
      適合開發環境或人工閱讀。
    • JSON
      適合集中式日誌系統(如 ELK Stack, Grafana Loki)進行解析和索引。
    • Logfmt
      另一種簡潔的結構化日誌格式。
  3. 組態 (Configuration)
    允許靈活組態日誌行為,例如:
    • 日誌級別設定
      根據環境動態調整輸出日誌的最低級別。
    • 格式設定
      選擇日誌的輸出格式。
    • 輸出目標設定
      將日誌輸出到檔案、標準輸出/錯誤,甚至是遠端服務。
  4. 上下文資訊 (Contextual Logging)
    能夠在日誌中嵌入上下文資訊,例如:
    • 請求 ID (Request ID)
      追蹤單一使用者請求在不同服務間的流動。
    • 追蹤 ID (Trace ID)
      在分散式系統中,將所有相關操作的日誌串聯起來。
  5. Hooks
    提供擴展點,允許在日誌事件發生時執行自定義邏輯,例如:
    • 自動加入元數據
      如自動為所有日誌加入應用程式版本號。
    • 根據日誌級別分發
      Error 級別以上的日誌發送到錯誤監控系統 (如 Sentry),而 Info 級別的日誌則發送到標準輸出。
    • 日誌過濾、轉換等。

主流 Golang 日誌框架介紹

Golang 生態系中有許多優秀的日誌框架,以下介紹三個最受歡迎的選擇:logruszapzerolog。它們各有優勢,可以根據您的專案需求進行選擇。

Logrus

GitHub - sirupsen/logrus: Structured, pluggable logging for Go.
Structured, pluggable logging for Go. Contribute to sirupsen/logrus development by creating an account on GitHub.
  • 特性
    logrus 是一個功能豐富的結構化日誌框架,它提供了強大的 Hooks 機制和多種格式化選項,使其非常靈活。
  • 優點
    • 豐富的功能:內建了多種 Formatter (如 JSON, Text) 和 Hooks
    • 易於擴展:可以輕鬆實現自定義 FormatterHooks,與第三方服務(如 Sentry, Logstash)整合。
    • 結構化日誌:支援 WithFields 加入鍵值對,方便解析。
  • 缺點
    相對於 zapzerolog,在高並發高性能場景下可能存在性能開銷
範例:Logrus 功能
package main

import (
	"fmt"
	"os"
	"time"

	"github.com/sirupsen/logrus"
)

// CustomHook 範例:自動加入服務名稱和環境
type CustomHook struct{}

func (h *CustomHook) Levels() []logrus.Level {
	return logrus.AllLevels // 所有日誌級別都會觸發這個 Hook
}

func (h *CustomHook) Fire(entry *logrus.Entry) error {
	entry.Data["service"] = "MyAwesomeService"
	entry.Data["env"] = os.Getenv("APP_ENV") // 從環境變數獲取環境
	return nil
}

func main() {
	// 1. 設定日誌格式為 JSON
	logrus.SetFormatter(&logrus.JSONFormatter{
		TimestampFormat: time.RFC3339Nano, // 高精度時間戳
		PrettyPrint:     true,             // 讓 JSON 輸出更易讀
	})

	// 2. 設定日誌輸出目標為標準輸出
	logrus.SetOutput(os.Stdout)

	// 3. 設定日誌級別
	logrus.SetLevel(logrus.DebugLevel) // 只輸出 Debug 及以上級別的日誌

	// 4. 加入 Hook
	logrus.AddHook(&CustomHook{})

	// 5. 日誌範例
	logrus.WithFields(logrus.Fields{
		"request_id": "req-12345", // 加入 requestId
		"user_id":    "user-abc",
	}).Info("使用者登入成功")

	logrus.Debug("這是一條調試日誌,只會在 Debug 級別下顯示")

	// 模擬錯誤
	err := fmt.Errorf("資料庫連接失敗: %s", "connection refused")
	logrus.WithFields(logrus.Fields{
		"error_code": "DB-001",
		"component":  "database",
	}).Error("處理請求時發生錯誤", err)

	// 帶有 Caller 資訊的日誌
	logrus.SetReportCaller(true) // 啟用 Caller 報告
	logrus.Warn("這是一個警告,請注意!")
}

Zap

GitHub - uber-go/zap: Blazing fast, structured, leveled logging in Go.
Blazing fast, structured, leveled logging in Go. Contribute to uber-go/zap development by creating an account on GitHub.
  • 特性
    zap 是 Uber 開發的高性能日誌框架,專為生產環境設計。它使用零分配 (zero-allocation) 的設計理念,在日誌量非常大的情況下表現卓越。
  • 優點
    • 極致性能:在高性能場景下,zap 的性能遠超其他日誌框架。
    • 結構化日誌:預設輸出結構化日誌,非常適合機器解析。
    • 型別安全:使用 Field 輔助函數,避免手動格式化,減少錯誤。
    • 兩種 API 模式SugaredLogger (類似 fmt 的糖衣語法,方便使用) 和 Logger (高性能,推薦用於生產)。
  • 缺點
    相對於 logrusHooks 機制相對簡潔,且預設輸出較為精簡,學習曲線稍高。
範例:Zap 基本功能
package main

import (
	"fmt"
	"os"

	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

func main() {
	// 1. 定義 Encoder 組態:JSON 格式,並自定義時間格式
	encoderCfg := zapcore.EncoderConfig{
		MessageKey:     "message",
		LevelKey:       "level",
		TimeKey:        "time",
		NameKey:        "logger",
		CallerKey:      "caller",
		StacktraceKey:  "stacktrace",
		LineEnding:     zapcore.DefaultLineEnding,
		EncodeLevel:    zapcore.CapitalLevelEncoder, // 大寫日誌級別
		EncodeTime:     zapcore.ISO8601TimeEncoder,  // ISO8601 時間格式
		EncodeDuration: zapcore.StringDurationEncoder,
		EncodeCaller:   zapcore.ShortCallerEncoder, // 短路徑的呼叫者資訊
	}

	// 2. 選擇日誌輸出目的地 (Stdout) 和日誌級別 (Info)
	core := zapcore.NewCore(
		zapcore.NewJSONEncoder(encoderCfg), // JSON 格式輸出
		zapcore.AddSync(os.Stdout),         // 輸出到標準輸出
		zap.InfoLevel,                      // 最低日誌級別
	)

	// 3. 建立 Logger
	logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel))
	// zap.AddCaller() 啟用呼叫者資訊
	// zap.AddStacktrace(zap.ErrorLevel) 在 Error 級別及以上自動捕獲堆疊追蹤

	defer logger.Sync() // 確保所有緩衝的日誌都寫入完成

	// 4. 使用 SugaredLogger 進行更方便的日誌記錄
	sugaredLogger := logger.Sugar()

	// 5. 日誌範例
	sugaredLogger.Infow("使用者登入成功",
		"request_id", "req-67890",
		"user_id", "user-xyz",
		"ip_address", "192.168.1.1",
	)

	sugaredLogger.Debug("這條 Debug 日誌不會被顯示,因為級別是 InfoLevel")

	// 模擬錯誤
	err := fmt.Errorf("檔案寫入失敗: %s", "權限不足")
	logger.Error("處理檔案時發生錯誤",
		zap.String("error_code", "FILE-002"),
		zap.String("component", "file_processor"),
		zap.Error(err), // 結構化地記錄錯誤
	)

	// 使用 logger.With 增加上下文資訊
	reqLogger := logger.With(zap.String("trace_id", "trace-abc-123"))
	reqLogger.Info("開始處理新請求")
	reqLogger.Warn("請求處理中遇到警告")
}

Zerolog

GitHub - rs/zerolog: Zero Allocation JSON Logger
Zero Allocation JSON Logger. Contribute to rs/zerolog development by creating an account on GitHub.
  • 特性
    zerolog 是另一個高性能的日誌框架,其設計目標是零記憶體分配。它以極簡的 API 和高度可組態性著稱,非常適合微服務和高吞吐量的應用。
  • 優點
    • 卓越性能:與 zap 類似,性能非常優秀。
    • 簡潔 API:提供鏈式調用 API,寫日誌非常流暢。
    • 高度可組態:通過設置全域選項或每個日誌實例的選項,實現靈活組態。
    • 預設結構化:輸出 JSON 格式的結構化日誌。
  • 缺點
    雖然有 Hooks,但不如 logrusHooks 機制豐富;如果對日誌格式有非常複雜的自定義需求,可能需要更多的手動處理。
範例:Zerolog 基本功能
package main

import (
	"fmt"
	"os"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
)

type AppNameHook struct{}

func (h AppNameHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
	e.Str("app_name", "MyGoApp")
}

func main() {
	// 1. 全域組態 Zerolog
	zerolog.TimeFieldFormat = zerolog.TimeFormatUnixNano // 時間戳格式為 Unix 納秒
	zerolog.SetGlobalLevel(zerolog.DebugLevel)           // 設定全域最低日誌級別

	// 2. 設定輸出目標和格式
	// 對於開發環境,可以使用 ConsoleWriter 輸出漂亮的純文字
	// log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339})

	// 對於生產環境,直接輸出到 Stdout (JSON 格式)
	log.Logger = zerolog.New(os.Stdout).With().Timestamp().Caller().Logger()
	// .With().Timestamp() 自動加入時間戳
	// .Caller() 自動加入呼叫者資訊

	// 3. 自定義 Hook 範例:加入應用程式名稱
	log.Logger = log.Logger.Hook(AppNameHook{})

	// 4. 日誌範例
	log.Info().
		Str("request_id", "req-abc-789").
		Int("user_id", 123).
		Msg("用戶成功註冊")

	log.Debug().Msg("這是一條 Debug 訊息")

	// 模擬錯誤
	err := fmt.Errorf("網路錯誤: %s", "無法連接到服務")
	log.Error().
		Err(err). // 結構化地記錄錯誤
		Str("component", "network").
		Str("error_code", "NET-003").
		Msg("發送請求失敗")

	// 帶有上下文的日誌 (通常透過中間件傳遞 context)
	// 在實際應用中,會從 context.Context 中提取 trace_id/request_id
	subLogger := log.With().Str("trace_id", "trace-xyz-456").Logger()
	subLogger.Info().Msg("處理交易開始")
	subLogger.Warn().Str("transaction_id", "txn-007").Msg("交易可能存在風險")
}

總結與選擇建議

  • logrus
    如果你需要豐富的內建功能、強大的 Hooks 擴展性,並且對日誌的性能要求不是極致(但對於大多數應用也足夠),logrus 是個不錯的選擇。它能讓你輕鬆與各種第三方日誌服務整合。
  • zap
    如果你的應用程式是高吞吐量、對性能極度敏感的服務,或者你偏好嚴格的結構化日誌輸出,那麼 zap 是最佳選擇。它的零分配設計和高效的日誌寫入能確保不會成為性能瓶頸。
  • zerolog
    zerolog 提供了與 zap 類似的極致性能,但 API 更為簡潔流暢。如果你重視性能、易用性,並且喜歡鏈式調用,那麼 zerolog 會是你的理想選擇。

以下是效能測試報告可以供大家參考

在實際專案中,你應該根據應用程式的規模、性能需求、以及團隊對日誌格式和可擴展性的偏好來選擇最適合的日誌框架。無論選擇哪一個,確保日誌能夠清晰地記錄應用程式的運行狀態,並提供足夠的上下文資訊,是提升可觀察性的關鍵。


結語:持續學習,打造卓越的 Golang 應用!

今天我們深入探討了 Golang 開發中不可或缺的組態管理和日誌管理套件。這些工具和實踐不僅能提升開發效率,更能幫助你打造出穩定且高效能的 Golang 應用!

篇幅有限,無法一次涵蓋所有精彩內容,但請別擔心!在下一篇文章 Part 2 中,我將會更深入地介紹 Web 框架和HTTP Client。 敬請期待!

你有哪些愛用的 Golang 套件或開發工具,也歡迎在下方留言與我分享!