2. Go 基础回顾:结构体、标签、接口与错误类型
在构建 REST API 服务时,结构体用于定义请求/响应模型,标签提供元数据(如字段名、校验规则),接口抽象行为,错误类型则统一处理异常。本章快速回顾这四个核心概念,为后续参数校验与错误处理打下基础。
2.1 结构体 (struct) 与标签 (tag)
结构体是 Go 中聚合不同类型字段的复合类型,常用于映射 JSON 请求体或数据库记录。标签(Tag)是附加在字段后的字符串,通过反射机制在运行时读取,广泛用于序列化、校验和 ORM 映射。
2.1.1 结构体定义与标签语法
type CreateUserRequest struct {
Username string `json:"username" validate:"required,min=3,max=20"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"gte=18,lte=120"`
Role string `json:"role" validate:"oneof=admin user"`
}
标签格式:key:"value",多个 key 用空格分隔。常用标签包括:
| 标签 Key | 用途 | 示例 |
|---|---|---|
json |
JSON 序列化/反序列化字段名 | json:"username" |
validate |
参数校验规则(如 validator 库) | validate:"required,email" |
form |
表单/URL 查询参数绑定 | form:"page" |
xml |
XML 序列化字段名 | xml:"item" |
gorm |
ORM 列名/约束 | gorm:"column:user_name" |
2.1.2 标签读取与反射
标签通过 reflect 包在运行时读取,这是校验库和序列化库的核心机制:
import "reflect"
func readTag(s interface{}) {
t := reflect.TypeOf(s).Elem()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
jsonTag := field.Tag.Get("json")
validateTag := field.Tag.Get("validate")
fmt.Printf("Field: %s, json: %s, validate: %s\n", field.Name, jsonTag, validateTag)
}
}
💡 提示: 标签中的 key 是大小写敏感的,通常使用小写。自定义标签 key 应避免与标准库冲突,建议使用项目前缀,如
myapp:"..."。
2.2 接口 (interface) 的概念
Go 的接口是隐式实现的——只要类型实现了接口的所有方法,就自动满足该接口。这种“鸭子类型”风格让代码更灵活,是依赖注入、中间件和策略模式的基础。
2.2.1 接口定义与实现
// 定义校验器接口
type Validator interface {
Validate() error
}
// 结构体实现接口
type User struct {
Name string
Email string
}
func (u User) Validate() error {
if u.Name == "" {
return errors.New("name is required")
}
if !strings.Contains(u.Email, "@") {
return errors.New("invalid email")
}
return nil
}
// 使用接口
func process(v Validator) error {
return v.Validate()
}
2.2.2 接口在 REST API 中的典型应用
- 校验器接口:所有请求 DTO 实现
Validate() error,统一校验入口。 - 存储层接口:定义
UserRepository接口,支持 mock 测试和切换实现。 - 中间件接口:
Handler或Middleware接口抽象 HTTP 处理逻辑。 - 错误处理接口:自定义错误类型实现
Error() string和状态码方法。
📌 关键点: 接口应保持小而专注(单一职责),推荐每个接口只包含 1-3 个方法。Go 标准库中的
io.Reader、io.Writer 就是最佳范例。
2.3 错误 (error) 类型基础
Go 的 error 是一个内置接口:
type error interface {
Error() string
}
任何实现了 Error() string 方法的类型都可以作为错误值。这是 Go 错误处理的核心抽象。
2.3.1 创建错误的常用方式
| 方式 | 代码示例 | 适用场景 |
|---|---|---|
errors.New |
errors.New("not found") |
简单静态错误消息 |
fmt.Errorf |
fmt.Errorf("user %d not found", id) |
带格式化参数的动态错误 |
| 自定义结构体 | type AppError struct { Code int; Msg string } |
携带额外上下文(状态码、内部错误) |
| 哨兵错误 | var ErrNotFound = errors.New("not found") |
用 == 比较的错误常量 |
2.3.2 自定义错误类型示例
type APIError struct {
HTTPStatus int `json:"-"`
Code string `json:"code"`
Message string `json:"message"`
Err error `json:"-"` // 内部原始错误
}
func (e *APIError) Error() string {
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
func (e *APIError) Unwrap() error {
return e.Err
}
// 使用
func findUser(id int) (*User, error) {
if id <= 0 {
return nil, &APIError{
HTTPStatus: 400,
Code: "INVALID_ID",
Message: "用户 ID 必须为正整数",
}
}
// ...
}
⚠️ 注意: 不要忽略错误!Go 中未处理的错误会导致不可预知的行为。在 REST API 中,每个可能返回 error 的调用都应被检查,并转换为统一的 API 错误响应。
2.4 核心概念关系图
下图展示了结构体、标签、接口和错误类型在 REST API 请求处理中的协作关系:
2.5 本章小结
- 结构体 + 标签:定义数据模型,标签提供元数据驱动校验和序列化。
- 接口:隐式实现,抽象行为(如校验、存储),便于测试和扩展。
- 错误类型:
error接口是基础,自定义错误可携带 HTTP 状态码、错误码等上下文。 - 三者结合:结构体承载数据 → 标签描述规则 → 接口定义行为 → 错误类型统一异常路径。
🎯 实践建议: 在项目初期就定义好基础的错误类型和校验接口,后续所有 handler 和 service 层统一使用,避免散落各处的
fmt.Errorf 和重复校验逻辑。