目標超單純:不管API成功或失敗,API 都回同一種外觀(JSON)。這樣前端不抓狂、後端好維護、除錯更快。就像統一制服,一眼認出同一隊。
為什麼要「統一格式」?
把 API 想成餐廳出餐:
如果成功回 JSON、失敗卻回文字或 HTML,就像今天用盤子、明天用紙袋——客人(前端)必定疑惑。解法:約定固定外觀,不管成功/失敗都長一樣。
成功:
{
"status": "success",
"message": "OK",
"data": { "id": 1, "name": "小明" }
}
失敗:
{
"status": "error",
"message": "查無此使用者",
"error_code": "USER_NOT_FOUND"
}
HTTP 狀態碼小抄(實務常用)
200 OK 成功|201 Created 新增成功|400 參數錯|401 未登入|403 沒權限|404 找不到|409 資料衝突|422 驗證沒過|500 伺服器錯誤
一步一步把 main.go 組起來(像堆樂高)
跟著貼,每一步都可編譯。最後會長成完整可跑的伺服器。
mkdir echo-unified-api && cd echo-unified-api
go mod init example.com/echo-unified-api
go get github.com/labstack/echo/v4
package main
import (
"net/http"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
e.GET("/health", func(c echo.Context) error {
return c.String(http.StatusOK, "OK")
})
e.Logger.Fatal(e.Start(":1323"))
}
type ApiResponse struct {
Status string `json:"status"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
}
func JSONSuccess(c echo.Context, msg string, data interface{}, code int) error {
if code == 0 { code = http.StatusOK }
return c.JSON(code, ApiResponse{ Status: "success", Message: msg, Data: data })
}
func JSONError(c echo.Context, msg, errCode string, httpCode int) error {
if httpCode == 0 { httpCode = http.StatusBadRequest }
return c.JSON(httpCode, ApiResponse{ Status: "error", Message: msg, ErrorCode: errCode })
}
func Health(c echo.Context) error {
return JSONSuccess(c, "OK", map[string]any{"service": "up"}, http.StatusOK)
}
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
var fakeUsers = map[int]User{
1: {ID: 1, Name: "小明", Email: "ming@example.com"},
2: {ID: 2, Name: "小美", Email: "mei@example.com"},
}
func GetUserByID(c echo.Context) error {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil || id <= 0 {
return JSONError(c, "參數格式錯誤:id 應為正整數", "BAD_ID_FORMAT", http.StatusBadRequest)
}
u, ok := fakeUsers[id]
if !ok {
return JSONError(c, "查無此使用者", "USER_NOT_FOUND", http.StatusNotFound)
}
return JSONSuccess(c, "使用者資料取得成功", u, http.StatusOK)
}
路由掛法:
api := e.Group("/api")
api.GET("/health", Health)
api.GET("/users/:id", GetUserByID)
e.Use(middleware.Logger()) // 請求日誌
e.Use(middleware.Recover()) // panic 自動復原
e.Use(middleware.CORS()) // 跨域
func makeHTTPErrorHandler() echo.HTTPErrorHandler {
return func(err error, c echo.Context) {
code := http.StatusInternalServerError
msg := "伺服器內部錯誤"
errCode := "INTERNAL_SERVER_ERROR"
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
if s, ok := he.Message.(string); ok && s != "" { msg = s } else if he.Message != nil {
msg = fmt.Sprintf("%v", he.Message)
}
switch code {
case http.StatusNotFound: errCode = "ROUTE_NOT_FOUND"
case http.StatusBadRequest: errCode = "BAD_REQUEST"
case http.StatusUnauthorized: errCode = "UNAUTHORIZED"
case http.StatusForbidden: errCode = "FORBIDDEN"
}
}
if !c.Response().Committed {
_ = c.JSON(code, ApiResponse{ Status: "error", Message: msg, ErrorCode: errCode })
}
}
}
在 main() 啟用:
e.HTTPErrorHandler = makeHTTPErrorHandler()
完整版 main.go
...完整程式碼如前所示...
測試一下
go run main.go
# 成功
curl -i http://localhost:1323/api/users/1
# 失敗:查無
curl -i http://localhost:1323/api/users/9999
# 健康檢查
curl -i http://localhost:1323/api/health
# 路由不存在
curl -i http://localhost:1323/api/unknown
出場前檢查
• 回應都有 status、message
• 成功用 data,失敗用 error_code
• HTTP 狀態碼用對
• 已設定 HTTPErrorHandler
• 常用 JSONSuccess / JSONError