Golang代码结构设计:哪种方法更优?

Golang代码结构设计:哪种方法更优? 大家好,

我正在开发一个Go代码API,其主要任务是从Cassandra数据库中获取数据,并进行某种形式的数据转换。但我对于采用哪种方法来实现这一目标感到困惑。

深入探讨我的代码库。 我遵循三层架构:存储层、服务层和HTTP层,其中: 存储层:负责从数据库检索数据 服务层:处理应用于验证的业务规则 HTTP层:从服务层获取数据并将其交付给客户端

我有一个名为models的目录,用于存储所有正在使用的Go结构体。

我的代码目录结构大致如下:

stores
  product.go
services
  product.go
http
  product.go
models
  product.go

现在解释一下问题:

我从数据库获取的数据结构大致如下:

type ProductModel struct{
  Name string
  Code string
  Department CodeNamePair
  Fact CodeNamePair
}

type CodeNamePair struct{
  Name string
  Code string
}

我希望得到的输出结构应该是:

type ProductResponse struct{
  Name string
  Code string
  DepartmentCode string
  DepartmentName string
  FactCode string
  FactName string
}

目前我遇到了两种在技术上正确且都能工作的方法。但对于其中一些相互冲突的观点,我需要得出结论并最终确定。以下是两种方法:

  1. 在ProductModel结构体上实现方法: 在models包中,我会编写一个名为transform的方法,将数据库结构体转换为响应结构体。类似这样:

    func (p *Product) Transform() ProductResponse {
      return ProductResponse{
        Name:p.Name
        Code:p.Code
        DepartmentCode:p.Department.Code
        DepartmentName:p.Department.Name
        FactCode:p.Fact.Code
        FactName:p.Fact.Name
      }
    }
    

    优点: a. 代码看起来更清晰、更易读,因为结构体和方法是紧密结合的。

    缺点: a. 必须实现的业务逻辑进入了models包,而它本应位于服务层。 b. 这会将Transform方法暴露给外部世界。

  2. 在服务层实现独立的函数,该函数接收ProductModel结构体并返回ProductResponse。类似这样:

    func transformProduct(p models.ProductModel) models.ProductResponse {
      return models.ProductResponse{
        Name:p.Name
        Code:p.Code
        DepartmentCode:p.Department.Code
        DepartmentName:p.Department.Name
        FactCode:p.Fact.Code
        FactName:p.Fact.Name
      }
    }
    

    优点: a. 数据转换的逻辑保留在服务层。 缺点: a. 当有更多结构体需要转换时,代码会变得混乱,并且服务层会出现更多函数。

注意:给出的示例非常基础,实际上我有一个庞大的结构体,包含近400个字段,需要逐个进行转换。

请告诉我哪种方法更好。如果您有新的实现方式的建议,那也会很有帮助。

提前感谢。


更多关于Golang代码结构设计:哪种方法更优?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

从概念上讲,ProductResponse 代表了 SQL 领域的“视图”,并且正确地属于存储层。(不要与 CQL 领域的“物化视图”混淆。)以下是一个概述 - https://en.wikipedia.org/wiki/View_(SQL)

更多关于Golang代码结构设计:哪种方法更优?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Go项目中,数据转换的实现方式取决于项目的规模和架构原则。基于你的三层架构,建议采用第二种方法(在服务层实现转换函数),原因如下:

  1. 关注点分离models包应仅包含数据结构的定义,不包含业务逻辑。转换逻辑属于服务层职责。
  2. 可测试性:独立的转换函数更容易进行单元测试。
  3. 避免循环依赖:如果转换需要引用其他服务层依赖,方法形式可能导致导入循环。

对于大型结构体(400个字段),建议使用代码生成工具或反射来减少手动编码错误。以下是示例实现:

// services/product.go
package services

import "your_project/models"

func TransformProduct(p models.ProductModel) models.ProductResponse {
    return models.ProductResponse{
        Name:           p.Name,
        Code:           p.Code,
        DepartmentCode: p.Department.Code,
        DepartmentName: p.Department.Name,
        FactCode:       p.Fact.Code,
        FactName:       p.Fact.Name,
    }
}

// 批量转换示例
func TransformProducts(products []models.ProductModel) []models.ProductResponse {
    responses := make([]models.ProductResponse, len(products))
    for i, p := range products {
        responses[i] = TransformProduct(p)
    }
    return responses
}

对于字段数量多的情况,可以考虑使用struct tags配合反射自动生成转换代码:

// 使用反射的通用转换函数示例
func TransformByReflect(src, dst interface{}) error {
    srcVal := reflect.ValueOf(src).Elem()
    dstVal := reflect.ValueOf(dst).Elem()
    
    for i := 0; i < srcVal.NumField(); i++ {
        srcField := srcVal.Field(i)
        dstField := dstVal.FieldByName(srcVal.Type().Field(i).Name)
        
        if dstField.IsValid() && dstField.CanSet() {
            dstField.Set(srcField)
        }
    }
    return nil
}

或者使用代码生成工具如github.com/switchupcb/copygen自动生成转换代码:

// 定义转换接口
//go:generate go run github.com/switchupcb/copygen
type Convert interface {
    ModelsToResponses(*models.ProductModel) *models.ProductResponse
}

在大型项目中,明确的职责划分比代码美观更重要。服务层处理转换逻辑能更好地维护架构一致性,特别是在需要添加验证、日志或监控等横切关注点时。

回到顶部