使用Golang实现REST API中的DTO设计

使用Golang实现REST API中的DTO设计 朋友们,我正在使用 GoLang 创建一个 API,实际上是一组微服务。在这个创建过程中产生了一些疑问。

大家好:

在我的 API 中,我有一个结构体,它以 JSON 格式返回一组属性。问题是,根据调用的方法不同,这个结构体返回时只包含其部分属性,而不是全部。在这种情况下,是使用 DTO 吗?或者 Go 中是否有其他类型的解决方案来帮助解决这类问题?

提前感谢提供帮助的人。

我来自巴西。我的英语水平一般。

5 回复

我该如何实现?

更多关于使用Golang实现REST API中的DTO设计的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


是的。表情符号

使用 omitempty

“omitempty” 选项指定如果字段具有空值(定义为 false、0、nil 指针、nil 接口值以及任何空数组、切片、映射或字符串),则应从编码中省略该字段。

我不太明白你的问题是什么。你是想从响应中移除那些结构体字段为 nil 的空(JSON null)元素吗?

你是想返回

{
   "foo": 1
}

而不是

{
  "foo": 1,
  "bar": null
}

对于一个 bar = nilstruct 吗?

在Go中实现REST API时,处理不同端点返回不同字段的场景确实常见。使用DTO(Data Transfer Object)是标准做法,但Go中有几种具体实现方式:

1. 使用独立的结构体作为DTO

为每个端点创建专门的结构体:

// 领域模型
type User struct {
    ID        int       `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    Password  string    `json:"-"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

// DTO - 用户列表响应
type UserListResponse struct {
    ID        int       `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
}

// DTO - 用户详情响应
type UserDetailResponse struct {
    ID        int       `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"created_at"`
}

// 转换函数
func ToListResponse(user User) UserListResponse {
    return UserListResponse{
        ID:    user.ID,
        Name:  user.Name,
        Email: user.Email,
    }
}

func ToDetailResponse(user User) UserDetailResponse {
    return UserDetailResponse{
        ID:        user.ID,
        Name:      user.Name,
        Email:     user.Email,
        CreatedAt: user.CreatedAt,
    }
}

2. 使用嵌入和omitempty标签

type UserResponse struct {
    ID        int        `json:"id"`
    Name      string     `json:"name"`
    Email     *string    `json:"email,omitempty"`
    CreatedAt *time.Time `json:"created_at,omitempty"`
    UpdatedAt *time.Time `json:"updated_at,omitempty"`
}

// 列表端点
func GetUsersHandler(w http.ResponseWriter, r *http.Request) {
    users := []User{/* 从数据库获取 */}
    
    response := make([]UserResponse, len(users))
    for i, user := range users {
        response[i] = UserResponse{
            ID:    user.ID,
            Name:  user.Name,
            Email: &user.Email, // 包含email
            // 不包含CreatedAt和UpdatedAt
        }
    }
    
    json.NewEncoder(w).Encode(response)
}

// 详情端点
func GetUserDetailHandler(w http.ResponseWriter, r *http.Request) {
    user := User{/* 从数据库获取 */}
    
    response := UserResponse{
        ID:        user.ID,
        Name:      user.Name,
        Email:     &user.Email,
        CreatedAt: &user.CreatedAt, // 包含更多字段
        UpdatedAt: &user.UpdatedAt,
    }
    
    json.NewEncoder(w).Encode(response)
}

3. 使用接口和自定义MarshalJSON

type User struct {
    ID        int       `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    Password  string    `json:"-"`
    CreatedAt time.Time `json:"created_at"`
}

type UserView interface {
    ToResponse() interface{}
}

type ListView struct{ *User }
type DetailView struct{ *User }

func (lv ListView) ToResponse() interface{} {
    return struct {
        ID    int    `json:"id"`
        Name  string `json:"name"`
        Email string `json:"email"`
    }{
        ID:    lv.ID,
        Name:  lv.Name,
        Email: lv.Email,
    }
}

func (dv DetailView) ToResponse() interface{} {
    return struct {
        ID        int       `json:"id"`
        Name      string    `json:"name"`
        Email     string    `json:"email"`
        CreatedAt time.Time `json:"created_at"`
    }{
        ID:        dv.ID,
        Name:      dv.Name,
        Email:     dv.Email,
        CreatedAt: dv.CreatedAt,
    }
}

// 使用示例
func GetUserListView(users []User) []interface{} {
    response := make([]interface{}, len(users))
    for i, user := range users {
        response[i] = ListView{&user}.ToResponse()
    }
    return response
}

4. 使用函数选项模式

type UserResponse struct {
    ID        int        `json:"id"`
    Name      string     `json:"name"`
    Email     *string    `json:"email,omitempty"`
    CreatedAt *time.Time `json:"created_at,omitempty"`
}

type ResponseOption func(*UserResponse)

func WithEmail(email string) ResponseOption {
    return func(ur *UserResponse) {
        ur.Email = &email
    }
}

func WithCreatedAt(createdAt time.Time) ResponseOption {
    return func(ur *UserResponse) {
        ur.CreatedAt = &createdAt
    }
}

func NewUserResponse(id int, name string, opts ...ResponseOption) UserResponse {
    resp := UserResponse{
        ID:   id,
        Name: name,
    }
    
    for _, opt := range opts {
        opt(&resp)
    }
    
    return resp
}

// 使用示例
func GetUserList() []UserResponse {
    return []UserResponse{
        NewUserResponse(1, "João", WithEmail("joao@email.com")),
        NewUserResponse(2, "Maria", WithEmail("maria@email.com")),
    }
}

func GetUserDetail() UserResponse {
    return NewUserResponse(
        1,
        "João",
        WithEmail("joao@email.com"),
        WithCreatedAt(time.Now()),
    )
}

推荐方案

对于微服务架构,推荐第一种方案(独立结构体DTO),因为:

  • 类型安全明确
  • 易于维护和测试
  • 文档清晰
  • 避免空指针问题
// 实际使用示例
func GetUsersHandler(w http.ResponseWriter, r *http.Request) {
    users := repository.GetUsers()
    
    response := make([]UserListResponse, len(users))
    for i, user := range users {
        response[i] = ToListResponse(user)
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

func GetUserHandler(w http.ResponseWriter, r *http.Request) {
    id := chi.URLParam(r, "id")
    user := repository.GetUserByID(id)
    
    response := ToDetailResponse(user)
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

这种方法在Go微服务中很常见,能有效分离内部模型和API响应结构。

回到顶部