到目前為止,我們的服務只有一個最小骨架:一個 /healthz
API 和對應測試。
今天開始,我們要讓服務「更像一個真的服務」,加上 中介層 (middleware),包含:
在實務開發裡,我們常需要對所有 API 請求做統一處理,例如:
如果沒有 middleware,每個 handler 都要自己寫這些東西 → 重複、容易漏。
所以我們抽一層「中介層」來統一處理。
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)
})
}
萬一 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)
})
}
服務通常需要設定,例如 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}
}
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)
}
}
現在我們有了:
啟動服務:
# 需避免使用 PORT=9090 go run main.go,請參考補充內容
PORT=9090 go run .
測試:
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
如果啟動服務出現錯誤,可用:
go clean -cache
PORT=9090 go run .
PORT=9090 go run main.go
vs PORT=9090 go run .
PORT=9090 go run main.go
這個指令只會編譯 單一檔案 main.go
。
.go
檔案(例如 config.go
, middleware.go
)。main.go
裡呼叫了 LoadConfig
、LoggingMiddleware
,但這些定義在 config.go
/ middleware.go
裡 → 就會報 undefined
。👉 適合用在:整個專案只有一個檔案,或者你想快速測一個獨立小檔案。
PORT=9090 go run .
這個指令會把 目前目錄裡的所有 .go
檔案(同一個 package
)一起編譯、執行。
main.go
+ config.go
+ middleware.go
都會被編譯進去。👉 適合用在:專案已經拆分多檔,並且需要一起編譯。
PORT=9090 go run main.go
的編譯結果今天我們完成了:
👉 明天,我們要從 /healthz 延伸,開始設計第一個「業務相關 API」。雖然還沒有接 Elasticsearch,但先讓 /search 可以吃一個查詢參數並回傳固定的假資料,建立「API 型態 & response 結構」的雛型