我是刚来Go的,来自C#背景,我只是对如何构建Go应用程序感到非常困惑.

假设我正在构建一个睡觉应用编程接口,它将位于数据库之上.还可以说,即使在完成之后,考虑到业务的变迁等因素,此应用程序也可能需要频繁更改.

在C#中,通过实体框架和DTO等工具,我从控制器给出的结果中抽象出数据库,从而在一定程度上缓解了这个问题.如果我更改数据库中一组字段的名称,我可能必须更改数据库访问逻辑,但希望我使用AutoMapper映射到实体的DTO可以保持不变,这样我就不会 destruct 依赖于给定DTO struct 的前端功能.

我应该用GO的 struct 复制这个 struct 吗?这种方法似乎有些错误,因为 struct 基本上只是DTO,我会有相当多的DTO struct 与实体 struct 相同.我还必须设置逻辑以将实体映射到DTO.不知何故,这一切感觉非常简单,我在Web上看到的许多示例只是序列化了数据库 struct .

简而言之,人们如何避免Go中API和数据库之间的过度耦合,以及如何广泛地分离应用程序的不同部分?

如果有什么不同的话,我计划使用sqlx将数据库结果封送到 struct 中,这意味着如果我不将实体与DTO分开,除了JSON标记之外,还会有更多的标记.

推荐答案

对于REST API,您通常会处理至少三个不同的实现层:

  • HTTP处理程序
  • 某种业务逻辑/用例
  • 持久存储/数据库接口

您可以分别处理和构建它们中的每一个,这样不仅可以将其解耦,而且还可以使其更易于测试.然后通过注入必要的位将这些部分组合在一起,因为它们符合您定义的接口.通常,这最终会留下main或单独的配置机制,唯一知道what的地方是组合和注入how.

Applying The Clean Architecture to Go applications条很好地说明了如何将各个部分分开.您应该严格遵循这种方法的程度在一定程度上取决于项目的复杂性.

下面是一个非常基本的细分,将处理程序与逻辑层和数据库层分开.

HTTP处理程序

处理程序除了根据需要将请求值映射到局部变量或可能的自定义数据 struct 之外,什么也不做.除此之外,它只运行用例逻辑,并在将结果写入响应之前将其映射.这也是将不同的错误映射到不同的响应对象的好地方.

type Interactor interface {
    Bar(foo string) ([]usecases.Bar, error)
}

type MyHandler struct {
    Interactor Interactor
}

func (handler MyHandler) Bar(w http.ResponseWriter, r *http.Request) {
    foo := r.FormValue("foo")
    res, _ := handler.Interactor.Bar(foo)

    // you may want to map/cast res to a different type that is encoded
    // according to your spec
    json.NewEncoder(w).Encode(res)
}

单元测试是测试HTTP响应是否包含不同结果和错误的正确数据的好方法.

用例/业务逻辑

由于存储库只是被指定为一个接口,因此很容易为业务逻辑创建单元测试,使用同样符合DataRepository的模拟存储库实现返回的不同结果.

type DataRepository interface {
    Find(f string) (Bar, error)
}

type Bar struct {
    Identifier string
    FooBar     int
}

type Interactor struct {
    DataRepository DataRepository
}

func (interactor *Interactor) Bar(f string) (Bar, error) {
    b := interactor.DataRepository.Find(f)

    // ... custom logic

    return b
}   

数据库接口

与数据库对话的部分实现了DataRepository接口,但在其他方面完全独立于它如何将数据转换为预期的类型.

type Repo {
    db sql.DB
}

func NewDatabaseRepo(db sql.DB) *Repo {
    // config if necessary...

    return &Repo{db: db}
}

func (r Repo)Find(f string) (usecases.Bar, error) {
    rows, err := db.Query("SELECT id, foo_bar FROM bar WHERE foo=?", f)
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    for rows.Next() {
        var id string, fooBar int
        if err := rows.Scan(&id, &fooBar); err != nil {
            log.Fatal(err)
        }
        // map row value to desired structure
        return usecases.Bar{Identifier: id, FooBar: fooBar}
    }

    return errors.New("not found")
}

同样,这允许单独测试数据库操作,而无需任何模拟SQL语句.

上面的代码是非常多的伪代码,而且是不完整的.

Go相关问答推荐

一种基于时间的Golang函数节制器

GORM Find(&;Room)操作使用空数据而不是实际数据填充 struct

GoFR HTTP服务初始化中Open遥测传输和超时配置的说明

创建使用逗号而不是加号分隔OU的CSR

由docker中的nginx提供的样式和图像在页面上不起作用

如何用';贪婪原则';正确地

在运行时更改 Go lang slog 的日志(log)级别

从 ApiGateway 中的 lambda Go 返回 Json

在golang中以JSON格式获取xwwwformurlencoded请求的嵌套键值对

在 Go sync.Map 中,为什么这部分实现不一致或者我误解了什么?

加载 docker 镜像失败

动态 SQL 集 Golang

获取切片元素的地址是否意味着 Go 中元素的副本?

go:embed 文件扩展名模式

没有堆栈跟踪的 go 程序崩溃是什么意思?

从golang中的url加载图像

如何在 Windows 中使用 github.com/AllenDang/giu 和 github.com/gordonklaus/portaudio 构建 GO 程序

如何排除溢出矩阵的坐标

如何获得Ent中数字列的总和

如何使用通用字段初始化匿名struct数组