写 JSON API 时,校验是绕不开的。字段缺失、字符串太长、枚举值不合法、嵌套数组为空,这些都应该在进入业务逻辑前拦住。很多项目会使用校验库,这没问题;但初学者最好先理解不用框架时规则应该放在哪里、错误怎么组织。
本文用“创建项目”接口做例子,写一套轻量校验方式。目标不是造校验框架,而是让代码清楚、错误可读、测试容易。
请求结构
type CreateProjectRequest struct {
Name string `json:"name"`
Description string `json:"description"`
Visibility string `json:"visibility"`
Tags []string `json:"tags"`
}
规则:
- name 必填,长度 3 到 60
- description 最多 500
- visibility 只能是 private 或 public
- tags 最多 10 个,每个不超过 30
字段错误结构
type FieldError struct {
Field string `json:"field"`
Message string `json:"message"`
}
type ValidationErrors []FieldError
func (e ValidationErrors) Error() string {
return "validation failed"
}
校验函数返回一个错误列表:
func (r CreateProjectRequest) Validate() error {
var errs ValidationErrors
name := strings.TrimSpace(r.Name)
if name == "" {
errs = append(errs, FieldError{Field: "name", Message: "名称不能为空"})
} else if len([]rune(name)) < 3 || len([]rune(name)) > 60 {
errs = append(errs, FieldError{Field: "name", Message: "名称长度必须在 3 到 60 之间"})
}
if len([]rune(r.Description)) > 500 {
errs = append(errs, FieldError{Field: "description", Message: "描述不能超过 500 字"})
}
if r.Visibility != "private" && r.Visibility != "public" {
errs = append(errs, FieldError{Field: "visibility", Message: "可见性只能是 private 或 public"})
}
if len(r.Tags) > 10 {
errs = append(errs, FieldError{Field: "tags", Message: "标签最多 10 个"})
}
for i, tag := range r.Tags {
if strings.TrimSpace(tag) == "" {
errs = append(errs, FieldError{Field: fmt.Sprintf("tags[%d]", i), Message: "标签不能为空"})
}
if len([]rune(tag)) > 30 {
errs = append(errs, FieldError{Field: fmt.Sprintf("tags[%d]", i), Message: "标签不能超过 30 字"})
}
}
if len(errs) > 0 {
return errs
}
return nil
}
这里使用 []rune 计算中文长度,避免 len(string) 按字节计算导致中文长度不符合直觉。
Handler 中使用
func (h *ProjectHandler) Create(w http.ResponseWriter, r *http.Request) {
var req CreateProjectRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid_json", "JSON 格式不正确")
return
}
if err := req.Validate(); err != nil {
var validation ValidationErrors
if errors.As(err, &validation) {
writeJSON(w, http.StatusBadRequest, map[string]any{
"error": map[string]any{
"code": "validation_failed",
"fields": validation,
},
})
return
}
writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
return
}
// 调用 service
}
前端拿到字段错误后,可以把错误显示在对应表单项旁边。这比只返回“参数错误”有用得多。
规范化和校验分开
有些字段需要 trim 或去重。可以先规范化:
func (r *CreateProjectRequest) Normalize() {
r.Name = strings.TrimSpace(r.Name)
r.Visibility = strings.TrimSpace(strings.ToLower(r.Visibility))
for i := range r.Tags {
r.Tags[i] = strings.TrimSpace(r.Tags[i])
}
}
Handler:
req.Normalize()
if err := req.Validate(); err != nil {
// ...
}
规范化会改变输入值,校验只判断是否合法。两者分开,代码更容易理解。
测试校验规则
func TestCreateProjectRequestValidate(t *testing.T) {
tests := []struct {
name string
req CreateProjectRequest
wantErr bool
}{
{name: "valid", req: CreateProjectRequest{Name: "Go 工具", Visibility: "private"}},
{name: "missing name", req: CreateProjectRequest{Visibility: "private"}, wantErr: true},
{name: "bad visibility", req: CreateProjectRequest{Name: "Go 工具", Visibility: "team"}, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.req.Validate()
if tt.wantErr && err == nil {
t.Fatal("expected error")
}
if !tt.wantErr && err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
}
校验函数不依赖 HTTP,很容易测试。复杂校验也应该尽量放在这种普通函数里,而不是藏在 handler 深处。
小结
Go JSON 请求校验可以从简单函数开始。请求结构负责表达输入,Normalize 做清理,Validate 返回字段错误列表,handler 把它映射成统一 JSON 响应。必填、长度、枚举、数组元素都可以清楚写出来。
校验库能减少样板代码,但不能替你决定业务规则。先把规则写清楚,再考虑是否引入库。入门阶段理解这个边界,比记住某个框架标签更重要。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。