Go Web 與網絡編程精華
知識來源:
- 《Building Web Apps with Go》
- 《Go API 編程》
- 《Go Web 編程》(Go Web Programming, Sau Sheong Chang)
- 《Go 網絡編程》(Network Programming with Go)
- 《Mastering Go Web Services》
- 《Go Web 編程》(2013, 謝孟軍/astaxie)
一、net/http 包深入(Handler 接口、ServeMux、Middleware 鏈)
1.1 Handler 接口——一切的核心
Go HTTP 處理的核心抽象只有一個接口:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
整個 net/http 的設計哲學都圍繞這個接口展開。路由器、中間件、靜態文件服務器——全部是 Handler 的實現。
HandlerFunc 適配器——函數也能當 Handler:
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
這是 Go 的經典設計模式:通過類型轉換,讓普通函數滿足接口。
// 以下兩種註冊方式完全等價
http.Handle("/hello", http.HandlerFunc(helloFunc))
http.HandleFunc("/hello", helloFunc)
自定義 Handler 結構體——攜帶依賴注入:
type UserHandler struct {
db *sql.DB
logger *slog.Logger
cache *redis.Client
}
func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 可以訪問 h.db, h.logger, h.cache
users, err := h.db.QueryContext(r.Context(), "SELECT ...")
// ...
}
// 註冊
mux.Handle("/users", &UserHandler{db: db, logger: logger, cache: cache})
1.2 ServeMux 路由機制
DefaultServeMux 與自定義 Mux:
// 使用 DefaultServeMux(不推薦,全局狀態,存在安全隱患)
http.HandleFunc("/api/users", usersHandler)
http.ListenAndServe(":8080", nil) // nil = DefaultServeMux
// 使用自定義 ServeMux(推薦)
mux := http.NewServeMux()
mux.HandleFunc("/api/users", usersHandler)
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
http.ListenAndServe(":8080", mux)
路由匹配規則:
- 精確匹配優先於前綴匹配
- 以 / 結尾的模式是子樹匹配(前綴匹配)
- 不以 / 結尾的模式是精確匹配
- 最長匹配優先
Go 1.22+ 增強路由——終於支持方法和路徑參數:
mux := http.NewServeMux()
// 方法限定
mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
// 路徑參數
mux.HandleFunc("GET /api/users/{id}", getUser)
mux.HandleFunc("DELETE /api/users/{id}", deleteUser)
// 通配符(匹配剩餘路徑)
mux.HandleFunc("GET /files/{path...}", serveFile)
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // 提取路徑參數
// ...
}
優先級規則(Go 1.22+):
- 更具體的模式優先(GET /users/{id} 優先於 /users/{id})
- 帶方法的優先於不帶方法的
- 如果兩個模式衝突且無法比較,註冊時 panic
1.3 自定義 Server 配置
生產環境必須使用自定義 http.Server:
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 15 * time.Second, // 讀取整個請求的超時
ReadHeaderTimeout: 5 * time.Second, // 讀取請求頭的超時
WriteTimeout: 15 * time.Second, // 寫響應的超時
IdleTimeout: 60 * time.Second, // Keep-Alive 空閒超時
MaxHeaderBytes: 1 << 20, // 請求頭最大 1MB
}
log.Fatal(server.ListenAndServe())
HTTPS / TLS 支持:
// 方式一:直接使用證書文件
server.ListenAndServeTLS("cert.pem", "key.pem")
// 方式二:自定義 TLS 配置
server.TLSConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
PreferServerCipherSuites: true,
}
server.ListenAndServeTLS("cert.pem", "key.pem")
// 方式三:Let's Encrypt 自動證書
import "golang.org/x/crypto/acme/autocert"
manager := &autocert.Manager{
Cache: autocert.DirCache("certs"),
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("example.com"),
}
server.TLSConfig = manager.TLSConfig()
server.ListenAndServeTLS("", "")
1.4 中間件模式
中間件是接收 Handler 並返回新 Handler 的函數,形成洋蔥模型:
type Middleware func(http.Handler) http.Handler
日誌中間件:
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// 包裝 ResponseWriter 以捕獲狀態碼
ww := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(ww, r)
slog.Info("request",
"method", r.Method,
"path", r.URL.Path,
"status", ww.statusCode,
"duration", time.Since(start),
"ip", r.RemoteAddr,
)
})
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
Recovery 中間件(panic 恢復):
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
slog.Error("panic recovered",
"error", err,
"stack", string(debug.Stack()),
)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
中間件鏈式組合:
func chainMiddleware(handler http.Handler, middlewares ...Middleware) http.Handler {
// 從後往前包裝,確保執行順序是從前到後
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
// 使用
finalHandler := chainMiddleware(
myHandler,
recoveryMiddleware, // 最外層,最先執行
loggingMiddleware,
corsMiddleware,
authMiddleware, // 最內層,最後執行
)
CORS 中間件:
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Max-Age", "86400")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
二、HTTP 服務器實現原理
2.1 啓動流程源碼分析
ListenAndServe 的完整調用鏈:
ListenAndServe(addr, handler)
-> net.Listen("tcp", addr) // 創建 TCP 監聽器
-> server.Serve(listener) // 進入主循環
-> for { listener.Accept() } // 循環接受連接
-> go conn.serve(ctx) // 每個連接一個 goroutine
-> serverHandler.ServeHTTP(w, r) // 調用 Handler
關鍵設計:每請求一個 goroutine。 Go 的 goroutine 非常輕量(初始棧僅 2KB),因此可以同時處理數萬併發連接,無需回調或線程池。
2.2 連接處理詳解
// 簡化版的 conn.serve 核心邏輯
func (c *conn) serve(ctx context.Context) {
defer c.close()
// TLS 握手(如果是 HTTPS)
if tlsConn, ok := c.rwc.(*tls.Conn); ok {
tlsConn.HandshakeContext(ctx)
}
for {
// 讀取請求(支持 HTTP/1.1 Keep-Alive,一個連接可復用多次)
w, err := c.readRequest(ctx)
if err != nil {
return
}
// 調用 Handler 處理請求
serverHandler{c.server}.ServeHTTP(w, w.req)
// 刷新響應
w.finishRequest()
// 檢查是否 Keep-Alive
if !w.shouldReuseConnection() {
return
}
}
}
2.3 ResponseWriter 的三層接口
// 基礎接口
type ResponseWriter interface {
Header() http.Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
}
// 實際實現還滿足以下可選接口:
// http.Flusher -> Flush() 方法,用於 SSE 等流式響應
// http.Hijacker -> Hijack() 方法,用於 WebSocket 升級
// http.Pusher -> Push() 方法,用於 HTTP/2 Server Push
// 檢測並使用 Flusher(SSE 場景)
if flusher, ok := w.(http.Flusher); ok {
fmt.Fprintf(w, "data: %s\n\n", message)
flusher.Flush()
}
// Hijack 接管連接(WebSocket 底層原理)
if hijacker, ok := w.(http.Hijacker); ok {
conn, bufrw, err := hijacker.Hijack()
// 此後可以直接操作底層 TCP 連接
}
2.4 Server-Sent Events (SSE) 實現
func sseHandler(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-r.Context().Done():
return
case t := <-ticker.C:
fmt.Fprintf(w, "event: time\ndata: %s\n\n", t.Format(time.RFC3339))
flusher.Flush()
}
}
}
三、RESTful API 設計與實現
3.1 資源設計與路由註冊
// CRUD 對應 HTTP 方法
// GET /api/v1/users -> 列表
// GET /api/v1/users/{id} -> 詳情
// POST /api/v1/users -> 創建
// PUT /api/v1/users/{id} -> 全量更新
// PATCH /api/v1/users/{id} -> 部分更新
// DELETE /api/v1/users/{id} -> 刪除
type UserAPI struct {
store UserStore
logger *slog.Logger
}
func (api *UserAPI) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET /api/v1/users", api.List)
mux.HandleFunc("GET /api/v1/users/{id}", api.Get)
mux.HandleFunc("POST /api/v1/users", api.Create)
mux.HandleFunc("PUT /api/v1/users/{id}", api.Update)
mux.HandleFunc("DELETE /api/v1/users/{id}", api.Delete)
}
3.2 統一響應格式
type APIResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
type PagedResponse struct {
Items interface{} `json:"items"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalCount int64 `json:"total_count"`
TotalPages int `json:"total_pages"`
}
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func success(w http.ResponseWriter, data interface{}) {
respondJSON(w, http.StatusOK, APIResponse{Code: 0, Message: "success", Data: data})
}
func errorResponse(w http.ResponseWriter, status int, msg string) {
respondJSON(w, status, APIResponse{Code: -1, Message: msg})
}
3.3 JSON 處理技巧
// 結構體 json tag
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"` // 空值不輸出
Password string `json:"-"` // 永遠不序列化
CreatedAt time.Time `json:"created_at"`
}
// 安全解碼:限制大小 + 拒絕未知字段
func decodeJSON(r *http.Request, v interface{}) error {
// 限制請求體大小 1MB
r.Body = http.MaxBytesReader(nil, r.Body, 1<<20)
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(v); err != nil {
return fmt.Errorf("invalid JSON: %w", err)
}
// 確保沒有多餘數據
if decoder.More() {
return errors.New("request body must only contain a single JSON object")
}
return nil
}
// 自定義 JSON 序列化
type UnixTime time.Time
func (t UnixTime) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Time(t).Unix())
}
func (t *UnixTime) UnmarshalJSON(data []byte) error {
var unix int64
if err := json.Unmarshal(data, &unix); err != nil {
return err
}
*t = UnixTime(time.Unix(unix, 0))
return nil
}
3.4 分頁、過濾與排序
type PaginationParams struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Sort string `json:"sort"`
Order string `json:"order"` // asc, desc
}
func parsePagination(r *http.Request) PaginationParams {
q := r.URL.Query()
page, _ := strconv.Atoi(q.Get("page"))
if page < 1 {
page = 1
}
pageSize, _ := strconv.Atoi(q.Get("page_size"))
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
sort := q.Get("sort")
order := q.Get("order")
if order != "asc" && order != "desc" {
order = "desc"
}
return PaginationParams{Page: page, PageSize: pageSize, Sort: sort, Order: order}
}
// SQL 中使用
// offset := (params.Page - 1) * params.PageSize
// query := fmt.Sprintf("SELECT * FROM users ORDER BY %s %s LIMIT ? OFFSET ?", sort, order)
3.5 請求驗證
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"`
Age int `json:"age"`
}
func (req CreateUserRequest) Validate() error {
var errs []string
if strings.TrimSpace(req.Name) == "" {
errs = append(errs, "name is required")
}
if !strings.Contains(req.Email, "@") {
errs = append(errs, "invalid email format")
}
if len(req.Password) < 8 {
errs = append(errs, "password must be at least 8 characters")
}
if req.Age < 0 || req.Age > 150 {
errs = append(errs, "age must be between 0 and 150")
}
if len(errs) > 0 {
return fmt.Errorf("validation failed: %s", strings.Join(errs, "; "))
}
return nil
}
// 也可用 go-playground/validator 做聲明式驗證
import "github.com/go-playground/validator/v10"
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=2,max=50"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8"`
Age int `json:"age" validate:"gte=0,lte=150"`
}
var validate = validator.New()
func (req CreateUserRequest) Validate() error {
return validate.Struct(req)
}
3.6 API 版本管理
// 方式一:URL 路徑版本(最常用)
mux.HandleFunc("GET /api/v1/users", v1ListUsers)
mux.HandleFunc("GET /api/v2/users", v2ListUsers)
// 方式二:請求頭版本
func versionMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
version := r.Header.Get("Accept-Version")
if version == "" {
version = "v1"
}
ctx := context.WithValue(r.Context(), "api-version", version)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
四、WebSocket 編程
4.1 基礎 WebSocket 服務
import "github.com/gorilla/websocket"
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
// 生產環境應嚴格檢查 Origin
origin := r.Header.Get("Origin")
return origin == "https://myapp.com"
},
}
func wsHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("Upgrade error:", err)
return
}
defer conn.Close()
// 設置讀取限制和超時
conn.SetReadLimit(512)
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
conn.SetPongHandler(func(string) error {
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err,
websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Printf("WebSocket error: %v", err)
}
break
}
log.Printf("Received: %s", message)
err = conn.WriteMessage(messageType, message)
if err != nil {
break
}
}
}
4.2 聊天室 Hub 模式
這是 WebSocket 應用的經典架構,來源於 gorilla/websocket 官方示例:
type Hub struct {
clients map[*Client]bool
broadcast chan []byte
register chan *Client
unregister chan *Client
mu sync.RWMutex
}
type Client struct {
hub *Hub
conn *websocket.Conn
send chan []byte
}
func newHub() *Hub {
return &Hub{
clients: make(map[*Client]bool),
broadcast: make(chan []byte, 256),
register: make(chan *Client),
unregister: make(chan *Client),
}
}
func (h *Hub) Run() {
for {
select {
case client := <-h.register:
h.clients[client] = true
case client := <-h.unregister:
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
case message := <-h.broadcast:
for client := range h.clients {
select {
case client.send <- message:
default:
close(client.send)
delete(h.clients, client)
}
}
}
}
}
// 客戶端讀取 goroutine
func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c
c.conn.Close()
}()
for {
_, message, err := c.conn.ReadMessage()
if err != nil {
break
}
c.hub.broadcast <- message
}
}
// 客戶端寫入 goroutine
func (c *Client) writePump() {
ticker := time.NewTicker(54 * time.Second)
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case message, ok := <-c.send:
if !ok {
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
c.conn.WriteMessage(websocket.TextMessage, message)
case <-ticker.C:
// 定時發送 Ping 保持連接
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
4.3 nhooyr/websocket(更現代的選擇)
import "nhooyr.io/websocket"
func wsHandler(w http.ResponseWriter, r *http.Request) {
conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
OriginPatterns: []string{"myapp.com"},
})
if err != nil {
return
}
defer conn.CloseNow()
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Minute)
defer cancel()
for {
typ, data, err := conn.Read(ctx)
if err != nil {
break
}
conn.Write(ctx, typ, data)
}
conn.Close(websocket.StatusNormalClosure, "")
}
五、TCP/UDP 網絡編程
5.1 TCP 服務器
func startTCPServer() {
listener, err := net.Listen("tcp", ":9000")
if err != nil {
log.Fatal(err)
}
defer listener.Close()
log.Println("TCP server listening on :9000")
for {
conn, err := listener.Accept()
if err != nil {
log.Println("Accept error:", err)
continue
}
go handleConnection(conn)
}
}
func handleConnection(conn net.Conn) {
defer conn.Close()
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
line := scanner.Text()
log.Printf("Received: %s from %s", line, conn.RemoteAddr())
fmt.Fprintf(conn, "Echo: %s\n", line)
// 每次收到數據後刷新超時
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
}
}
5.2 TCP 客戶端
func tcpClient() {
conn, err := net.DialTimeout("tcp", "localhost:9000", 5*time.Second)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
fmt.Fprintln(conn, "Hello, Server!")
response, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
log.Fatal(err)
}
fmt.Printf("Response: %s", response)
}
5.3 帶長度前綴的消息協議
TCP 是流協議,需要自行處理消息邊界。常見方案是長度前綴:
// 發送:4字節大端序長度前綴 + 消息體
func sendMessage(conn net.Conn, msg []byte) error {
length := uint32(len(msg))
buf := make([]byte, 4+len(msg))
binary.BigEndian.PutUint32(buf[:4], length)
copy(buf[4:], msg)
_, err := conn.Write(buf)
return err
}
// 接收
func recvMessage(conn net.Conn) ([]byte, error) {
header := make([]byte, 4)
if _, err := io.ReadFull(conn, header); err != nil {
return nil, err
}
length := binary.BigEndian.Uint32(header)
if length > 10<<20 { // 限制最大 10MB
return nil, fmt.Errorf("message too large: %d", length)
}
data := make([]byte, length)
if _, err := io.ReadFull(conn, data); err != nil {
return nil, err
}
return data, nil
}
5.4 UDP 編程
// UDP 服務器
func udpServer() {
addr, _ := net.ResolveUDPAddr("udp", ":9001")
conn, err := net.ListenUDP("udp", addr)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
buf := make([]byte, 1024)
for {
n, remoteAddr, err := conn.ReadFromUDP(buf)
if err != nil {
continue
}
log.Printf("From %s: %s", remoteAddr, buf[:n])
conn.WriteToUDP([]byte("ACK"), remoteAddr)
}
}
// UDP 客戶端
func udpClient() {
addr, _ := net.ResolveUDPAddr("udp", "localhost:9001")
conn, err := net.DialUDP("udp", nil, addr)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
conn.Write([]byte("Hello UDP"))
buf := make([]byte, 1024)
n, _ := conn.Read(buf)
fmt.Println("Response:", string(buf[:n]))
}
5.5 併發連接限制
func startLimitedServer(maxConns int) {
listener, _ := net.Listen("tcp", ":9000")
sem := make(chan struct{}, maxConns) // 信號量限制併發
for {
conn, err := listener.Accept()
if err != nil {
continue
}
sem <- struct{}{} // 獲取信號量
go func() {
defer func() { <-sem }() // 釋放信號量
handleConnection(conn)
}()
}
}
六、HTTP 客戶端與連接池管理
6.1 自定義 Transport(連接池核心)
transport := &http.Transport{
// 連接池配置
MaxIdleConns: 100, // 全局最大空閒連接數
MaxIdleConnsPerHost: 10, // 每個 Host 最大空閒連接數
MaxConnsPerHost: 100, // 每個 Host 最大連接數(含活躍+空閒)
IdleConnTimeout: 90 * time.Second, // 空閒連接超時回收
// 連接超時
DialContext: (&net.Dialer{
Timeout: 30 * time.Second, // TCP 連接超時
KeepAlive: 30 * time.Second, // TCP Keep-Alive 間隔
}).DialContext,
// TLS 握手超時
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
// 啓用 HTTP/2
ForceAttemptHTTP2: true,
}
client := &http.Client{
Timeout: 30 * time.Second, // 整個請求超時(含連接、發送、接收)
Transport: transport,
}
關鍵要點:
- http.DefaultClient 沒有超時,生產環境絕對不能用
- http.Transport 內部維護連接池,必須復用,不要每次請求新建
- MaxIdleConnsPerHost 默認值只有 2,高併發場景必須調大
- 一定要讀完並關閉 resp.Body,否則連接無法歸還連接池
6.2 構造請求
// 帶 Context 的請求(推薦)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.example.com/users", body)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("User-Agent", "MyApp/1.0")
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// 必須讀完 Body,即使不用,否則連接不會歸還連接池
if resp.StatusCode != http.StatusOK {
io.Copy(io.Discard, resp.Body)
return fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
6.3 帶重試與指數退避
func doWithRetry(client *http.Client, req *http.Request, maxRetries int) (*http.Response, error) {
var resp *http.Response
var err error
for attempt := 0; attempt <= maxRetries; attempt++ {
// 重要:需要克隆 Body,因為請求體只能讀一次
if req.GetBody != nil {
req.Body, _ = req.GetBody()
}
resp, err = client.Do(req)
if err == nil && resp.StatusCode < 500 {
return resp, nil
}
if resp != nil {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}
// 指數退避 + 抖動
backoff := time.Duration(1<<uint(attempt)) * time.Second
jitter := time.Duration(rand.Int63n(int64(backoff / 2)))
time.Sleep(backoff + jitter)
}
return resp, fmt.Errorf("failed after %d retries: %w", maxRetries, err)
}
6.4 連接池監控
// 獲取連接池狀態(用於 Prometheus 指標等)
func monitorTransport(t *http.Transport) {
ticker := time.NewTicker(10 * time.Second)
for range ticker.C {
// Go 沒有直接暴露連接池狀態的 API
// 可通過 CloseIdleConnections() 手動清理
// 或用 httptrace 包追蹤連接復用情況
}
}
// 使用 httptrace 追蹤連接行為
import "net/http/httptrace"
trace := &httptrace.ClientTrace{
GotConn: func(info httptrace.GotConnInfo) {
log.Printf("連接復用: %v, 空閒: %v, 空閒時長: %v",
info.Reused, info.WasIdle, info.IdleTime)
},
ConnectStart: func(network, addr string) {
log.Printf("開始建立連接: %s %s", network, addr)
},
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
七、Web 安全(CSRF、XSS、SQL 注入防護、JWT 認證)
7.1 CSRF 防護
原理: 跨站請求偽造利用瀏覽器自動攜帶 Cookie 的機制。防禦方式是在表單中嵌入一個服務器生成的隨機 Token,提交時驗證。
import "crypto/rand"
// 生成 CSRF Token
func generateCSRFToken() string {
b := make([]byte, 32)
rand.Read(b)
return base64.URLEncoding.EncodeToString(b)
}
// CSRF 中間件
func csrfMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// GET/HEAD/OPTIONS 請求不檢查
if r.Method == "GET" || r.Method == "HEAD" || r.Method == "OPTIONS" {
// 生成 Token 並設置 Cookie
token := generateCSRFToken()
http.SetCookie(w, &http.Cookie{
Name: "csrf_token",
Value: token,
Path: "/",
HttpOnly: false, // JS 需要讀取
SameSite: http.SameSiteStrictMode,
})
ctx := context.WithValue(r.Context(), "csrf_token", token)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
// POST/PUT/DELETE 請求驗證 Token
cookie, err := r.Cookie("csrf_token")
if err != nil {
http.Error(w, "CSRF token missing", http.StatusForbidden)
return
}
formToken := r.Header.Get("X-CSRF-Token")
if formToken == "" {
formToken = r.FormValue("csrf_token")
}
if !hmac.Equal([]byte(cookie.Value), []byte(formToken)) {
http.Error(w, "CSRF token mismatch", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
推薦使用 gorilla/csrf 庫:
import "github.com/gorilla/csrf"
csrfProtect := csrf.Protect(
[]byte("32-byte-long-auth-key-here!!!!!"),
csrf.Secure(true), // 僅 HTTPS
)
http.ListenAndServe(":8080", csrfProtect(mux))
7.2 XSS 防護
核心原則:永遠不要信任用戶輸入,輸出時必須轉義。
// 1. html/template 自動轉義(最重要的防線)
// html/template 會自動對 {{.}} 進行 HTML 轉義
// <script>alert('xss')</script> -> <script>alert('xss')</script>
// 2. 手動轉義
import "html"
safe := html.EscapeString(userInput)
// 3. 輸入清洗(使用 bluemonday)
import "github.com/microcosm-cc/bluemonday"
// 嚴格模式:去除所有 HTML 標籤
p := bluemonday.StrictPolicy()
clean := p.Sanitize(userInput)
// UGC 模式:允許安全標籤
p := bluemonday.UGCPolicy()
clean := p.Sanitize(userInput)
// 4. Content-Security-Policy 響應頭
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Security-Policy",
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
next.ServeHTTP(w, r)
})
}
7.3 SQL 注入防護
// 錯誤寫法:直接拼接 SQL(嚴重安全漏洞)
query := "SELECT * FROM users WHERE name = '" + username + "'"
// 攻擊者輸入: ' OR '1'='1
// 正確寫法:使用參數化查詢(預編譯語句)
// MySQL
row := db.QueryRow("SELECT id, name FROM users WHERE name = ?", username)
// PostgreSQL
row := db.QueryRow("SELECT id, name FROM users WHERE name = $1", username)
// 預編譯語句(高頻查詢推薦)
stmt, err := db.Prepare("SELECT id, name FROM users WHERE name = ? AND age > ?")
defer stmt.Close()
rows, err := stmt.Query(username, age)
// ORM 也要注意:避免原生 SQL 拼接
// GORM 正確方式
db.Where("name = ?", username).First(&user)
// GORM 錯誤方式
db.Where(fmt.Sprintf("name = '%s'", username)).First(&user) // 仍然有注入風險
7.4 JWT 認證完整實現
import "github.com/golang-jwt/jwt/v5"
var jwtSecret = []byte(os.Getenv("JWT_SECRET"))
type Claims struct {
UserID int64 `json:"user_id"`
Role string `json:"role"`
jwt.RegisteredClaims
}
// 生成 Access Token + Refresh Token
func generateTokenPair(userID int64, role string) (accessToken, refreshToken string, err error) {
// Access Token(短期,如 15 分鐘)
accessClaims := Claims{
UserID: userID,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "myapp",
},
}
accessToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims).SignedString(jwtSecret)
if err != nil {
return
}
// Refresh Token(長期,如 7 天)
refreshClaims := jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(7 * 24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Subject: strconv.FormatInt(userID, 10),
}
refreshToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims).SignedString(jwtSecret)
return
}
// 驗證 Token
func parseToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{},
func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return jwtSecret, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
// JWT 認證中間件
func jwtAuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if !strings.HasPrefix(authHeader, "Bearer ") {
http.Error(w, "Missing or invalid Authorization header", http.StatusUnauthorized)
return
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
claims, err := parseToken(tokenString)
if err != nil {
http.Error(w, "Invalid token: "+err.Error(), http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), "claims", claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// 基於角色的權限控制
func requireRole(role string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value("claims").(*Claims)
if claims.Role != role {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
7.5 密碼安全
import "golang.org/x/crypto/bcrypt"
func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
func checkPassword(hashedPassword, password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
return err == nil
}
7.6 請求限流
import "golang.org/x/time/rate"
// 全局限流
func rateLimitMiddleware(rps float64, burst int) Middleware {
limiter := rate.NewLimiter(rate.Limit(rps), burst)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}
// 按 IP 限流
type IPRateLimiter struct {
limiters sync.Map
rate rate.Limit
burst int
}
func (rl *IPRateLimiter) GetLimiter(ip string) *rate.Limiter {
if limiter, ok := rl.limiters.Load(ip); ok {
return limiter.(*rate.Limiter)
}
limiter := rate.NewLimiter(rl.rate, rl.burst)
rl.limiters.Store(ip, limiter)
return limiter
}
八、模板引擎(html/template)
8.1 基礎用法
// 解析並渲染模板
tmpl, err := template.ParseFiles("templates/index.html")
tmpl.Execute(w, data)
// 解析目錄下所有模板
tmpl, err := template.ParseGlob("templates/*.html")
tmpl.ExecuteTemplate(w, "index.html", data)
// 生產環境推薦:啓動時預解析,避免運行時文件讀取
var templates = template.Must(template.ParseGlob("templates/*.html"))
8.2 模板語法大全
<!-- 變量輸出(自動 HTML 轉義) -->
<h1>{{.Title}}</h1>
<!-- 條件判斷 -->
{{if .IsLoggedIn}}
<p>Welcome, {{.Username}}!</p>
{{else if .IsGuest}}
<p>Welcome, Guest!</p>
{{else}}
<a href="/login">Login</a>
{{end}}
<!-- 循環 -->
{{range .Items}}
<li>{{.Name}} - {{.Price}}</li>
{{else}}
<li>No items found.</li>
{{end}}
<!-- range 帶 index -->
{{range $index, $item := .Items}}
<li>{{$index}}: {{$item.Name}}</li>
{{end}}
<!-- 變量賦值 -->
{{$name := .User.Name}}
<p>{{$name}}</p>
<!-- 管道操作 -->
<p>{{.CreatedAt | formatDate}}</p>
<p>{{.Name | upper | printf "Hello, %s"}}</p>
<!-- 模板嵌套 -->
{{template "header" .}}
{{template "content" .}}
<!-- 使用 block 定義可覆蓋的塊 -->
{{block "sidebar" .}}
<p>Default sidebar content</p>
{{end}}
<!-- with 改變上下文 -->
{{with .User}}
<p>{{.Name}} ({{.Email}})</p>
{{end}}
8.3 自定義模板函數
funcMap := template.FuncMap{
"formatDate": func(t time.Time) string {
return t.Format("2006-01-02 15:04:05")
},
"upper": strings.ToUpper,
"truncate": func(s string, n int) string {
if len(s) > n {
return s[:n] + "..."
}
return s
},
"safeHTML": func(s string) template.HTML {
return template.HTML(s) // 注意:跳過轉義,僅用於可信內容
},
"dict": func(values ...interface{}) map[string]interface{} {
m := make(map[string]interface{})
for i := 0; i < len(values); i += 2 {
m[values[i].(string)] = values[i+1]
}
return m
},
}
// 必須在 ParseFiles 之前調用 Funcs
tmpl := template.New("").Funcs(funcMap)
tmpl = template.Must(tmpl.ParseGlob("templates/*.html"))
8.4 佈局模式(Layout / Base Template)
// templates/base.html
{{define "base"}}
<!DOCTYPE html>
<html>
<head>
<title>{{template "title" .}}</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<nav>{{template "nav" .}}</nav>
<main>{{template "content" .}}</main>
<footer>{{template "footer" .}}</footer>
</body>
</html>
{{end}}
// templates/home.html
{{define "title"}}Home Page{{end}}
{{define "content"}}
<h1>Welcome, {{.User.Name}}</h1>
{{end}}
// 渲染時指定 "base" 模板
func renderPage(w http.ResponseWriter, page string, data interface{}) {
tmpl := template.Must(template.ParseFiles(
"templates/base.html",
"templates/"+page+".html",
))
tmpl.ExecuteTemplate(w, "base", data)
}
九、靜態文件服務
9.1 FileServer
// 基礎方式
fs := http.FileServer(http.Dir("./static"))
mux.Handle("/static/", http.StripPrefix("/static/", fs))
// Go 1.16+ embed 嵌入靜態文件(部署時無需附帶文件目錄)
//go:embed static/*
var staticFiles embed.FS
mux.Handle("/static/", http.FileServer(http.FS(staticFiles)))
// 單頁應用(SPA)支持:找不到文件時返回 index.html
func spaHandler(staticFS http.FileSystem) http.Handler {
fileServer := http.FileServer(staticFS)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
// 嘗試打開文件
f, err := staticFS.Open(path)
if err != nil {
// 文件不存在,返回 index.html(由前端路由接管)
r.URL.Path = "/"
} else {
f.Close()
}
fileServer.ServeHTTP(w, r)
})
}
9.2 文件上傳
func uploadHandler(w http.ResponseWriter, r *http.Request) {
// 限制上傳大小
r.Body = http.MaxBytesReader(w, r.Body, 10<<20) // 10MB
err := r.ParseMultipartForm(10 << 20)
if err != nil {
http.Error(w, "File too large", http.StatusBadRequest)
return
}
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "Invalid file", http.StatusBadRequest)
return
}
defer file.Close()
// 驗證文件類型
buf := make([]byte, 512)
file.Read(buf)
contentType := http.DetectContentType(buf)
if contentType != "image/jpeg" && contentType != "image/png" {
http.Error(w, "Only JPEG and PNG allowed", http.StatusBadRequest)
return
}
file.Seek(0, io.SeekStart) // 重置讀取位置
// 生成安全文件名
ext := filepath.Ext(header.Filename)
newName := fmt.Sprintf("%s%s", uuid.New().String(), ext)
dst, err := os.Create(filepath.Join("./uploads", newName))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer dst.Close()
io.Copy(dst, file)
fmt.Fprintf(w, "Uploaded: %s (%d bytes)", newName, header.Size)
}
9.3 文件下載
func downloadHandler(w http.ResponseWriter, r *http.Request) {
filename := filepath.Base(r.URL.Query().Get("file")) // 安全處理:只取文件名
filePath := filepath.Join("./files", filename)
// 檢查文件是否存在
if _, err := os.Stat(filePath); os.IsNotExist(err) {
http.Error(w, "File not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Disposition", "attachment; filename="+filename)
w.Header().Set("Content-Type", "application/octet-stream")
http.ServeFile(w, r, filePath)
}
十、Session/Cookie 管理
10.1 Cookie 操作
// 設置 Cookie
func setSessionCookie(w http.ResponseWriter, sessionID string) {
http.SetCookie(w, &http.Cookie{
Name: "session_id",
Value: sessionID,
Path: "/",
HttpOnly: true, // JS 不可讀取(防 XSS)
Secure: true, // 僅 HTTPS 傳輸
SameSite: http.SameSiteLaxMode, // 防 CSRF
MaxAge: 86400, // 過期時間(秒)
})
}
// 讀取 Cookie
func getSessionID(r *http.Request) (string, error) {
cookie, err := r.Cookie("session_id")
if err != nil {
return "", err
}
return cookie.Value, nil
}
// 刪除 Cookie
func clearSessionCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: "session_id",
Value: "",
Path: "/",
MaxAge: -1, // 立即過期
})
}
10.2 服務端 Session 存儲
// 內存存儲(僅開發環境,不支持多實例部署)
type MemorySessionStore struct {
mu sync.RWMutex
sessions map[string]*Session
}
type Session struct {
ID string
Data map[string]interface{}
CreatedAt time.Time
ExpiresAt time.Time
}
func (store *MemorySessionStore) Get(id string) (*Session, bool) {
store.mu.RLock()
defer store.mu.RUnlock()
s, ok := store.sessions[id]
if !ok || time.Now().After(s.ExpiresAt) {
return nil, false
}
return s, true
}
func (store *MemorySessionStore) Create() *Session {
store.mu.Lock()
defer store.mu.Unlock()
id := generateSessionID()
s := &Session{
ID: id,
Data: make(map[string]interface{}),
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(24 * time.Hour),
}
store.sessions[id] = s
return s
}
func generateSessionID() string {
b := make([]byte, 32)
rand.Read(b)
return base64.URLEncoding.EncodeToString(b)
}
10.3 使用 gorilla/sessions(推薦)
import "github.com/gorilla/sessions"
// Cookie Store(Session 數據加密存儲在 Cookie 中)
var store = sessions.NewCookieStore([]byte("secret-key"))
// 或使用 Redis/數據庫後端
// var store = redistore.NewRediStore(10, "tcp", "localhost:6379", "", []byte("secret-key"))
func loginHandler(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "session-name")
// 認證邏輯...
session.Values["user_id"] = user.ID
session.Values["role"] = user.Role
session.Options.MaxAge = 86400 * 7 // 7 天
session.Save(r, w)
}
func protectedHandler(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "session-name")
userID, ok := session.Values["user_id"].(int64)
if !ok {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// 已認證,繼續處理...
}
func logoutHandler(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "session-name")
session.Options.MaxAge = -1 // 刪除 Session
session.Save(r, w)
http.Redirect(w, r, "/", http.StatusSeeOther)
}
10.4 Redis Session 存儲(生產環境推薦)
import "github.com/redis/go-redis/v9"
type RedisSessionStore struct {
client *redis.Client
prefix string
ttl time.Duration
}
func (s *RedisSessionStore) Get(ctx context.Context, sessionID string) (map[string]interface{}, error) {
data, err := s.client.Get(ctx, s.prefix+sessionID).Bytes()
if err == redis.Nil {
return nil, nil
}
var session map[string]interface{}
json.Unmarshal(data, &session)
return session, err
}
func (s *RedisSessionStore) Set(ctx context.Context, sessionID string, data map[string]interface{}) error {
b, _ := json.Marshal(data)
return s.client.Set(ctx, s.prefix+sessionID, b, s.ttl).Err()
}
func (s *RedisSessionStore) Delete(ctx context.Context, sessionID string) error {
return s.client.Del(ctx, s.prefix+sessionID).Err()
}
十一、Web 框架對比(Gin、Echo 等)
11.1 標準庫 vs 框架選型參考
| 特性 | net/http (1.22+) | Gin | Echo | Chi |
|---|---|---|---|---|
| 路由參數 | 支持 | 支持 | 支持 | 支持 |
| 方法路由 | 支持 | 支持 | 支持 | 支持 |
| 路由分組 | 不支持 | 支持 | 支持 | 支持 |
| 中間件 | 手動鏈 | 內置 | 內置 | 內置 |
| 參數綁定 | 手動 | 自動 | 自動 | 手動 |
| 驗證 | 手動 | 集成 | 集成 | 手動 |
| 性能 | 高 | 極高 | 極高 | 高 |
| 依賴 | 零 | 較少 | 較少 | 極少 |
| 學習曲線 | 低 | 低 | 低 | 低 |
選型建議:
- 小型項目/微服務:標準庫 net/http(Go 1.22+) 或 Chi
- 中大型 API 項目:Gin(生態最大、社區最活躍)
- 需要極致性能:Gin 或 Echo
- 偏好標準庫風格:Chi(完全兼容 net/http)
11.2 Gin 框架核心用法
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default() // 包含 Logger 和 Recovery 中間件
// 路由分組
api := r.Group("/api/v1")
api.Use(authMiddleware()) // 組級中間件
{
api.GET("/users", listUsers)
api.GET("/users/:id", getUser)
api.POST("/users", createUser)
api.PUT("/users/:id", updateUser)
api.DELETE("/users/:id", deleteUser)
}
r.Run(":8080")
}
// Handler 使用 gin.Context
func getUser(c *gin.Context) {
id := c.Param("id") // 路徑參數
name := c.Query("name") // 查詢參數
page := c.DefaultQuery("page", "1")
user, err := findUser(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": user})
}
// 參數綁定與驗證
type CreateUserInput struct {
Name string `json:"name" binding:"required,min=2,max=50"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
}
func createUser(c *gin.Context) {
var input CreateUserInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 創建用戶...
c.JSON(http.StatusCreated, gin.H{"data": user})
}
// Gin 中間件
func authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
claims, err := parseToken(strings.TrimPrefix(token, "Bearer "))
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
return
}
c.Set("user_id", claims.UserID) // 存入上下文
c.Next() // 繼續處理
}
}
11.3 Echo 框架核心用法
import "github.com/labstack/echo/v4"
func main() {
e := echo.New()
// 內置中間件
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20)))
// 路由分組
api := e.Group("/api/v1")
api.Use(jwtMiddleware)
api.GET("/users", listUsers)
api.GET("/users/:id", getUser)
api.POST("/users", createUser)
e.Logger.Fatal(e.Start(":8080"))
}
func getUser(c echo.Context) error {
id := c.Param("id")
user, err := findUser(id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, "user not found")
}
return c.JSON(http.StatusOK, user)
}
// 參數綁定
type CreateUserInput struct {
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
}
func createUser(c echo.Context) error {
var input CreateUserInput
if err := c.Bind(&input); err != nil {
return err
}
if err := c.Validate(&input); err != nil {
return err
}
// ...
return c.JSON(http.StatusCreated, user)
}
11.4 Chi 框架(標準庫兼容風格)
import "github.com/go-chi/chi/v5"
func main() {
r := chi.NewRouter()
// 中間件(標準 net/http 中間件完全兼容)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(60 * time.Second))
r.Route("/api/v1", func(r chi.Router) {
r.Use(jwtAuth) // 組級中間件
r.Route("/users", func(r chi.Router) {
r.Get("/", listUsers) // GET /api/v1/users
r.Post("/", createUser) // POST /api/v1/users
r.Route("/{id}", func(r chi.Router) {
r.Get("/", getUser) // GET /api/v1/users/{id}
r.Put("/", updateUser) // PUT /api/v1/users/{id}
r.Delete("/", deleteUser)
})
})
})
http.ListenAndServe(":8080", r)
}
// Handler 簽名完全是標準 net/http
func getUser(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
// ...
}
十二、Context 在 Web 開發中的應用
12.1 請求級 Context
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // 請求取消時自動 cancel
// 派生超時 Context
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
result, err := queryDatabase(ctx, "SELECT ...")
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
http.Error(w, "Request timeout", http.StatusGatewayTimeout)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
respondJSON(w, http.StatusOK, result)
}
12.2 在中間件中傳遞值
type contextKey string
const userKey contextKey = "user"
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, err := authenticate(r)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), userKey, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func profileHandler(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value(userKey).(*User)
respondJSON(w, http.StatusOK, user)
}
十三、RPC 與 gRPC
13.1 標準庫 net/rpc
type MathService struct{}
type Args struct {
A, B int
}
func (m *MathService) Add(args *Args, reply *int) error {
*reply = args.A + args.B
return nil
}
// 服務端
func startRPCServer() {
rpc.Register(new(MathService))
rpc.HandleHTTP()
listener, _ := net.Listen("tcp", ":1234")
http.Serve(listener, nil)
}
// 客戶端
func rpcClient() {
client, _ := rpc.DialHTTP("tcp", "localhost:1234")
var reply int
client.Call("MathService.Add", &Args{7, 8}, &reply)
fmt.Printf("7 + 8 = %d\n", reply) // 15
}
13.2 gRPC
// user.proto
syntax = "proto3";
package user;
service UserService {
rpc GetUser (GetUserRequest) returns (UserResponse);
rpc ListUsers (ListUsersRequest) returns (stream UserResponse);
}
message GetUserRequest { int64 id = 1; }
message UserResponse {
int64 id = 1;
string name = 2;
string email = 3;
}
// 服務端
type userServer struct {
pb.UnimplementedUserServiceServer
}
func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.UserResponse, error) {
return &pb.UserResponse{Id: req.Id, Name: "Alice", Email: "alice@example.com"}, nil
}
func main() {
lis, _ := net.Listen("tcp", ":50051")
s := grpc.NewServer()
pb.RegisterUserServiceServer(s, &userServer{})
s.Serve(lis)
}
十四、數據庫操作
14.1 database/sql
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
func initDB() (*sql.DB, error) {
db, err := sql.Open("mysql", "user:pass@tcp(localhost:3306)/dbname?parseTime=true")
if err != nil {
return nil, err
}
db.SetMaxOpenConns(25) // 最大打開連接數
db.SetMaxIdleConns(5) // 最大空閒連接數
db.SetConnMaxLifetime(5 * time.Minute) // 連接最大存活時間
return db, db.Ping()
}
// 查詢
var user User
err := db.QueryRowContext(ctx, "SELECT id, name FROM users WHERE id = ?", id).
Scan(&user.ID, &user.Name)
// 事務
tx, err := db.BeginTx(ctx, nil)
defer tx.Rollback()
tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromID)
tx.ExecContext(ctx, "UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toID)
err = tx.Commit()
十五、優雅關閉(Graceful Shutdown)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", homeHandler)
server := &http.Server{Addr: ":8080", Handler: mux}
go func() {
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("HTTP server error: %v", err)
}
}()
// 等待中斷信號
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server exited gracefully")
}
十六、DNS 與地址解析
// DNS 查詢
ips, _ := net.LookupIP("example.com")
names, _ := net.LookupAddr("93.184.216.34")
cname, _ := net.LookupCNAME("www.example.com")
mxRecords, _ := net.LookupMX("example.com")
// 解析地址
host, port, _ := net.SplitHostPort("localhost:8080")
addr, _ := net.ResolveTCPAddr("tcp", "localhost:8080")
十七、常用第三方庫速查
| 用途 | 庫名 | 說明 |
|---|---|---|
| HTTP 路由 | chi, gorilla/mux | 功能豐富的路由器 |
| Web 框架 | gin, echo, fiber | 高性能 Web 框架 |
| ORM | gorm, ent, sqlx | 數據庫 ORM |
| 驗證 | go-playground/validator | 結構體字段驗證 |
| 配置 | viper, envconfig | 配置管理 |
| 日誌 | slog, zap, zerolog | 結構化日誌 |
| JWT | golang-jwt/jwt | JWT 認證 |
| WebSocket | gorilla/websocket, nhooyr/websocket | WebSocket 支持 |
| HTTP 客戶端 | resty, go-retryablehttp | 增強版 HTTP 客戶端 |
| gRPC | google.golang.org/grpc | gRPC 框架 |
| Swagger | swaggo/swag | API 文檔生成 |
| CSRF | gorilla/csrf | CSRF 防護 |
| Session | gorilla/sessions | Session 管理 |
| HTML 清洗 | bluemonday | XSS 防護 |
| 限流 | x/time/rate, tollbooth | 請求限流 |
十八、項目結構最佳實踐
myapp/
├── cmd/
│ └── server/
│ └── main.go # 入口:組裝依賴、啓動服務
├── internal/
│ ├── handler/ # HTTP 處理器(Controller 層)
│ │ ├── user.go
│ │ └── middleware.go
│ ├── service/ # 業務邏輯(Service 層)
│ │ └── user.go
│ ├── repository/ # 數據訪問(DAO 層)
│ │ └── user.go
│ └── model/ # 數據模型
│ └── user.go
├── pkg/ # 可導出的公共包
│ ├── response/
│ └── validator/
├── api/ # API 定義(OpenAPI/Proto)
├── configs/ # 配置文件
├── migrations/ # 數據庫遷移
├── static/ # 靜態資源
├── templates/ # 模板文件
├── go.mod
└── go.sum
十九、速查備忘
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.StatusConflict // 409
http.StatusUnprocessableEntity // 422
http.StatusTooManyRequests // 429
http.StatusInternalServerError // 500
http.StatusBadGateway // 502
http.StatusServiceUnavailable // 503
net 包關鍵接口
type Conn interface {
Read(b []byte) (n int, err error)
Write(b []byte) (n int, err error)
Close() error
LocalAddr() Addr
RemoteAddr() Addr
SetDeadline(t time.Time) error
SetReadDeadline(t time.Time) error
SetWriteDeadline(t time.Time) error
}
type Listener interface {
Accept() (Conn, error)
Close() error
Addr() Addr
}
主題測試文章,只做測試使用。發佈者:Walker,轉轉請注明出處:https://walker-learn.xyz/archives/6741