用 Go 建立最小 HTTP Server: 專案結構與 Chi Router
DevOps 學習筆記
從零開始建立一個符合北美業界標準的最小 Go HTTP server, 包含專案結構說明與 chi router 使用
我的背景是 .NET Engineer, 因此筆記中會用 .NET 的對應概念來輔助釐清 Go 的設計
如果你也有 .NET 背景, 這些對比應該會有幫助
專案結構
整體配置
|
|
cmd/ 目錄
Go 規定每個可執行程式需要一個 package main 和 main() 函數。cmd/ 是 Go 社群慣例, 用來放這個 repo 會產生幾個 binary 的入口
|
|
Binary 是什麼? Go 編譯後產生的單一可執行檔, 不需要任何 runtime 就能直接執行。對比 .NET 的 MyApp.dll (需要 runtime), Go binary 是完全獨立的。只有 package main 的檔案才產生 binary, internal/ 裡的程式碼是被編譯進去的, 不是獨立存在的
main.go 只負責讀設定、組裝元件、啟動 server, 不寫業務邏輯
對比 .NET:
| Go | .NET |
|---|---|
cmd/server/main.go |
Program.cs |
internal/ 目錄
Go 語言層級的封裝機制。internal/ 裡面的 package, 只有同一個 module 內的程式碼可以 import, 外部 module 完全無法使用
|
|
internal/ 本身不是 Service 也不是 Repository, 它只是封裝邊界, 裡面自己決定怎麼分層
.NET 也有類似概念:
|
|
Go 用目錄名稱來強制執行同樣的事, 編譯器層面直接阻擋外部 import
go.mod 與 go.sum
是什麼?
| 檔案 | 對比 .NET | 用途 |
|---|---|---|
go.mod |
*.csproj |
定義 module 名稱、Go 版本、依賴套件 |
go.sum |
packages.lock.json |
每個依賴的 hash, 確保套件沒被竄改 |
go.sum 是自動產生的, 不需要手寫。兩個都要 commit 進 git, 這樣團隊每個人拿到的套件版本和內容完全一致
建立流程
|
|
Router 選擇
業界 Go backend 幾乎都會加一個輕量 router:
| 選項 | 特性 |
|---|---|
chi |
輕量、完全相容 net/http、業界常用 |
gin |
功能完整, 也很主流 |
| 標準庫 | 純練習可以, 業界幾乎都加 router |
chi 是 net/http 的薄包裝, 最接近 Go idiom, 又符合業界實際做法
實作
internal/handler/health.go
|
|
cmd/server/main.go
|
|
/healthz 是什麼?
業界標配 endpoint, 用途:
- Docker:
HEALTHCHECK指令定期打這個 endpoint, 確認 container 是否存活 - Kubernetes: liveness probe / readiness probe
- Load balancer: 確認 instance 可以接流量
Response 就是 200 OK + {"status": "ok"}
幾個 Go 特有的問題
為什麼 r 是 pointer (*http.Request)?
Go 傳參數預設是複製整份資料 (pass by value)。http.Request 是大 struct, 裡面含 headers、body、URL、context 等, 傳 pointer 只傳 8 bytes 的記憶體位址, 不複製資料
另外, middleware 可能會修改 request (例如加 context), 用 pointer 才能讓修改對所有後續 handler 可見
http.ResponseWriter 沒有 * 是因為它本身已經是 interface, interface 在 Go 內部就是 pointer, 不需要再加
為什麼 r 沒用到但不報錯?
Go 不允許宣告變數卻不用, 但函數參數例外。r *http.Request 必須寫, 因為 chi 要求 handler 的函數簽名必須符合這個格式:
|
|
這是一個契約, 類似 .NET 的 interface。chi 不管你用不用 r, 它只看函數簽名是否符合
map[string]string 是什麼?
Go 的 map 語法: map[KeyType]ValueType
|
|
轉成 JSON 就是 {"status": "ok"}
每個檔案都要 import? 不能集中嗎?
Go 沒有全域 import, 每個檔案必須自己宣告用到的東西。這是刻意的設計, 讓每個檔案的依賴一目瞭然
- 標準庫寫短路徑:
"net/http"、"encoding/json" - 第三方套件寫完整路徑:
"github.com/go-chi/chi/v5" - 使用時只用最後一段的 package name:
chi.NewRouter(), 不是github.com/go-chi/chi/v5.NewRouter()
名稱衝突時可加別名:
|
|
Handler 對比 .NET Controller
|
|
|
|
Go 沒有 class, handler 就是符合特定簽名的函數。Route 的設定在 main.go 集中管理, 不是用 attribute 分散在各個 controller 上
middleware 的作用
|
|
Logger: 每個 request 進來都自動 log, 對比 .NET 的app.UseHttpLogging()Recoverer: handler 如果 panic, 不讓整個 server 掛掉, 自動回 500, 對比 .NET 的app.UseExceptionHandler()
本機測試
|
|
References
- Go Documentation — 官方文件, 涵蓋語言規範與標準庫
- Go Modules Reference — go.mod、go.sum、依賴管理的完整說明
- Standard Go Project Layout — 社群整理的 Go 專案結構慣例,
cmd/、internal/等目錄的由來 - chi router — 輕量 HTTP router, 完全相容
net/http