不是所有 Web 请求都是 JSON。登录页、搜索框、后台表单、文件上传仍然会用表单。Go 标准库对表单解析支持很好,但几个 API 容易混:FormValue、PostFormValue、ParseForm、ParseMultipartForm。本文用登录表单讲清楚基本用法。
GET 查询参数
搜索接口通常用 query:
func search(w http.ResponseWriter, r *http.Request) {
keyword := r.URL.Query().Get("q")
page := r.URL.Query().Get("page")
_ = keyword
_ = page
}
GET 参数直接从 r.URL.Query() 读取,不需要 ParseForm。它只来自 URL,不会读取 body。
普通 POST 表单
HTML:
<form method="post" action="/login">
<input name="email">
<input name="password" type="password">
<button>登录</button>
</form>
Handler:
func login(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "bad form", http.StatusBadRequest)
return
}
email := r.PostForm.Get("email")
password := r.PostForm.Get("password")
_ = email
_ = password
}
PostForm 只包含 body 里的表单字段。Form 会合并 query 和 body。登录这种场景更推荐 PostForm,避免 URL query 覆盖或混入同名字段。
FormValue 的便利和代价
email := r.FormValue("email")
FormValue 会自动调用 ParseMultipartForm 或 ParseForm,并从 query 和 body 合并后的 Form 里取值。它很方便,但会隐藏错误。对于严肃表单,建议显式 ParseForm,检查错误,再读 PostForm。
小型搜索页用 FormValue 问题不大;登录、支付、配置修改这类接口,显式解析更清楚。
限制 body 大小
表单也要限制大小:
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
if err := r.ParseForm(); err != nil {
http.Error(w, "form too large", http.StatusBadRequest)
return
}
不要让客户端提交无限大的 body。即使只是登录表单,也应该有合理上限。
multipart 表单
文件上传用 multipart:
func upload(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 10<<20)
if err := r.ParseMultipartForm(10 << 20); err != nil {
http.Error(w, "bad multipart form", http.StatusBadRequest)
return
}
title := r.FormValue("title")
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "file required", http.StatusBadRequest)
return
}
defer file.Close()
_ = title
_ = header
}
ParseMultipartForm 可能使用临时文件。处理完成后可以清理:
defer func() {
if r.MultipartForm != nil {
_ = r.MultipartForm.RemoveAll()
}
}()
校验表单
type LoginForm struct {
Email string
Password string
}
func parseLoginForm(r *http.Request) (LoginForm, error) {
if err := r.ParseForm(); err != nil {
return LoginForm{}, err
}
form := LoginForm{
Email: strings.TrimSpace(r.PostForm.Get("email")),
Password: r.PostForm.Get("password"),
}
if form.Email == "" || form.Password == "" {
return LoginForm{}, errors.New("email and password are required")
}
return form, nil
}
把解析和校验放到函数里,handler 会更干净,测试也更容易写。
测试表单
func TestParseLoginForm(t *testing.T) {
body := strings.NewReader("email=a%40example.com&password=secret")
req := httptest.NewRequest(http.MethodPost, "/login", body)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
form, err := parseLoginForm(req)
if err != nil {
t.Fatal(err)
}
if form.Email != "a@example.com" {
t.Fatalf("email = %q", form.Email)
}
}
测试要设置正确 Content-Type,否则解析结果可能不符合预期。
多值字段
复选框和多选输入会提交多个同名字段。不要只用 Get:
roles := r.PostForm["roles"]
可以逐个校验:
for _, role := range roles {
if role != "admin" && role != "editor" && role != "viewer" {
return errors.New("invalid role")
}
}
表单字段和 JSON 一样需要白名单校验。前端隐藏字段、下拉选项都不能当作可信输入,用户完全可以自己构造请求。
CSRF 边界
浏览器表单会自动携带 Cookie,所以修改类表单通常还要考虑 CSRF 防护。Go 标准库不内置 CSRF 方案,但你应该知道它属于表单登录态的重要边界。内部工具可以依赖额外认证和 SameSite Cookie,公开站点则需要更完整的 token 机制。
小结
Go 表单解析并不复杂:GET query 用 r.URL.Query(),普通 POST 表单显式 ParseForm 后读 r.PostForm,文件上传用 ParseMultipartForm 和 FormFile。FormValue 很方便,但会隐藏解析错误,也会合并 query 和 body。
表单接口同样需要大小限制、字段校验和测试。不要因为它不是 JSON 就放松边界。HTTP 输入都不可信,解析清楚只是第一步。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。