iT邦幫忙

2025 iThome 鐵人賽

DAY 4
1

到目前為止,我們的服務只有一個最小骨架:一個 /healthz API 和對應測試。

今天開始,我們要讓服務「更像一個真的服務」,加上 中介層 (middleware),包含:

  • 結構化日誌 (logging)
  • Recovery(避免 panic 直接炸掉服務)
  • 設定管理 (configuration)

為什麼要有 Middleware?

在實務開發裡,我們常需要對所有 API 請求做統一處理,例如:

  • 紀錄每次請求的 log
  • 異常時自動捕捉並輸出錯誤訊息
  • 讀取環境變數,讓開發/測試/生產有不同設定

如果沒有 middleware,每個 handler 都要自己寫這些東西 → 重複、容易漏。

所以我們抽一層「中介層」來統一處理。


Step 1:結構化日誌

Go 內建的 log 很簡單,但不夠適合服務端。

這裡先用 log 寫一個 middleware,將每次請求的 method、path、耗時 打出來。

新增 middleware.go

package main

import (
	"log"
	"net/http"
	"time"
)

func LoggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		next.ServeHTTP(w, r)
		duration := time.Since(start)

		log.Printf("%s %s %v", r.Method, r.URL.Path, duration)
	})
}


Step 2:Recovery Middleware

萬一 handler 裡出現 panic,如果不處理,整個伺服器會直接 crash。

我們可以加一層 Recovery middleware 來兜底:

func RecoveryMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				log.Printf("panic: %v", err)
				http.Error(w, "internal server error", http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	})
}


Step 3:設定管理

服務通常需要設定,例如 port、資料庫連線字串。

我們可以先從最簡單的「讀取環境變數」開始。

新增 config.go

package main

import (
	"log"
	"os"
)

type Config struct {
	Port string
}

func LoadConfig() *Config {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080" // 預設值
	}
	log.Printf("Loaded config: port=%s", port)
	return &Config{Port: ":" + port}
}


Step 4:整合進 main.go

修改 main.go

package main

import (
	"fmt"
	"log"
	"net/http"
)

func healthHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "ok")
}

func main() {
	cfg := LoadConfig()

	mux := http.NewServeMux()
	mux.HandleFunc("/healthz", healthHandler)

	handler := LoggingMiddleware(RecoveryMiddleware(mux))

	log.Printf("Server listening on %s", cfg.Port)
	if err := http.ListenAndServe(cfg.Port, handler); err != nil {
		log.Fatal(err)
	}
}

現在我們有了:

  • 統一的 log
  • panic 保護
  • 可透過環境變數控制 port

Step 5:測試一下

啟動服務:

# 需避免使用 PORT=9090 go run main.go,請參考補充內容
PORT=9090 go run .

https://ithelp.ithome.com.tw/upload/images/20250918/20138331z9kGSzQlfV.png

測試:

curl http://localhost:9090/healthz
# ok

終端機會看到類似 log:

2025/09/16  Loaded config: port=9090
2025/09/16  Server listening on :9090
2025/09/16  GET /healthz 10.584µs

https://ithelp.ithome.com.tw/upload/images/20250918/20138331T9IDHYCx6d.png

如果啟動服務出現錯誤,可用:

go clean -cache

PORT=9090 go run .

補充:PORT=9090 go run main.go vs PORT=9090 go run .

1. PORT=9090 go run main.go

這個指令只會編譯 單一檔案 main.go

  • 它不會去編譯同資料夾裡的其他 .go 檔案(例如 config.go, middleware.go)。
  • 如果 main.go 裡呼叫了 LoadConfigLoggingMiddleware,但這些定義在 config.go / middleware.go 裡 → 就會報 undefined

👉 適合用在:整個專案只有一個檔案,或者你想快速測一個獨立小檔案。

2. PORT=9090 go run .

這個指令會把 目前目錄裡的所有 .go 檔案(同一個 package)一起編譯、執行。

  • main.go + config.go + middleware.go 都會被編譯進去。
  • 這才是一般專案開發時應該用的方式。

👉 適合用在:專案已經拆分多檔,並且需要一起編譯。

目前專案結構

https://ithelp.ithome.com.tw/upload/images/20250918/20138331h6BZgOIiQ5.png

  • 在本專案使用 PORT=9090 go run main.go 的編譯結果
    https://ithelp.ithome.com.tw/upload/images/20250918/20138331X0qkfx7VcS.png

小結

今天我們完成了:

  • 實作 LoggingMiddleware,統一輸出請求資訊
  • 實作 RecoveryMiddleware,避免 panic 讓服務整個掛掉
  • 加入 設定管理,透過環境變數控制 port

👉 明天,我們要從 /healthz 延伸,開始設計第一個「業務相關 API」。雖然還沒有接 Elasticsearch,但先讓 /search 可以吃一個查詢參數並回傳固定的假資料,建立「API 型態 & response 結構」的雛型



上一篇
Day 3 - 測試骨架:Table-driven 測試
下一篇
Day 5 - API 成形:先回假資料的 /search
系列文
用 Golang + Elasticsearch + Kubernetes 打造雲原生搜尋服務6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言