10.1 最小 HTTP 服务器

package main

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

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // fmt.Fprintf() 将格式化后的字符串写入到指定输出流的函数
		fmt.Fprintf(w, "Hello, World!")
	})

	fmt.Println("服务器启动在 http://localhost:8080")
	// 用 log.Fatal 捕获错误,如果启动失败会打印错误信息并退出
	log.Fatal(http.ListenAndServe(":8080", nil))
}

运行:go run main.go,浏览器访问 http://localhost:8080 就能看到 "Hello, World!"。

对比 Node.js 原生 http

const http = require('http')
http.createServer((req, res) => {
    res.end('Hello, World!')
}).listen(8080)

Go 版本结构类似,但路由注册是声明式的http.HandleFunc),不用写 if-else 判断 URL。

解析两个核心参数

func handler(w http.ResponseWriter, r *http.Request) {
    // w:写入响应(ResponseWriter)
    // r:读取请求(Request)
}
  • http.ResponseWriter:往这里写响应内容(写头、写 body)
  • *http.Request:从这里读请求信息(方法、URL、Header、Body)

10.2 路由:HandleFunc 与 Handle

HandleFunc(最常用,函数式)

http.HandleFunc("/users", usersHandler)
http.HandleFunc("/posts", postsHandler)

func usersHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "用户列表")
}

路由匹配规则

http.HandleFunc("/api", handler1)      // 只匹配 /api
http.HandleFunc("/api/", handler2)     // 匹配 /api/ 开头的所有路径(带斜杠是子树)

⚠️ 标准库路由的局限

标准库不支持路径参数

// 想实现 /users/123 获取 ID=123 的用户
// 标准库做不到!只能写 /users/ 然后在 handler 里手动解析 r.URL.Path

这就是为什么需要框架(Gin、Echo、Chi)。框架提供:

  • 路径参数 /users/:id
  • 查询参数便捷访问
  • 中间件
  • JSON 绑定
  • 路由分组

下一章我们用 Gin,但先看标准库能做什么。


10.3 读取请求:方法、URL、Header、Body

func handler(w http.ResponseWriter, r *http.Request) {
    // 1. HTTP 方法
    method := r.Method          // "GET" / "POST" / "PUT" / "DELETE"

    // 2. URL 和路径
    path := r.URL.Path          // "/users"
    query := r.URL.Query()
    name := query.Get("name")   // 查询参数 ?name=xxx
    page := query.Get("page")   // ?page=1

    // 3. Header
    contentType := r.Header.Get("Content-Type")
    auth := r.Header.Get("Authorization")

    // 4. Body(POST 的 JSON 等)
    body, err := io.ReadAll(r.Body)
    defer r.Body.Close()
    if err != nil { ... }

    fmt.Println(string(body))
}

对比 Node.js Express

// Express
app.get('/users', (req, res) => {
    req.method           // 'GET'
    req.path             // '/users'
    req.query.name       // 查询参数
    req.params.id        // 路径参数
    req.headers['auth']  // 请求头
    req.body             // body(需要 body-parser)
})

// Go 标准库
// req.Method, req.URL.Path, req.URL.Query().Get()
// req.Header.Get()
// 手动读 req.Body

10.4 写 JSON 响应(Go API 核心)

API 返回 JSON 是最常见场景。Go 用 encoding/json 包:

type User struct {
    ID   int    `json:"id"`            // ← 结构体 tag:控制 JSON 字段名
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func userHandler(w http.ResponseWriter, r *http.Request) {
    user := User{ID: 1, Name: "张三", Age: 28}

    // 设置响应头
    w.Header().Set("Content-Type", "application/json")

    // 编码 JSON 并写入响应
    json.NewEncoder(w).Encode(user)
    // 或:
    // data, _ := json.Marshal(user)
    // w.Write(data)
}

访问后会得到:

{"id":1,"name":"张三","age":28}

结构体 Tag 详解

type User struct {
    ID       int    `json:"id"`                    // JSON 字段叫 id
    Name     string `json:"name"`                  // JSON 字段叫 name
    Email    string `json:"email,omitempty"`       // 空值时省略
    Password string `json:"-"`                     // 完全忽略,不输出
    Age      int    `json:"age,string"`            // 序列化成字符串
}

对比 JS:JS 的对象字段名就是 JSON 字段名,无需配置。Go 是静态类型,结构体字段名(如 Name)和 JSON 字段名(如 name)通过 tag 映射。


10.5 解析 JSON 请求体(POST/PUT)

type CreateUserRequest struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func createUserHandler(w http.ResponseWriter, r *http.Request) {
    // 只接受 POST
    if r.Method != "POST" {
        http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
        return
    }

    // 解析 JSON body
    var req CreateUserRequest
    err := json.NewDecoder(r.Body).Decode(&req)
    if err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }

    // 用 req.Name、req.Age ...
    fmt.Fprintf(w, "创建用户: %s, %d 岁", req.Name, req.Age)
}

对比 Express

// Express(需 body-parser 中间件)
app.post('/users', (req, res) => {
    const { name, age } = req.body   // 自动解析好的对象
})

// Go 标准库:手动 Decode
var req CreateUserRequest
json.NewDecoder(r.Body).Decode(&req)

10.6 完整的 CRUD API 示例

下面是一个内存版「用户管理 API」,演示 GET / POST / PUT / DELETE:

// GET    /users       获取所有用户
// GET    /users?id=1  获取单个用户(用 query 参数模拟)
// POST   /users       创建用户
// PUT    /users?id=1  更新用户
// DELETE /users?id=1  删除用户

完整代码在 code/ch10-httpapi/main.go强烈建议你运行它,用浏览器或 Postman 测试各个接口。

测试方法

启动后:

# 浏览器访问
http://localhost:8080/users

# 或用 curl(cmd 里)
curl http://localhost:8080/users
curl -X POST http://localhost:8080/users -H "Content-Type: application/json" -d "{\"name\":\"张三\",\"age\":28}"

10.7 Handler 接口(理解框架的基础)

第 06 章学了接口,这里看它在 HTTP 里怎么用。理解这一节,你就能看懂 Gin 等框架的底层原理。

10.7.1 Handler 接口是什么

http.Handler 是一个接口(第 06 章学的接口):

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

意思是:任何有 ServeHTTP 方法的类型,都能当 HTTP 处理器用。这就是 Go HTTP 框架的根基。

10.7.2 你一直在用的 HandleFunc 是语法糖

// 这两种写法等价
http.HandleFunc("/x", myFunc)
http.Handle("/x", http.HandlerFunc(myFunc))

HandleFunc 内部把普通函数包装成了 HandlerFunc 类型(它实现了 ServeHTTP 方法)。这就是为什么你一直能传函数给 HandleFunc——背后是接口实现。

10.7.3 自己实现 Handler 接口(实战)

什么时候你需要自己实现 Handler?写中间件、自定义路由器时。看个最小例子——一个「记录访问日志」的 Handler:

// 自己定义一个类型,实现 Handler 接口
type LogHandler struct {
    next http.Handler   // 包装下一个 handler(中间件模式)
}

// 实现 ServeHTTP 方法 → 满足 http.Handler 接口
func (h *LogHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    h.next.ServeHTTP(w, r)    // 调用真正的处理逻辑
    log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
}

func main() {
    // 真正的业务 handler
    hello := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("hello"))
    })

    // 用 LogHandler 包装它
    http.Handle("/", &LogHandler{next: hello})

    http.ListenAndServe(":8080", nil)
}
// 访问 / 后,控制台打印:GET / 12.3µs

这就是中间件的原理——一个 Handler 包装另一个 Handler,在调用前后加自己的逻辑。第 11 章的 Gin 中间件、各种 Web 框架,底层都是这个套路。

10.7.4 为什么理解这个很重要

框架(Gin)本质上:Gin 的 gin.Engine 就是实现了 http.Handler 接口(有自己的 ServeHTTP 方法,内部做路由分发)。所以你可以这样把 Gin 嵌进标准库:

r := gin.Default()
// ... 注册路由
http.ListenAndServe(":8080", r)   // r 本身就是 http.Handler

理解了「框架 = 自己实现的 Handler」,下一章学 Gin 会非常顺。


10.8 优雅关闭服务器

本节涉及信号处理和 context(第 08 章 8.8 节学的)。如果你跳读了,先去看 8.8 的 context 专题。

10.8.1 先搞懂:为什么要「优雅关闭」

假设你的服务器正在处理一个耗时 2 秒的订单请求(写数据库、发邮件)。这时候你按了 Ctrl+C 想重启服务,会发生什么?

  • 直接关闭(粗暴):正在处理的请求立刻中断,可能钱扣了但订单没建完,数据不一致
  • 优雅关闭(平滑):先停止接收新请求,给正在处理的请求一段缓冲时间处理完,然后再退出

生产环境必须用优雅关闭,否则会产生大量「半成品」请求。

10.8.2 最小可用版(先理解思路)

srv := &http.Server{Addr: ":8080", Handler: mux}

// 1. 把「启动服务」放 goroutine 里跑(不阻塞 main)
go func() {
    if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
        log.Fatal(err)   // 真正的启动错误才退出(ErrServerClosed 是正常关闭信号)
    }
}()

// 2. main 在这里等,直到收到 Ctrl+C
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)   // 监听 Ctrl+C(SIGINT 信号)
<-stop                              // 阻塞,直到收到信号
fmt.Println("收到关闭信号,开始优雅关闭...")

// 3. 调 Shutdown:停止接收新请求,等现有请求处理完
if err := srv.Shutdown(context.Background()); err != nil {
    log.Fatal(err)
}
fmt.Println("服务器已关闭")

三个关键点

  1. signal.Notify(stop, os.Interrupt):监听 Ctrl+C 信号,收到时往 stop channel 发一个值
  2. <-stop:main 在这里阻塞等待,收到信号才往下走
  3. srv.Shutdown(ctx):停止接新请求,等在处理的请求完成。会阻塞直到处理完或超时

10.8.3 加超时版(生产推荐)

上面用的是 context.Background()(无限等待),万一有个请求卡死,服务永远关不掉。生产环境加个超时:

// 收到关闭信号后,给最多 10 秒处理剩余请求
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
    log.Println("超时强制关闭:", err)   // 10 秒还没处理完,强制关
}

这里 context.WithTimeout 的作用就是「最多等 10 秒」。10 秒内请求处理完就正常关,10 秒还没完就强制关(参考第 08 章 8.8 节 context 超时控制)。

10.8.4 完整生产版代码

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(2 * time.Second)   // 模拟耗时请求
        w.Write([]byte("done"))
    })

    srv := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    // 启动服务(非阻塞)
    go func() {
        log.Println("服务器启动在 :8080")
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("启动失败: %v", err)
        }
    }()

    // 等待中断信号(Ctrl+C 或 kill)
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, os.Interrupt, syscall.SIGTERM)   // 监听两种信号
    <-quit
    log.Println("正在关闭服务器...")

    // 10 秒内处理完现有请求
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        log.Printf("强制关闭: %v\n", err)
    }
    log.Println("服务器已退出")
}

测试:启动后,访问 http://localhost:8080(请求会处理 2 秒),然后立即按 Ctrl+C。你会发现服务器没有立刻退出,而是等那个请求处理完返回 done 后才退出——这就是优雅关闭。

10.8.5 术语速查

概念 是什么
os.Signal 操作系统的信号(Ctrl+C 是 SIGINT,kill 默认是 SIGTERM)
signal.Notify 注册信号监听,收到信号时往 channel 发
http.ErrServerClosed Shutdown 后 ListenAndServe 返回的错误,表示正常关闭,不是真错误
srv.Shutdown(ctx) 优雅关闭:停止接新请求,等现有请求完成(受 ctx 超时控制)

10.9 标准库的局限(为什么要用框架?)

用标准库写 5 个接口没问题,但写到 50 个接口时会遇到:

痛点 标准库 框架(Gin)
路径参数 /users/:id 手动解析 c.Param("id")
查询参数 r.URL.Query().Get() c.Query("name")
JSON 绑定 手动 Decode c.ShouldBindJSON(&req)
中间件 自己实现 内置支持
路由分组 自己实现 router.Group("/api")
错误处理 手动 统一中间件
参数校验 手动 内置 validator

下一章的 Gin 会优雅地解决这些问题。


10.10 Node.js / PHP ↔ Go 对照表

概念 Node.js / PHP Go 备注
HTTP 服务器 http 模块 / nginx net/http Go 标准库很完整
路由注册 app.get/post http.HandleFunc
请求对象 req / $_REQUEST *http.Request
响应对象 res / echo http.ResponseWriter
查询参数 req.query r.URL.Query().Get()
请求体 req.body io.ReadAll(r.Body)
返回 JSON res.json() json.NewEncoder(w).Encode()
状态码 res.status(404) w.WriteHeader(404)
设置头 res.set() w.Header().Set()
监听端口 .listen(8080) http.ListenAndServe(":8080")
JSON 字段名 对象属性名 struct tag Go 特有