claude-code/claude-zh/skills/golang-patterns/SKILL.md

674 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: golang-patterns
description: 道地的 Go 語言模式、最佳實踐與慣例,用於建構穩健、高效且易於維護的 Go 應用程序。
---
# Go 開發模式 (Go Development Patterns)
道地的 Go 語言模式與最佳實踐,用於建構穩健、高效且易於維護的應用程序。
## 何時啟用
- 撰寫新的 Go 程式碼。
- 審查 Go 程式碼。
- 重構現有的 Go 程式碼。
- 設計 Go 套件 (Packages) 或模組 (Modules)。
## 核心原則
### 1. 簡潔與清晰 (Simplicity and Clarity)
Go 語言偏好簡潔而非精巧。程式碼應該是顯而易見且易於閱讀的。
```go
// 推薦 (Good):清晰且直接
func GetUser(id string) (*User, error) {
user, err := db.FindUser(id)
if err != nil {
return nil, fmt.Errorf("獲取使用者 %s 失敗: %w", id, err)
}
return user, nil
}
// 不推薦 (Bad):過於取巧
func GetUser(id string) (*User, error) {
return func() (*User, error) {
if u, e := db.FindUser(id); e == nil {
return u, nil
} else {
return nil, e
}
}()
}
```
### 2. 讓零值 (Zero Value) 變得有用
設計型別時,應使其零值在無需初始化的情况下即可直接使用。
```go
// 推薦 (Good):零值很有用
type Counter struct {
mu sync.Mutex
count int // 零值為 0可直接使用
}
func (c *Counter) Inc() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}
// 推薦 (Good)bytes.Buffer 的零值即可工作
var buf bytes.Buffer
buf.WriteString("hello")
// 不推薦 (Bad):需要明確初始化
type BadCounter struct {
counts map[string]int // 操作 nil map 會引發 panic
}
```
### 3. 接受介面,回傳結構體 (Accept Interfaces, Return Structs)
函式應接受介面 (Interface) 參數,並回傳具體型別 (Concrete Types/Structs)。
```go
// 推薦 (Good):接受介面,回傳具體型別
func ProcessData(r io.Reader) (*Result, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
return &Result{Data: data}, nil
}
// 不推薦 (Bad):回傳介面 (無謂地隱藏了實作細節)
func ProcessData(r io.Reader) (io.Reader, error) {
// ...
}
```
## 錯誤處理模式 (Error Handling Patterns)
### 帶有上下文的錯誤包裝 (Error Wrapping)
```go
// 推薦 (Good):使用上下文包裝錯誤
func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("載入配置檔案 %s 失敗: %w", path, err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("解析配置檔案 %s 失敗: %w", path, err)
}
return &cfg, nil
}
```
### 自定義錯誤型別
```go
// 定義領域特定的錯誤
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("欄位 %s 驗證失敗: %s", e.Field, e.Message)
}
// 常見案例的哨兵錯誤 (Sentinel errors)
var (
ErrNotFound = errors.New("找不到資源")
ErrUnauthorized = errors.New("未經授權")
ErrInvalidInput = errors.New("無效的輸入")
)
```
### 使用 errors.Is 與 errors.As 進行錯誤檢查
```go
func HandleError(err error) {
// 檢查特定的錯誤內容
if errors.Is(err, sql.ErrNoRows) {
log.Println("找不到紀錄")
return
}
// 檢查錯誤型別
var validationErr *ValidationError
if errors.As(err, &validationErr) {
log.Printf("欄位 %s 發生驗證錯誤: %s",
validationErr.Field, validationErr.Message)
return
}
// 未知錯誤
log.Printf("發生非預期錯誤: %v", err)
}
```
### 絕不忽略錯誤
```go
// 不推薦 (Bad):使用空白識別字忽略錯誤
result, _ := doSomething()
// 推薦 (Good):處理錯誤,或明確說明為何可以安全忽略
result, err := doSomething()
if err != nil {
return err
}
// 可接受 (Acceptable):當錯誤真的無關緊要時 (極少見)
_ = writer.Close() // 盡力清理資源,錯誤已在別處記錄
```
## 併發模式 (Concurrency Patterns)
### 工作池 (Worker Pool)
```go
func WorkerPool(jobs <-chan Job, results chan<- Result, numWorkers int) {
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
results <- process(job)
}
}()
}
wg.Wait()
close(results)
}
```
### 使用 Context 處理取消與超時
```go
func FetchWithTimeout(ctx context.Context, url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("建立請求失敗: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("獲取 %s 失敗: %w", url, err)
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
```
### 優雅關機 (Graceful Shutdown)
```go
func GracefulShutdown(server *http.Server) {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("正在關閉伺服器...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("伺服器被迫強制關閉: %v", err)
}
log.Println("伺服器已退出")
}
```
### 使用 errgroup 協調多個 Goroutine
```go
import "golang.org/x/sync/errgroup"
func FetchAll(ctx context.Context, urls []string) ([][]byte, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([][]byte, len(urls))
for i, url := range urls {
i, url := i, url // 捕捉迴圈變數
g.Go(func() error {
data, err := FetchWithTimeout(ctx, url)
if err != nil {
return err
}
results[i] = data
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
```
### 避免 Goroutine 洩漏
```go
// 不推薦 (Bad):若 Context 被取消Goroutine 會洩漏
func leakyFetch(ctx context.Context, url string) <-chan []byte {
ch := make(chan []byte)
go func() {
data, _ := fetch(url)
ch <- data // 如果沒有接收者,會永遠阻塞在這裡
}()
return ch
}
// 推薦 (Good):正確處理取消信號
func safeFetch(ctx context.Context, url string) <-chan []byte {
ch := make(chan []byte, 1) // 具備緩衝的通道
go func() {
data, err := fetch(url)
if err != nil {
return
}
select {
case ch <- data:
case <-ctx.Done():
}
}()
return ch
}
```
## 介面設計 (Interface Design)
### 小型、聚焦的介面
```go
// 推薦 (Good):單一方法的介面
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// 根據需要組合介面
type ReadWriteCloser interface {
Reader
Writer
Closer
}
```
### 在使用處定義介面
```go
// 介面應定義在消費者套件中,而非生產者套件
package service
// UserStore 定義了此服務所需的功能
type UserStore interface {
GetUser(id string) (*User, error)
SaveUser(user *User) error
}
type Service struct {
store UserStore
}
// 具體實作可以放在另一個套件中
// 它不需要知道這個介面的存在
```
### 使用型別斷言 (Type Assertions) 處理選用行為
```go
type Flusher interface {
Flush() error
}
func WriteAndFlush(w io.Writer, data []byte) error {
if _, err := w.Write(data); err != nil {
return err
}
// 如果支援 Flush則執行之
if f, ok := w.(Flusher); ok {
return f.Flush()
}
return nil
}
```
## 套件組織 (Package Organization)
### 標準專案結構
```text
myproject/
├── cmd/
│ └── myapp/
│ └── main.go # 程式入口點
├── internal/
│ ├── handler/ # HTTP 處理常式
│ ├── service/ # 業務邏輯
│ ├── repository/ # 資料存取
│ └── config/ # 配置管理
├── pkg/
│ └── client/ # 公開的 API 客戶端
├── api/
│ └── v1/ # API 定義 (proto, OpenAPI)
├── testdata/ # 測試用的資料
├── go.mod
├── go.sum
└── Makefile
```
### 套件命名規則
```go
// 推薦 (Good):簡短、小寫、無底線
package http
package json
package user
// 不推薦 (Bad):冗長、大小寫混合或重複
package httpHandler
package json_parser
package userService // 重複的 'Service' 後綴
```
### 避免套件層級的狀態 (Package-Level State)
```go
// 不推薦 (Bad):全域可變狀態
var db *sql.DB
func init() {
db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))
}
// 推薦 (Good):依賴注入
type Server struct {
db *sql.DB
}
func NewServer(db *sql.DB) *Server {
return &Server{db: db}
}
```
## 結構體設計 (Struct Design)
### 功能選項模式 (Functional Options Pattern)
```go
type Server struct {
addr string
timeout time.Duration
logger *log.Logger
}
type Option func(*Server)
func WithTimeout(d time.Duration) Option {
return func(s *Server) {
s.timeout = d
}
}
func WithLogger(l *log.Logger) Option {
return func(s *Server) {
s.logger = l
}
}
func NewServer(addr string, opts ...Option) *Server {
s := &Server{
addr: addr,
timeout: 30 * time.Second, // 預設值
logger: log.Default(), // 預設值
}
for _, opt := range opts {
opt(s)
}
return s
}
// 用法
server := NewServer(":8080",
WithTimeout(60*time.Second),
WithLogger(customLogger),
)
```
### 使用嵌入 (Embedding) 進行組合
```go
type Logger struct {
prefix string
}
func (l *Logger) Log(msg string) {
fmt.Printf("[%s] %s\n", l.prefix, msg)
}
type Server struct {
*Logger // 嵌入 - Server 會繼承 Log 方法
addr string
}
func NewServer(addr string) *Server {
return &Server{
Logger: &Logger{prefix: "SERVER"},
addr: addr,
}
}
// 用法
s := NewServer(":8080")
s.Log("正在啟動...") // 呼叫嵌入的 Logger.Log
```
## 記憶體與效能
### 在已知大小時預先配置 Slice
```go
// 不推薦 (Bad):導致 Slice 多次擴容
func processItems(items []Item) []Result {
var results []Result
for _, item := range items {
results = append(results, process(item))
}
return results
}
// 推薦 (Good):一次完成配置
func processItems(items []Item) []Result {
results := make([]Result, 0, len(items))
for _, item := range items {
results = append(results, process(item))
}
return results
}
```
### 為頻繁配置使用 sync.Pool
```go
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func ProcessRequest(data []byte) []byte {
buf := bufferPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufferPool.Put(buf)
}()
buf.Write(data)
// 處理中...
return buf.Bytes()
}
```
### 避免在迴圈中直接拼接字串
```go
// 不推薦 (Bad):會產生大量的字串記憶體配置
func join(parts []string) string {
var result string
for _, p := range parts {
result += p + ","
}
return result
}
// 推薦 (Good):使用 strings.Builder 進行單次配置
func join(parts []string) string {
var sb strings.Builder
for i, p := range parts {
if i > 0 {
sb.WriteString(",")
}
sb.WriteString(p)
}
return sb.String()
}
// 最佳做法 (Best):直接使用標準函式庫
func join(parts []string) string {
return strings.Join(parts, ",")
}
```
## Go 工具整合
### 核心指令
```bash
# 建置與執行
go build ./...
go run ./cmd/myapp
# 測試
go test ./...
go test -race ./...
go test -cover ./...
# 靜態分析
go vet ./...
staticcheck ./...
golangci-lint run
# 模組管理
go mod tidy
go mod verify
# 格式化
gofmt -w .
goimports -w .
```
### 建議的 Linter 配置 (.golangci.yml)
```yaml
linters:
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused
- gofmt
- goimports
- misspell
- unconvert
- unparam
linters-settings:
errcheck:
check-type-assertions: true
govet:
check-shadowing: true
issues:
exclude-use-default: false
```
## 快速參考Go 語言慣用語 (Idioms)
| 慣用語 | 描述 |
|-------|-------------|
| 接受介面,回傳結構體 | 函式參數用介面,回傳值用具體型別 |
| 錯誤即值 (Errors are values) | 將錯誤視為一等公民值處理,而非例外情況 |
| 不要透過共享記憶體來通訊 | 應使用通道 (Channels) 在 Goroutine 間進行協調 |
| 讓零值變得有用 | 型別應在不需顯式初始化的情況下即可運作 |
| 少量拷貝優於少量依賴 | 避免不必要的外部相依套件 |
| 清晰優於精巧 | 優先考量程式碼的可讀性 |
| gofmt 是大家的朋友 | 務必始終使用 gofmt/goimports 手動格式化 |
| 提早回傳 (Return early) | 先處理錯誤情況,保持正常路徑 (Happy Path) 不縮排 |
## 應避免的反模式
```go
// 不推薦 (Bad):在長函式中使用具名回傳值 (Naked returns)
func process() (result int, err error) {
// ... 50 行程式碼 ...
return // 到底回傳了什麼?
}
// 不推薦 (Bad):將 panic 用於流程控制
func GetUser(id string) *User {
user, err := db.Find(id)
if err != nil {
panic(err) // 絕對不要這樣做
}
return user
}
// 不推薦 (Bad):在結構體中傳遞 Context
type Request struct {
ctx context.Context // Context 應作為第一個參數
ID string
}
// 推薦 (Good)Context 作為第一個參數
func ProcessRequest(ctx context.Context, id string) error {
// ...
}
// 不推薦 (Bad):混合使用數值與指標接收者 (Receivers)
type Counter struct{ n int }
func (c Counter) Value() int { return c.n } // 數值接收者
func (c *Counter) Increment() { c.n++ } // 指標接收者
// 請擇一使用並保持一致性
```
**請記住**Go 程式碼應該以「無趣」為最高指導原則 — 意即它是可預測的、一致的且易於理解的。如有疑慮,請保持簡潔。