--- 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 程式碼應該以「無趣」為最高指導原則 — 意即它是可預測的、一致的且易於理解的。如有疑慮,請保持簡潔。