這是 Go 語言從零到 Web 應用系列的第十三篇,進入進階篇章。前面我們用標準庫 net/http 建構了完整的 Web 應用,現在來看看 Web 框架在標準庫之上提供了什麼價值。

為什麼需要 Web 框架?

標準庫 net/http 非常強大,但在建構大型應用時會遇到一些不便:

// 標準庫:手動解析路由參數
mux := http.NewServeMux()
mux.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
    // 需要手動從 URL 提取 ID
    id := strings.TrimPrefix(r.URL.Path, "/users/")
    if id == "" {
        http.Error(w, "missing id", http.StatusBadRequest)
        return
    }
    // 還需要手動判斷 HTTP method
    switch r.Method {
    case http.MethodGet:
        // 處理 GET
    case http.MethodPut:
        // 處理 PUT
    default:
        http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
    }
})

Web 框架幫你解決的問題:

  • 路由參數/users/:id 自動解析
  • HTTP method 路由GET /users/:idPUT /users/:id 分開定義
  • 請求綁定:JSON、Query、Form 自動綁定到 struct
  • 參數驗證:struct tag 自動驗證
  • 中間件:標準化的中間件鏈
  • 錯誤處理:集中式錯誤處理

Go 生態中最熱門的兩個框架是 GinEcho,讓我們用同一個 API 來對比。

Gin

安裝:

go get -u github.com/gin-gonic/gin

路由與參數

package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default() // 內建 Logger + Recovery 中間件

    // 路由參數
    r.GET("/users/:id", getUser)

    // 路由群組
    api := r.Group("/api/v1")
    {
        api.GET("/books", listBooks)
        api.POST("/books", createBook)
        api.GET("/books/:id", getBook)
        api.PUT("/books/:id", updateBook)
        api.DELETE("/books/:id", deleteBook)
    }

    r.Run(":8080")
}

JSON 綁定與驗證

type CreateBookRequest struct {
    Title  string `json:"title" binding:"required,min=1,max=200"`
    Author string `json:"author" binding:"required"`
    Year   int    `json:"year" binding:"required,gte=1000,lte=2100"`
}

func createBook(c *gin.Context) {
    var req CreateBookRequest

    // ShouldBindJSON 自動解析 JSON 並驗證
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }

    // 建立書籍...
    c.JSON(http.StatusCreated, gin.H{
        "id":     1,
        "title":  req.Title,
        "author": req.Author,
        "year":   req.Year,
    })
}

中間件

// 自定義中間件
func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        if token == "" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
                "error": "missing authorization header",
            })
            return
        }

        // 驗證 token...
        userID := "user-123" // 假設從 token 解析出來
        c.Set("userID", userID)
        c.Next() // 繼續執行下一個 handler
    }
}

// 使用中間件
func main() {
    r := gin.Default()

    // 全域中間件
    r.Use(AuthMiddleware())

    // 群組中間件
    admin := r.Group("/admin")
    admin.Use(AdminOnly())
    {
        admin.GET("/stats", getStats)
    }
}

完整 REST API 範例

package main

import (
    "net/http"
    "strconv"
    "sync"

    "github.com/gin-gonic/gin"
)

type Book struct {
    ID     int    `json:"id"`
    Title  string `json:"title"`
    Author string `json:"author"`
}

var (
    books  = make(map[int]Book)
    nextID = 1
    mu     sync.RWMutex
)

func main() {
    r := gin.Default()

    r.GET("/books", func(c *gin.Context) {
        mu.RLock()
        defer mu.RUnlock()
        result := make([]Book, 0, len(books))
        for _, b := range books {
            result = append(result, b)
        }
        c.JSON(http.StatusOK, result)
    })

    r.POST("/books", func(c *gin.Context) {
        var b Book
        if err := c.ShouldBindJSON(&b); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }
        mu.Lock()
        b.ID = nextID
        nextID++
        books[b.ID] = b
        mu.Unlock()
        c.JSON(http.StatusCreated, b)
    })

    r.GET("/books/:id", func(c *gin.Context) {
        id, _ := strconv.Atoi(c.Param("id"))
        mu.RLock()
        b, ok := books[id]
        mu.RUnlock()
        if !ok {
            c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
            return
        }
        c.JSON(http.StatusOK, b)
    })

    r.DELETE("/books/:id", func(c *gin.Context) {
        id, _ := strconv.Atoi(c.Param("id"))
        mu.Lock()
        delete(books, id)
        mu.Unlock()
        c.Status(http.StatusNoContent)
    })

    r.Run(":8080")
}

Echo

安裝:

go get -u github.com/labstack/echo/v4

路由與參數

package main

import (
    "net/http"
    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)

func main() {
    e := echo.New()

    // 內建中間件
    e.Use(middleware.Logger())
    e.Use(middleware.Recover())

    // 路由參數
    e.GET("/users/:id", getUser)

    // 路由群組
    api := e.Group("/api/v1")
    api.GET("/books", listBooks)
    api.POST("/books", createBook)
    api.GET("/books/:id", getBook)
    api.PUT("/books/:id", updateBook)
    api.DELETE("/books/:id", deleteBook)

    e.Logger.Fatal(e.Start(":8080"))
}

JSON 綁定與驗證

type CreateBookRequest struct {
    Title  string `json:"title" validate:"required,min=1,max=200"`
    Author string `json:"author" validate:"required"`
    Year   int    `json:"year" validate:"required,gte=1000,lte=2100"`
}

func createBook(c echo.Context) error {
    var req CreateBookRequest

    // Bind 自動解析 JSON
    if err := c.Bind(&req); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }

    // Echo 需要額外設定 validator
    if err := c.Validate(&req); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }

    return c.JSON(http.StatusCreated, map[string]interface{}{
        "id":     1,
        "title":  req.Title,
        "author": req.Author,
        "year":   req.Year,
    })
}

中間件

// 自定義中間件
func AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        token := c.Request().Header.Get("Authorization")
        if token == "" {
            return echo.NewHTTPError(http.StatusUnauthorized, "missing authorization header")
        }

        // 驗證 token...
        userID := "user-123"
        c.Set("userID", userID)
        return next(c) // 繼續執行
    }
}

// 使用中間件
func main() {
    e := echo.New()

    // 全域中間件
    e.Use(AuthMiddleware)

    // 群組中間件
    admin := e.Group("/admin", AdminOnly)
    admin.GET("/stats", getStats)
}

完整 REST API 範例(同 Gin 功能)

package main

import (
    "net/http"
    "strconv"
    "sync"

    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)

type Book struct {
    ID     int    `json:"id"`
    Title  string `json:"title"`
    Author string `json:"author"`
}

var (
    books  = make(map[int]Book)
    nextID = 1
    mu     sync.RWMutex
)

func main() {
    e := echo.New()
    e.Use(middleware.Logger())
    e.Use(middleware.Recover())

    e.GET("/books", func(c echo.Context) error {
        mu.RLock()
        defer mu.RUnlock()
        result := make([]Book, 0, len(books))
        for _, b := range books {
            result = append(result, b)
        }
        return c.JSON(http.StatusOK, result)
    })

    e.POST("/books", func(c echo.Context) error {
        var b Book
        if err := c.Bind(&b); err != nil {
            return echo.NewHTTPError(http.StatusBadRequest, err.Error())
        }
        mu.Lock()
        b.ID = nextID
        nextID++
        books[b.ID] = b
        mu.Unlock()
        return c.JSON(http.StatusCreated, b)
    })

    e.GET("/books/:id", func(c echo.Context) error {
        id, _ := strconv.Atoi(c.Param("id"))
        mu.RLock()
        b, ok := books[id]
        mu.RUnlock()
        if !ok {
            return echo.NewHTTPError(http.StatusNotFound, "not found")
        }
        return c.JSON(http.StatusOK, b)
    })

    e.DELETE("/books/:id", func(c echo.Context) error {
        id, _ := strconv.Atoi(c.Param("id"))
        mu.Lock()
        delete(books, id)
        mu.Unlock()
        return c.NoContent(http.StatusNoContent)
    })

    e.Logger.Fatal(e.Start(":8080"))
}

Gin vs Echo 對比

維度 Gin Echo
GitHub Stars 80k+ 30k+
Handler 簽名 func(c *gin.Context) func(c echo.Context) error
錯誤處理 手動寫 response 回傳 error,集中處理
驗證 內建(binding tag) 需自行整合 validator
效能 極快(httprouter 基底) 極快(自建路由樹)
中間件 gin.HandlerFunc 標準函式包裝
路由群組 r.Group() + 大括號 e.Group()
JSON 回應 c.JSON(code, obj) return c.JSON(code, obj)
社群生態 較大,第三方套件多 中等,核心團隊維護良好
API 風格 較隱式(不回傳 error) 較顯式(handler 回傳 error)

如何選擇?

選 Gin 如果:

  • 你的團隊已經熟悉 Gin
  • 需要更多第三方生態整合
  • 偏好內建的驗證功能

選 Echo 如果:

  • 你偏好 Go 慣用的 error 回傳模式
  • 需要更好的錯誤集中處理
  • 喜歡較乾淨的 API 設計

兩者效能差距極小,選擇哪個主要看團隊偏好和 API 風格喜好。

下一步

選好了框架,下一步要解決資料庫操作的問題。下一篇我們來比較 Go 的兩大資料庫工具:GORM(全功能 ORM)和 sqlx(SQL-first 輕量擴展)。