這是 Go 語言從零到 Web 應用系列的第六篇,也是實戰篇的開始。從這篇開始,我們要用前五篇學到的 Go 基礎,開始建構一個真正的 Web 應用。

為什麼 Go 適合做 Web 開發?

Go 的標準庫 net/http 已經是一個生產等級的 HTTP 伺服器。不像其他語言需要額外安裝框架(如 Python 的 Flask、Node.js 的 Express),Go 開箱即用就能處理 HTTP 請求。

這個標準庫的品質高到什麼程度?許多知名的 Go Web 框架(如 Gin、Echo)本質上都是在 net/http 之上加了一層便利功能。

最簡單的 Web Server

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, World!")
    })

    fmt.Println("Server starting on :8080")
    http.ListenAndServe(":8080", nil)
}

執行後用瀏覽器打開 http://localhost:8080,你就會看到 “Hello, World!"。

就是這麼簡單——5 行核心程式碼就建好了一個 HTTP 伺服器。

理解 Handler

http.Handler 介面

Go 的 HTTP 處理核心是 Handler 介面:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

任何實作了 ServeHTTP 方法的型別都可以處理 HTTP 請求。

http.HandlerFunc

大多數時候我們不會自己實作 Handler 介面,而是用 HandlerFunc——它是一個將普通函式轉成 Handler 的型別:

// 定義處理函式
func homeHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome to the homepage!")
}

func aboutHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "About page")
}

func main() {
    http.HandleFunc("/", homeHandler)
    http.HandleFunc("/about", aboutHandler)

    http.ListenAndServe(":8080", nil)
}

自訂 Handler 結構

type APIHandler struct {
    Version string
}

func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "API Version: %s", h.Version)
}

func main() {
    api := &APIHandler{Version: "1.0"}
    http.Handle("/api", api)
    http.ListenAndServe(":8080", nil)
}

Request 物件

http.Request 包含了請求的所有資訊:

func debugHandler(w http.ResponseWriter, r *http.Request) {
    // HTTP 方法
    fmt.Fprintf(w, "Method: %s\n", r.Method)

    // 請求路徑
    fmt.Fprintf(w, "Path: %s\n", r.URL.Path)

    // Query 參數
    fmt.Fprintf(w, "Query: %s\n", r.URL.RawQuery)

    // 取得特定 query 參數
    name := r.URL.Query().Get("name")
    fmt.Fprintf(w, "Name: %s\n", name)

    // 請求標頭
    fmt.Fprintf(w, "User-Agent: %s\n", r.Header.Get("User-Agent"))
    fmt.Fprintf(w, "Content-Type: %s\n", r.Header.Get("Content-Type"))

    // 遠端地址
    fmt.Fprintf(w, "Remote: %s\n", r.RemoteAddr)
}

讀取 Request Body

func bodyHandler(w http.ResponseWriter, r *http.Request) {
    // 讀取 body
    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Error reading body", http.StatusBadRequest)
        return
    }
    defer r.Body.Close()

    fmt.Fprintf(w, "Body: %s", string(body))
}

Response 物件

http.ResponseWriter 用來建構回應:

func responseHandler(w http.ResponseWriter, r *http.Request) {
    // 設定回應標頭
    w.Header().Set("Content-Type", "text/plain; charset=utf-8")
    w.Header().Set("X-Custom-Header", "my-value")

    // 設定狀態碼(必須在寫入 body 之前)
    w.WriteHeader(http.StatusOK)  // 200

    // 寫入回應 body
    w.Write([]byte("Hello!"))
}

常用的 HTTP 狀態碼

http.StatusOK                  // 200
http.StatusCreated             // 201
http.StatusNoContent           // 204
http.StatusBadRequest          // 400
http.StatusUnauthorized        // 401
http.StatusForbidden           // 403
http.StatusNotFound            // 404
http.StatusMethodNotAllowed    // 405
http.StatusInternalServerError // 500

快速回應錯誤

func handler(w http.ResponseWriter, r *http.Request) {
    // http.Error 會同時設定 Content-Type、狀態碼和 body
    http.Error(w, "Something went wrong", http.StatusInternalServerError)
}

路由

基本路由

mux := http.NewServeMux()

mux.HandleFunc("/", homeHandler)
mux.HandleFunc("/about", aboutHandler)
mux.HandleFunc("/contact", contactHandler)

http.ListenAndServe(":8080", mux)

Go 1.22+ 增強路由

Go 1.22 大幅增強了 ServeMux 的路由功能:

mux := http.NewServeMux()

// HTTP 方法限定
mux.HandleFunc("GET /users", listUsersHandler)
mux.HandleFunc("POST /users", createUserHandler)

// 路徑參數
mux.HandleFunc("GET /users/{id}", getUserHandler)
mux.HandleFunc("PUT /users/{id}", updateUserHandler)
mux.HandleFunc("DELETE /users/{id}", deleteUserHandler)

// 在 handler 中取得路徑參數
func getUserHandler(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    fmt.Fprintf(w, "User ID: %s", id)
}

這讓我們不需要第三方路由器就能建構 RESTful API。

根據 Method 分發

在 Go 1.22 之前,你需要手動檢查 HTTP 方法:

func usersHandler(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        // 列出使用者
    case http.MethodPost:
        // 建立使用者
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

Go 1.22+ 之後,直接在路由定義中指定方法更加簡潔。

JSON 回應

Web API 最常使用 JSON 格式。Go 的 encoding/json 標準庫讓 JSON 處理非常簡單。

回傳 JSON

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

func getUserHandler(w http.ResponseWriter, r *http.Request) {
    user := User{
        ID:    1,
        Name:  "Jack",
        Email: "[email protected]",
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}

JSON 輔助函式

建立一個通用的 JSON 回應函式,之後會經常用到:

func writeJSON(w http.ResponseWriter, status int, data any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func writeError(w http.ResponseWriter, status int, message string) {
    writeJSON(w, status, map[string]string{"error": message})
}

// 使用
func handler(w http.ResponseWriter, r *http.Request) {
    user := User{ID: 1, Name: "Jack", Email: "[email protected]"}
    writeJSON(w, http.StatusOK, user)
}

解析 JSON 請求

func createUserHandler(w http.ResponseWriter, r *http.Request) {
    var user User

    // 解碼 request body
    err := json.NewDecoder(r.Body).Decode(&user)
    if err != nil {
        writeError(w, http.StatusBadRequest, "Invalid JSON")
        return
    }

    // 簡單驗證
    if user.Name == "" {
        writeError(w, http.StatusBadRequest, "Name is required")
        return
    }

    // 假設建立成功,回傳帶有 ID 的使用者
    user.ID = 1
    writeJSON(w, http.StatusCreated, user)
}

靜態檔案服務

提供靜態檔案

// 提供 ./static 目錄下的檔案
fs := http.FileServer(http.Dir("./static"))
mux.Handle("/static/", http.StripPrefix("/static/", fs))

目錄結構:

project/
├── main.go
└── static/
    ├── style.css
    └── script.js

存取 http://localhost:8080/static/style.css 就會回傳 static/style.css 的內容。

完整範例:簡易 API Server

把以上所有概念整合起來:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "sync"
)

// --- 資料模型 ---

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

// --- 簡易記憶體儲存 ---

type BookStore struct {
    mu     sync.Mutex
    books  []Book
    nextID int
}

func NewBookStore() *BookStore {
    return &BookStore{nextID: 1}
}

func (s *BookStore) All() []Book {
    s.mu.Lock()
    defer s.mu.Unlock()
    return append([]Book{}, s.books...)
}

func (s *BookStore) Add(b Book) Book {
    s.mu.Lock()
    defer s.mu.Unlock()
    b.ID = s.nextID
    s.nextID++
    s.books = append(s.books, b)
    return b
}

// --- JSON 輔助函式 ---

func writeJSON(w http.ResponseWriter, status int, data any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func writeError(w http.ResponseWriter, status int, message string) {
    writeJSON(w, status, map[string]string{"error": message})
}

// --- Handler ---

func main() {
    store := NewBookStore()

    // 加入一些初始資料
    store.Add(Book{Title: "The Go Programming Language", Author: "Donovan & Kernighan"})
    store.Add(Book{Title: "Go in Action", Author: "Kennedy, Ketelsen & Martin"})

    mux := http.NewServeMux()

    // GET /books — 列出所有書籍
    mux.HandleFunc("GET /books", func(w http.ResponseWriter, r *http.Request) {
        books := store.All()
        writeJSON(w, http.StatusOK, books)
    })

    // POST /books — 新增書籍
    mux.HandleFunc("POST /books", func(w http.ResponseWriter, r *http.Request) {
        var book Book
        if err := json.NewDecoder(r.Body).Decode(&book); err != nil {
            writeError(w, http.StatusBadRequest, "Invalid JSON")
            return
        }

        if book.Title == "" || book.Author == "" {
            writeError(w, http.StatusBadRequest, "Title and author are required")
            return
        }

        created := store.Add(book)
        writeJSON(w, http.StatusCreated, created)
    })

    // GET / — 首頁
    mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
        writeJSON(w, http.StatusOK, map[string]string{
            "message": "Welcome to the Book API",
            "version": "1.0",
        })
    })

    addr := ":8080"
    fmt.Printf("Server starting on %s\n", addr)
    log.Fatal(http.ListenAndServe(addr, mux))
}

測試 API

# 取得首頁
curl http://localhost:8080/

# 列出所有書籍
curl http://localhost:8080/books

# 新增書籍
curl -X POST http://localhost:8080/books \
  -H "Content-Type: application/json" \
  -d '{"title":"Learning Go","author":"Jon Bodner"}'

# 再次列出(應該有 3 本)
curl http://localhost:8080/books

伺服器設定最佳實踐

在生產環境中,不建議直接使用 http.ListenAndServe,而是建立自訂的 http.Server

server := &http.Server{
    Addr:         ":8080",
    Handler:      mux,
    ReadTimeout:  15 * time.Second,
    WriteTimeout: 15 * time.Second,
    IdleTimeout:  60 * time.Second,
}

log.Fatal(server.ListenAndServe())

設定超時可以防止慢速攻擊(Slowloris)等安全問題。

下一步

我們已經建立了第一個能運作的 API Server!但程式碼目前都塞在一個檔案裡。下一篇我們將學習如何組織 Go Web 專案的目錄結構、使用中間件、以及更進階的路由技巧。