需要预先了解:


1. 标准库(二):JSON 与 HTTP 服务

encoding/json 和 net/http 是写服务端程序的两块基石。这一篇从"结构体怎么变成 JSON"讲到"一个带中间件、超时和优雅关闭的迷你 REST 服务",把 反射与 unsafe 里看到的 tag 机制落到实际用法上。


1.1 encoding/json 基础

两个函数覆盖 90% 的场景:

b, err := json.Marshal(v)        // 值 -> JSON 字节切片
err = json.Unmarshal(data, &v)   // JSON -> 值(必须传指针)
u := User{Name: "一撮灰", Score: 98.5, Created: time.Date(2026, 9, 9, 12, 0, 0, 0, time.UTC)}
b, _ := json.Marshal(u)
fmt.Println(string(b))
{"name":"一撮灰","score":"98.5","created":"2026-09-09T12:00:00Z","address":{"city":"上海"}}

需要带缩进的输出用 json.MarshalIndent(v, "", " ")。

Unmarshal 有几个行为需要记住:

  • 字段名匹配不区分大小写:JSON 里的 "AGE" 也能填到 Age 字段;
  • 未知字段被忽略(想报错要用 Decoder.DisallowUnknownFields);
  • JSON 里缺失的字段保持零值。
var u2 User
json.Unmarshal([]byte(`{"name":"灰灰","AGE":3,"score":"88.5","address":{"city":"北京"}}`), &u2)
fmt.Printf("%+v\n", u2)
{Name:灰灰 Age:3 ... Score:88.5 ... Address:{City:北京}}

1.2 字段名与 tag

默认的字段名就是 Go 字段名(Name 输出为 "Name"),这通常不是我们想要的。用 json tag 指定:

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

结构体与指针 里只讲了 tag 的语法,反射与 unsafe 里讲了 StructTag.Get 怎么解析它——encoding/json 就是在运行时读这些 tag 来决定字段名的。


1.3 tag 进阶

写法 含义
json:"name" 改名为 name
json:"-" 完全忽略该字段
json:"age,omitempty" 零值时省略(数值 0、空串、nil 切片等)
json:"score,string" 以字符串形式编码数值/布尔
json:",omitempty" 只加选项,不改名

实测效果:

type User struct {
	Name     string  `json:"name"`
	Age      int     `json:"age,omitempty"`      // 0 被省略
	Email    string  `json:"-"`                  // 完全不出现在 JSON 里
	Password string  `json:"password,omitempty"` // 空串被省略
	Score    float64 `json:"score,string"`       // 数字变字符串
	Created  time.Time `json:"created"`
	Address  Address `json:"address"`
	Tags     []string `json:"tags,omitempty"`
}
{"name":"一撮灰","score":"98.5","created":"2026-09-09T12:00:00Z","address":{"city":"上海"}}

omitempty 只对零值生效。想把 0 也保留下来,就不能加这个选项——这是前后端对接时常见的分歧点。

嵌套结构体会递归序列化。匿名嵌入的字段会被"提升"到外层:

type Base struct {
	ID int `json:"id"`
}

type Post struct {
	Base
	Title string `json:"title"`
}

Post 序列化的结果是 {"id":1,"title":"..."},ID 平铺在外层。如果嵌入字段和外层字段同名,外层优先。

nil 切片与空切片

json.Marshal(map[string]any{
	"list":  []int{1, 2, 3},
	"empty": []int{},
	"nil":   []int(nil),
})
{"empty":[],"list":[1,2,3],"nil":null}

前端期望数组时,nil 会变成 null 导致解析报错——接口返回列表时记得初始化为 []T{}(见 数组与切片)。


1.4 自定义序列化

实现 json.Marshaler / json.Unmarshaler 接口就能接管某个类型的编解码:

type Status int

func (s Status) MarshalJSON() ([]byte, error) {
	switch s {
	case StatusActive:
		return json.Marshal("active")
	case StatusBanned:
		return json.Marshal("banned")
	default:
		return json.Marshal("unknown")
	}
}

func (s *Status) UnmarshalJSON(data []byte) error {
	var str string
	if err := json.Unmarshal(data, &str); err != nil {
		return err
	}
	// ...
	return nil
}
a := Account{ID: 1, Status: StatusActive}
b, _ := json.Marshal(a)
fmt.Println(string(b)) // {"id":1,"status":"active"}

注意 UnmarshalJSON 必须用指针接收者,否则无法修改值。

json.RawMessage 延迟解析

有时候 JSON 里某个字段的具体类型取决于另一个字段:

var raw struct {
	Type string          `json:"type"`
	Data json.RawMessage `json:"data"`
}
json.Unmarshal([]byte(`{"type":"user","data":{"name":"延迟解析"}}`), &raw)

RawMessage 就是原始的字节切片,等到知道 Type 之后再解析 Data。


1.5 流式 API

Marshal/Unmarshal 处理的是整块字节。数据来自网络或文件时,用 Encoder/Decoder 直接接 io.Writer/io.Reader:

dec := json.NewDecoder(r)
for {
	var acc Account
	if err := dec.Decode(&acc); err != nil {
		break // io.EOF 表示结束
	}
	fmt.Println(acc)
}

Decoder 可以从一个连接里连续解出多个 JSON 值,这在长连接和流式日志场景里很有用。Encoder.Encode 会自动追加换行。


1.6 net/http 服务端

最小的服务:

http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "hello")
})
http.ListenAndServe(":8080", nil)

http.Handler 是一个只有一个方法的接口:

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

http.HandlerFunc 是一个适配器,让普通函数满足这个接口。自己实现 Handler 就是定义一个带 ServeHTTP 方法的结构体:

type App struct {
	store *Store
}

func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// 自己路由
}

路由(Go 1.22+)

http.ServeMux 从 1.22 起支持方法和路径参数:

mux := http.NewServeMux()
mux.HandleFunc("GET /users", listUsers)
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("GET /users/{id}", getUser)

路径参数用 r.PathValue("id") 取。模式里 {id} 匹配单个路径段,{path...} 匹配剩余部分。不写方法时匹配所有方法。


1.7 中间件

中间件的类型是 func(http.Handler) http.Handler,接收一个 Handler 返回一个新的 Handler,形成"洋葱"结构:

func logging(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		next.ServeHTTP(w, r)
		log.Printf("%s %s 耗时 %v", r.Method, r.URL.Path, time.Since(start))
	})
}
请求 -> logging -> withJSON -> mux -> handler
响应 <- logging <- withJSON <- mux <- handler

组合多个中间件:

handler := logging(withJSON(mux))

中间件适合放日志、认证、限流、恢复 panic、设置公共响应头等横切逻辑。恢复 panic 的中间件几乎是必需的——一个没被 recover 的 panic 会让整个进程退出(见 错误处理)。


1.8 请求处理

// 读取请求体,限制大小防止恶意请求
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))

// 查询参数
q := r.URL.Query().Get("keyword")

// 表单
r.ParseForm()
name := r.FormValue("name")

// 路径参数(1.22+)
id := r.PathValue("id")

// 返回 JSON
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)

顺序很重要:先设置 Header,再 WriteHeader,最后写 body。一旦开始写 body,Header 就定下来了。


1.9 服务端的错误处理

HTTP 服务里错误分两类,处理方式不同。

可预期的失败(参数非法、资源不存在)用状态码 + 统一格式的 JSON 响应表达:

writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name 不能为空"})
writeJSON(w, http.StatusNotFound, map[string]string{"error": "用户不存在"})

几条惯例:

  1. 4xx 表示调用方的问题,5xx 表示服务端的问题,不要都用 200 + 错误字段;
  2. 错误响应的字段名保持一致(比如统一用 error),方便客户端处理;
  3. 不要把内部错误(SQL 语句、文件路径、堆栈)直接返回给客户端,记录到日志即可;
  4. 内部错误用 %w 包装后向上传递,在最外层统一转换成响应(错误包装的用法见 错误处理)。

不可预期的 panic 必须在中间件里兜底,否则一个请求触发的 panic 会让整个进程退出:

func recoverer(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				log.Printf("panic: %v\n%s", err, debug.Stack())
				writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "服务器内部错误"})
			}
		}()
		next.ServeHTTP(w, r)
	})
}

recover 只能拦住同一个 goroutine 的 panic,所以这个中间件要放在最外层(见 错误处理)。


1.10 超时与优雅关闭

http.Server 的零值没有超时,慢连接会一直占着 goroutine。生产环境要显式设置:

srv := &http.Server{
	Addr:              ":8080",
	Handler:           handler,
	ReadHeaderTimeout: 5 * time.Second,
	ReadTimeout:       10 * time.Second,
	WriteTimeout:      10 * time.Second,
	IdleTimeout:       60 * time.Second,
}

优雅关闭:收到信号后停止接收新请求,等待正在处理的请求完成,再退出:

quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

<-quit

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

if err := srv.Shutdown(ctx); err != nil {
	log.Fatal(err)
}

Shutdown 返回后,所有活跃连接都已结束(或超过 5 秒被强制中断)。这一步在容器环境里尤其重要——Kubernetes 滚动更新时会给进程发 SIGTERM,不给它机会处理完在途请求就会丢数据。


1.11 客户端

client := &http.Client{Timeout: 3 * time.Second}

resp, err := client.Get("https://example.com")
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)

两点注意:

  1. resp.Body 必须关闭,否则连接无法复用,会泄漏文件描述符;
  2. Client.Timeout 与 context 的区别:Client.Timeout 覆盖整个请求(连接、重定向、读 body),context 更灵活,可以只限制某一阶段,也能在业务取消时立即中断:
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
resp, err := client.Do(req)

默认的 http.Client 会复用连接(keep-alive)。需要精细控制连接池时调整 Transport 的 MaxIdleConnsPerHost 等参数。


1.12 实战:迷你 REST 服务

把上面的内容拼起来,一个约 120 行的服务:

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

type Store struct {
	mu     sync.RWMutex
	users  map[int]User
	nextID int
}

func NewStore() *Store {
	return &Store{users: map[int]User{}, nextID: 1}
}

func (s *Store) List() []User {
	s.mu.RLock()
	defer s.mu.RUnlock()
	out := make([]User, 0, len(s.users)) // 初始化为空切片,避免输出 null
	for _, u := range s.users {
		out = append(out, u)
	}
	return out
}

func (s *Store) Get(id int) (User, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	u, ok := s.users[id]
	return u, ok
}

func (s *Store) Create(name string) User {
	s.mu.Lock()
	defer s.mu.Unlock()
	u := User{ID: s.nextID, Name: name}
	s.users[u.ID] = u
	s.nextID++
	return u
}

路由与处理器:

mux := http.NewServeMux()

mux.HandleFunc("GET /users", func(w http.ResponseWriter, r *http.Request) {
	writeJSON(w, http.StatusOK, store.List())
})

mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
	var id int
	fmt.Sscanf(r.PathValue("id"), "%d", &id)
	u, ok := store.Get(id)
	if !ok {
		writeJSON(w, http.StatusNotFound, map[string]string{"error": "用户不存在"})
		return
	}
	writeJSON(w, http.StatusOK, u)
})

mux.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		return
	}
	var req struct {
		Name string `json:"name"`
	}
	if err := json.Unmarshal(body, &req); err != nil || req.Name == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name 不能为空"})
		return
	}
	writeJSON(w, http.StatusCreated, store.Create(req.Name))
})

handler := logging(withJSON(mux))

启动、测试、优雅关闭:

srv := &http.Server{
	Addr:              "127.0.0.1:18080",
	Handler:           handler,
	ReadHeaderTimeout: 5 * time.Second,
	ReadTimeout:       10 * time.Second,
	WriteTimeout:      10 * time.Second,
	IdleTimeout:       60 * time.Second,
}

go func() {
	if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		log.Fatal(err)
	}
}()

用 http.Client 实测输出:

POST 状态: 201 响应: {"id":1,"name":"一撮灰"}

GET /users: [{"id":1,"name":"一撮灰"},{"id":2,"name":"灰灰"}]

GET /users/1: 200 {"id":1,"name":"一撮灰"}

GET /users/999: 404 {"error":"用户不存在"}

带 ctx 的请求状态: 200
服务已优雅关闭

这个骨架里包含了并发安全的存储(RWMutex)、中间件、路径参数、JSON 编解码、请求体限流、超时和优雅关闭——生产服务的结构基本就是这样。


1.13 练习

  1. 给迷你服务加上 PUT /users/{id} 和 DELETE /users/{id},注意处理"用户不存在"的返回码。
  2. 加一个 recover 中间件:在处理器里故意 panic,验证请求返回 500 而不是整个进程退出。
  3. 用 json.Decoder + DisallowUnknownFields 改造 POST /users,让请求里出现未知字段时返回 400。