Go JSON 请求校验入门:不用框架也能写清楚规则

用创建项目 API 示例讲 Go JSON 请求校验:字段必填、长度限制、枚举值、嵌套结构和结构化错误响应。

写 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 响应。必填、长度、枚举、数组元素都可以清楚写出来。

校验库能减少样板代码,但不能替你决定业务规则。先把规则写清楚,再考虑是否引入库。入门阶段理解这个边界,比记住某个框架标签更重要。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页