在Go语言里搭建JSON API接口,核心就是处理好HTTP请求体的读取、结构体与JSON的映射,以及响应数据的序列化。标准库已经提供了足够多的工具,不需要引入外部框架也能写出清晰可靠的接口逻辑。

一、定义请求与响应的数据结构
在解析JSON之前,先要明确接口收发的数据形状。Go里通常使用结构体来约束字段,借助json标签控制序列化和反序列化的名称映射。比如一个用户注册的接口,客户端会传用户名、邮箱和年龄,服务端要返回用户ID和创建结果。
使用结构体而不是裸map[string]interface{}的好处是,编译器能帮我们检查字段类型,避免运行时才发现的类型错误。下面的代码展示了基础结构定义,其中Age使用了omitempty,这样当客户端不传年龄时,响应里就不会出现零值字段。
package main
import "encoding/json"
// 客户端请求结构
type RegisterRequest struct {
Username string `json:"username"`
Email string `json:"email"`
Age int `json:"age,omitempty"`
}
// 服务端响应结构
type RegisterResponse struct {
UserID int `json:"user_id"`
Msg string `json:"msg"`
}
二、解析客户端JSON请求体
接收到HTTP请求后,不能直接把r.Body当成字符串处理。正确做法是用json.NewDecoder从请求体流中解码到结构体指针。这样既能处理较大的JSON,也能在解析出错时给出明确的位置信息。
需要注意,如果客户端传来的JSON字段类型不对,比如把age写成字符串,Decode会返回错误。这时候应当返回四百状态码,而不是让程序崩溃。下面的处理函数演示了完整的解析与错误分支,其中我们显式调用了json.NewDecoder(r.Body).Decode,并在失败时写回标准错误报文。
func registerHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "only POST allowed", http.StatusMethodNotAllowed)
return
}
var req RegisterRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "invalid json format"})
return
}
// 这里可加入业务逻辑,如写数据库
resp := RegisterResponse{UserID: 1001, Msg: "success"}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
三、返回JSON响应数据
响应阶段使用json.NewEncoder(w).Encode(data)可以直接把结构体写成JSON并刷入响应体,它比先json.Marshal再w.Write更简洁,也少一次内存拷贝。设置Content-Type头为application/json是规范做法,能让前端正确识别。
如果返回前想对数据做过滤,例如隐藏内部字段,可以定义专用的响应结构体,或者使用json:"-"标签屏蔽某些字段。下面的例子展示了一个查询接口,把用户密码字段屏蔽掉,只暴露安全信息,避免敏感数据泄露。
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Password string `json:"-"`
}
func getUserHandler(w http.ResponseWriter, r *http.Request) {
u := User{ID: 1, Name: "alice", Password: "secret"}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(u)
}
四、结构体与map解析方式对比
除了结构体,有些动态接口会用map[string]interface{}接收JSON。这种方式灵活,适合字段不固定的Webhook场景,但缺点是无法在编译期发现拼写错误,且取值时要做大量类型断言,代码可读性下降。
下面的表格列出两者的主要差异,方便在项目中做取舍。对于业务边界清晰的API,推荐用结构体;对于中转代理或插件式数据,map更合适。
| 对比维度 | 结构体解析 | map解析 |
|---|---|---|
| 类型安全 | 编译期检查 | 运行时断言 |
| 字段约束 | 强约束 | 弱约束 |
| 适用场景 | 固定业务接口 | 动态数据透传 |
五、常见错误与规避办法
新手常犯的一个错误是在Decode之前手动ioutil.ReadAll再Unmarshal,这会增加一次内存分配,而且容易忘记关闭或读完Body。直接用Decoder读流更省心。另外,结构体字段如果首字母小写,json包是无法访问的,会导致解析出来全是零值。
还有一个坑是返回错误时也应当保证是合法JSON。有些代码在出错时直接http.Error写纯文本,前端按JSON解析就会抛异常。统一用json.NewEncoder输出错误对象,能让前后端契约保持一致,降低联调成本。
// 错误示范:返回纯文本
// http.Error(w, "bad", 400)
// 正确示范:返回JSON错误
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "bad request"})