Go: Go HTTP 服务开发

最后更新:2026-08-26

Go 标准库的 net/http 包功能完备,无需第三方框架就能构建生产级 Web 服务。

当你的团队决定"不引入任何 Web framework,就用标准库"时,你能否像 Gin 或 Echo 一样写出清晰的路由和中间件?这节课你将掌握 Go HTTP 服务的全部核心技术。

1. 你将学到


2. 一个后端工程师的真实故事

(1) 痛点:一个简单的 API,选了 Gin,三个月后升级卡住

Alice 是后端团队的一员,她需要搭一个用户管理的 REST API:

"我用 Gin 框架写了 3 个路由:GET /users、POST /users、GET /users/:id。但三个月后 Go 1.22 发布了,标准库原生支持了方法和路径参数。现在我想去掉 Gin dependency,但所有 handler 签名都要改——gin.Context vs http.ResponseWriter。老板说'为了少一个依赖重构几百行代码,不值得'。"

她当时的选择:

GO
// Gin 依赖版本(三个月后想迁移)
r := gin.Default()
r.GET("/users", listUsers)               // gin.Context
r.POST("/users", createUser)             // gin.Context
r.GET("/users/:id", getUser)             // gin.Context
// 想迁到标准库?所有 handler 签名都要改!

(2) Go 1.22 的解法:标准库原生路由

GO
// 标准库版本(Go 1.22+,无需任何依赖)
package main

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

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

var users = []User{{ID: 1, Name: "Alice"}}

func main() {
    mux := http.NewServeMux()

    // Go 1.22 增强路由:method + 路径模式 + 路径参数
    mux.HandleFunc("GET /users", listUsers)
    mux.HandleFunc("POST /users", createUser)
    mux.HandleFunc("GET /users/{id}", getUser)

    log.Println("服务启动于 :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

// 标准 handler 签名:http.ResponseWriter + *http.Request
func listUsers(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(users)
}

func createUser(w http.ResponseWriter, r *http.Request) {
    var u User
    if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    u.ID = len(users) + 1
    users = append(users, u)
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(u)
}

func getUser(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id") // 路径参数!
    // 查找 user...
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(User{ID: 1, Name: "Alice"})
}

(3) 收益:Gin 风格 vs 标准库(Go 1.22)

特性 Gin(第三方) 标准库(Go < 1.22) 标准库(Go 1.22+)
路径参数 :id ❌ 需手动解析 {id}
方法路由 ❌ Handler 内判断 "GET /path"
JSON 响应 c.JSON() 手动设置 Header 手动设置 Header
依赖 1 个外部包 0 0
性能 略慢(反射) 原生 原生
💡 提示: Go 1.22 的 net/http 增强路由已经足够大多数 Web 项目使用。如果你不需要框架特有的功能(如自动绑定/验证、丰富的中间件生态),优先考虑标准库


3. HTTP 基础

▶ 示例:最简单的 HTTP 服务

GO
package main

import (
    "fmt"
    "log"
    "net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %s!", r.URL.Query().Get("name"))
}

func main() {
    http.HandleFunc("/hello", helloHandler)
    log.Println("服务启动于 :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}
▶ 试一试

测试:

BASH
$ curl "http://localhost:8080/hello?name=Alice"
Hello, Alice!

(1) 核心类型

类型 说明
http.ResponseWriter 写入 HTTP 响应的接口
*http.Request HTTP 请求,包含 URL/Header/Body/Form
http.Handler 接口:ServeHTTP(w, r)
http.HandlerFunc 函数适配器:将普通函数转为 Handler
http.ServeMux 路由复用器

(2) Handler 接口详解

GO
package main

import (
    "fmt"
    "log"
    "net/http"
)

// 方式 1:实现 Handler 接口
type Greeter struct {
    Greeting string
}

func (g *Greeter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "%s, %s!", g.Greeting, r.URL.Path[1:])
}

// 方式 2:HandlerFunc 适配器
func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %s!", r.URL.Query().Get("name"))
}

func main() {
    mux := http.NewServeMux()

    // 结构体 Handler
    mux.Handle("/greet", &Greeter{Greeting: "Welcome"})

    // 函数 Handler(HandlerFunc 自动转换)
    mux.HandleFunc("/hello", helloHandler)

    log.Print(http.ListenAndServe(":8080", mux))
}

4. Go 1.22 增强路由

▶ 示例:方法 + 路径模式 + 路径参数

GO 📖 仅展示
package main

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

type Item struct {
    ID    int     `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

var items = []Item{
    {ID: 1, Name: "Laptop", Price: 999.99},
    {ID: 2, Name: "Mouse", Price: 29.99},
}

func main() {
    mux := http.NewServeMux()

    // Go 1.22 方法 + 路径模式
    mux.HandleFunc("GET /items", listItems)
    mux.HandleFunc("POST /items", createItem)
    mux.HandleFunc("GET /items/{id}", getItem)
    mux.HandleFunc("PUT /items/{id}", updateItem)
    mux.HandleFunc("DELETE /items/{id}", deleteItem)

    // 通配符后缀:路径前缀匹配
    mux.HandleFunc("GET /items/{path...}", wildcardHandler)

    log.Print(http.ListenAndServe(":8080", mux))
}

func listItems(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(items)
}

func createItem(w http.ResponseWriter, r *http.Request) {
    var item Item
    if err := json.NewDecoder(r.Body).Decode(&item); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    item.ID = len(items) + 1
    items = append(items, item)
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(item)
}

func getItem(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    // 查找 item...
    _ = id
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(items[0])
}

func updateItem(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    _ = id
    // 更新逻辑...
    w.WriteHeader(http.StatusNoContent)
}

func deleteItem(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    _ = id
    // 删除逻辑...
    w.WriteHeader(http.StatusNoContent)
}

func wildcardHandler(w http.ResponseWriter, r *http.Request) {
    path := r.PathValue("path")
    w.Header().Set("Content-Type", "text/plain")
    http.Error(w, "Not found: "+path, http.StatusNotFound)
}
逻辑代码 61 行(超过 40 行限制,仅展示)

(1) Go 1.22 路由模式对比

模式 Go < 1.22 Go 1.22+ 示例
方法匹配 不支持(Handler 内 if 判断) 支持 "GET /items"
路径参数 不支持 {name} 语法 "GET /items/{id}"
通配符后缀 不支持 {path...} "GET /static/{file...}"
精确路径 /items "GET /items" 严格匹配 /items
前缀匹配 /items/ "GET /items/" 匹配 /items/...

▶ 示例:路由优先级

GO
package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    mux := http.NewServeMux()

    // 精确路径 > 前缀路径
    mux.HandleFunc("GET /items", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "items list")
    })
    mux.HandleFunc("GET /items/{id}", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "item %s\n", r.PathValue("id"))
    })
    mux.HandleFunc("GET /items/featured", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "featured items")
    })

    log.Print(http.ListenAndServe(":8080", mux))
}
▶ 试一试

测试:

BASH
$ curl localhost:8080/items
items list
$ curl localhost:8080/items/42
item 42
$ curl localhost:8080/items/featured
featured items    # 精确匹配优先于 {id} 通配
🔥 易错: 路由注册顺序不重要——Go 1.22 的路由匹配基于优先级规则(精确 > 前缀 > 通配),而不是注册顺序。更高优先级的路由会覆盖低优先级的匹配,即使后者先注册。


5. 请求与响应

▶ 示例:查询参数、表单、JSON

GO 📖 仅展示
package main

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

type Response struct {
    Method  string      `json:"method"`
    Path    string      `json:"path"`
    Query   interface{} `json:"query,omitempty"`
    Form    interface{} `json:"form,omitempty"`
    JSON    interface{} `json:"json,omitempty"`
}

func handler(w http.ResponseWriter, r *http.Request) {
    resp := Response{
        Method: r.Method,
        Path:   r.URL.Path,
    }

    // 查询参数
    if r.Method == http.MethodGet {
        resp.Query = r.URL.Query()
    }

    // 表单数据
    if r.Method == http.MethodPost {
        contentType := r.Header.Get("Content-Type")
        switch {
        case contentType == "application/x-www-form-urlencoded":
            r.ParseForm()
            resp.Form = r.Form
        case contentType == "application/json":
            var body interface{}
            json.NewDecoder(r.Body).Decode(&body)
            resp.JSON = body
        }
    }

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

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", handler)

    log.Print(http.ListenAndServe(":8080", mux))
}
逻辑代码 42 行(超过 40 行限制,仅展示)

▶ 示例:JSON 响应工具函数

GO
package main

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

// JSON 响应工具函数
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
    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})
}

type Product struct {
    ID    int     `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

func getProduct(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    // 模拟查找
    if id != "1" {
        writeError(w, http.StatusNotFound, "product not found")
        return
    }
    writeJSON(w, http.StatusOK, Product{ID: 1, Name: "Laptop", Price: 999.99})
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /products/{id}", getProduct)

    log.Print(http.ListenAndServe(":8080", mux))
}
▶ 试一试
100%
sequenceDiagram
    participant Client as HTTP 客户端
    participant Mux as ServeMux
    participant Handler as Handler
    
    Client->>Mux: GET /products/1
    Mux->>Mux: 路由匹配
    Mux->>Handler: ServeHTTP(w, r)
    Handler->>Handler: r.PathValue("id") → "1"
    Handler->>Handler: writeJSON(w, 200, product)
    Handler-->>Client: HTTP 200 + JSON body

(2) HTTP 状态码速查

代码 常量 用途
200 http.StatusOK 成功
201 http.StatusCreated 资源创建成功
204 http.StatusNoContent 成功但无响应体
400 http.StatusBadRequest 客户端请求错误
401 http.StatusUnauthorized 未认证
403 http.StatusForbidden 无权限
404 http.StatusNotFound 资源不存在
500 http.StatusInternalServerError 服务端内部错误

6. 静态文件服务

GO
package main

import (
    "log"
    "net/http"
)

func main() {
    mux := http.NewServeMux()

    // 静态文件服务:/static/ 前缀 → ./static/ 目录
    mux.Handle("GET /static/", http.StripPrefix("/static/",
        http.FileServer(http.Dir("./static"))))

    // 单文件
    mux.Handle("GET /favicon.ico", http.FileServer(http.Dir("./static")))

    log.Print(http.ListenAndServe(":8080", mux))
}

7. 完整示例:REST 风格笔记 API

GO
// notes_api.go
package main

import (
    "encoding/json"
    "log"
    "net/http"
    "strconv"
    "sync"
    "time"
)

// ---------- Model ----------

type Note struct {
    ID        int       `json:"id"`
    Title     string    `json:"title"`
    Content   string    `json:"content"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

// ---------- Store ----------

type NoteStore struct {
    mu    sync.RWMutex
    notes map[int]Note
    nextID int
}

func NewNoteStore() *NoteStore {
    return &NoteStore{
        notes:  make(map[int]Note),
        nextID: 1,
    }
}

func (s *NoteStore) Create(title, content string) Note {
    s.mu.Lock()
    defer s.mu.Unlock()
    n := Note{
        ID:        s.nextID,
        Title:     title,
        Content:   content,
        CreatedAt: time.Now(),
        UpdatedAt: time.Now(),
    }
    s.nextID++
    s.notes[n.ID] = n
    return n
}

func (s *NoteStore) List() []Note {
    s.mu.RLock()
    defer s.mu.RUnlock()
    result := make([]Note, 0, len(s.notes))
    for _, n := range s.notes {
        result = append(result, n)
    }
    return result
}

func (s *NoteStore) Get(id int) (Note, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    n, ok := s.notes[id]
    return n, ok
}

func (s *NoteStore) Update(id int, title, content string) (Note, bool) {
    s.mu.Lock()
    defer s.mu.Unlock()
    n, ok := s.notes[id]
    if !ok {
        return Note{}, false
    }
    n.Title = title
    n.Content = content
    n.UpdatedAt = time.Now()
    s.notes[id] = n
    return n, true
}

func (s *NoteStore) Delete(id int) bool {
    s.mu.Lock()
    defer s.mu.Unlock()
    _, ok := s.notes[id]
    if !ok {
        return false
    }
    delete(s.notes, id)
    return true
}

// ---------- API ----------

type NotesAPI struct {
    store *NoteStore
}

func NewNotesAPI(store *NoteStore) *NotesAPI {
    return &NotesAPI{store: store}
}

func (api *NotesAPI) Register(mux *http.ServeMux) {
    mux.HandleFunc("GET /notes", api.ListNotes)
    mux.HandleFunc("POST /notes", api.CreateNote)
    mux.HandleFunc("GET /notes/{id}", api.GetNote)
    mux.HandleFunc("PUT /notes/{id}", api.UpdateNote)
    mux.HandleFunc("DELETE /notes/{id}", api.DeleteNote)
}

// 工具函数
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
    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})
}

// ---------- Handlers ----------

func (api *NotesAPI) ListNotes(w http.ResponseWriter, r *http.Request) {
    notes := api.store.List()
    writeJSON(w, http.StatusOK, notes)
}

func (api *NotesAPI) CreateNote(w http.ResponseWriter, r *http.Request) {
    var input struct {
        Title   string `json:"title"`
        Content string `json:"content"`
    }
    if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON body")
        return
    }
    if input.Title == "" {
        writeError(w, http.StatusBadRequest, "title is required")
        return
    }

    note := api.store.Create(input.Title, input.Content)
    writeJSON(w, http.StatusCreated, note)
}

func (api *NotesAPI) GetNote(w http.ResponseWriter, r *http.Request) {
    idStr := r.PathValue("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid note ID")
        return
    }

    note, ok := api.store.Get(id)
    if !ok {
        writeError(w, http.StatusNotFound, "note not found")
        return
    }
    writeJSON(w, http.StatusOK, note)
}

func (api *NotesAPI) UpdateNote(w http.ResponseWriter, r *http.Request) {
    idStr := r.PathValue("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid note ID")
        return
    }

    var input struct {
        Title   string `json:"title"`
        Content string `json:"content"`
    }
    if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON body")
        return
    }

    note, ok := api.store.Update(id, input.Title, input.Content)
    if !ok {
        writeError(w, http.StatusNotFound, "note not found")
        return
    }
    writeJSON(w, http.StatusOK, note)
}

func (api *NotesAPI) DeleteNote(w http.ResponseWriter, r *http.Request) {
    idStr := r.PathValue("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid note ID")
        return
    }

    if !api.store.Delete(id) {
        writeError(w, http.StatusNotFound, "note not found")
        return
    }
    w.WriteHeader(http.StatusNoContent)
}

func main() {
    store := NewNoteStore()
    api := NewNotesAPI(store)

    mux := http.NewServeMux()
    api.Register(mux)

    log.Println("Note API 启动于 :8080")
    log.Println("可用端点:")
    log.Println("  GET    /notes       — 列出所有笔记")
    log.Println("  POST   /notes       — 创建笔记")
    log.Println("  GET    /notes/{id}  — 获取单个笔记")
    log.Println("  PUT    /notes/{id}  — 更新笔记")
    log.Println("  DELETE /notes/{id}  — 删除笔记")
    log.Fatal(http.ListenAndServe(":8080", mux))
}
🔥 易错: http.Error(w, msg, code) 不会设置 Content-Type 为 JSON。如果你返回 JSON error,使用 json.NewEncoder(w).Encode(errResp) 并手动设置 Header()。标准库的 http.Error() 返回纯文本。


❓ 常见问题

Q Go 1.22 的路由增强和 Gin 比如何?
A 大多数场景已足够。Go 1.22 支持方法匹配、路径参数、通配符。Gin 的额外价值在于请求绑定/验证、中间件生态、错误处理。如果你的项目不需要这些,标准库更轻量更安全(零依赖)。
Q Handler 和 HandlerFunc 什么区别?
A Handler 是接口(需要实现 ServeHTTP method),HandlerFunc 是函数类型适配器——它让普通函数自动满足 Handler interface。两者完全等价:mux.Handle("/path", handler)mux.HandleFunc("/path", handlerFunc) 效果相同。
Q 如何获取路径参数?
A Go 1.22+ 使用 r.PathValue("name") 获取 {name} 类型的路径参数。旧版本需要手动从 r.URL.Path 解析或使用第三方库。路径参数名必须与路由模式中的 {name} 一致。
Q ListenAndServe 和 ListenAndServeTLS 区别?
A 前者是 HTTP(:80),后者是 HTTPS(:443)需要提供证书和私钥文件。HTTPS 示例:http.ListenAndServeTLS(":443", "cert.pem", "key.pem", mux)
Q 如何优雅关闭 HTTP 服务?
Aserver.Shutdown(ctx) 配合 os/signal 捕获中断信号。Shutdown 会等待所有活跃连接处理完成后再关闭。不要用 server.Close()——它会强制中断正在处理的请求。
Q http.ListenAndServe 第二个参数传 nil 是什么意思?
A 传 nil 表示使用 http.DefaultServeMux(全局默认路由)。推荐显式创建 http.NewServeMux() 避免污染全局路由——尤其是在测试中需要隔离路由时。
Q ServeMux 支持子路由嵌套吗?
A 标准库的 ServeMux 不支持嵌套路由(子路由分组)。可以通过手动注册带公共前缀的路由实现类似效果,或使用第三方路由库如 chi(极轻量)实现嵌套。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建一个简单的 HTTP 服务,注册 3 个路由:GET /time 返回当前时间 JSON、GET /health 返回 {"status": "ok"}GET /version 返回版本号。使用 Go 1.22 增强路由语法。

  2. 进阶题(难度⭐⭐):实现一个待办事项 API(TODO List)。要求:(1) 完整的 CRUD 操作;(2) 使用 Go 1.22 方法和路径参数路由;(3) JSON 请求和响应;(4) 内存存储(map + RWMutex 保护);(5) 返回合适的 HTTP 状态码。

  3. 挑战题(难度⭐⭐⭐):实现一个短链接服务。要求:(1) POST /shorten 接收长 URL 返回短码(6 位随机字符串);(2) GET /{code} 301 重定向到原始 URL;(3) 访问统计:GET /stats/{code} 返回访问次数;(4) 用 -race 验证并发安全;(5) RWMutex 保护统计计数器。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏