通过泛型扩展 BookSwap 应用

到目前为止,我们已经看到了如何编写泛型函数并使用泛型编写更简单的测试实用程序。这已经被证明是一种非常强大的机制,为我们提供了灵活性和类型安全性,这是空接口无法实现的。在本节中,我们将学习如何在我们示例的 REST API(BookSwap 应用程序)中使用泛型。

假设 BookSwap 应用程序希望扩展其业务模式,并开始在其常规书籍业务模式之外交换杂志。图11.3 展示了应用程序的新系统图:

image 2025 01 04 21 24 08 711
Figure 1. Figure 11.3 – The extended BookSwap application

前面的示例考虑了 BookSwap 应用程序的单体架构,但同样的考虑也适用于微服务架构。必须在整个应用程序中进行更改以支持新模型,从数据库级别开始:

  • 将创建一个 Magazines 数据库表。就像 Books 表一样,它将对 Users 主键 id 具有外键依赖。

  • 将创建 MagazineService 以与数据库查询交互。就像 BookService 一样,它将支持 upsert、list 和 swap 操作。

  • UserService 将对 MagazineService 具有依赖关系,允许它在此服务上执行操作并将信息转发给用户。

  • PostingService 将需要在成功交换 MagazineBook 时处理它们。由于此服务是外部的,我们可以假设此信息将通过 HTTP 请求传输。

其中一些更改确实需要专用代码,因为我们不希望使杂志和书籍过于紧密耦合。我们可能利用泛型的一个例子是在构建 HTTP 响应期间。到目前为止,Response 仅包含 Books 项:

type Response struct {
    Message string      `json:"message,omitempty"`
    Error   string      `json:"error,omitempty"`
    Books   []db.Book   `json:"books,omitempty"`
    User    *db.User    `json:"user,omitempty"`
}

我们用从 BookService 返回的书籍填充 Books 切片。我们现在需要扩展 Response 结构体以能够处理 MagazinesResponse 是一个广泛使用的类型,因此它是泛型实现的一个很好的候选者。

我们创建一个 ResponseItemType 自定义约束,其中包含 db.Bookdb.Magazine 类型的集合:

type ResponseItemType interface {
    db.Book | db.Magazine
}

如果添加了更多类型,我们可以将它们添加到此自定义类型约束中,并在整个应用程序中使用它们。

接下来,我们使用 ResponseItemType 作为 Response 的类型参数:

type Response[T ResponseItemType] struct {
    Message string      `json:"message,omitempty"`
    Error   string      `json:"error,omitempty"`
    Items   []T         `json:"items,omitempty"`
    User    *db.User    `json:"user,omitempty"`
}

我们使用占位符 T 作为响应的类型,然后将其用作 Items 切片的类型。Items 切片现在能够包含 db.Bookdb.Magazine 类型。所有与 Response 交互的其他函数现在也需要处理泛型 Response

我们将相同的类型参数添加到负责在 Response 中填充数据的 writeResponse 函数中:

func writeResponse[T ResponseItemType](w http.ResponseWriter, status int, resp *Response[T]) {
    // 实现
}

泛型函数只是将类型传递给响应,实现逻辑不需要其他更改。类型要么需要提供,要么作为占位符传递。

在调用端,我们还需要处理泛型方面。每个处理程序都使用 writeResponse 函数来填充 Response 上的数据并将其返回给客户端。ListBooks 处理程序演示了如何在调用端处理此问题:

// ListBooks 由 HTTP GET /books 调用。
func (h *Handler) ListBooks(w http.ResponseWriter, r *http.Request) {
    books, err := h.bs.List()
    if err != nil {
        writeResponse(w, http.StatusInternalServerError, &Response[db.Book]{
            Error: err.Error(),
        })
        return
    }
    // 发送 HTTP 状态和书籍列表
    writeResponse(w, http.StatusOK, &Response[db.Book]{
        Items: books,
    })
}

负责书籍的处理程序将以类似的方式处理响应写入。我们将 db.Book 类型传递给 Response 并调用 writeResponse 函数。我们不需要向此函数传递类型参数,因为可以从 Response 参数的调用中推断出类型。在错误的情况下,我们将错误写入 Response 并返回它,停止执行。在成功路径的情况下,我们将书籍写入 Items 切片。

Magazine 处理程序将以相同的方式实现,使用 db.Magazine 类型代替。我们可以使用我们在前几节中探讨的相同表测试技术来测试我们的响应逻辑。

这结束了我们对 Go 中泛型代码的探索。这个强大的工具使我们能够编写灵活的代码,可以与不同的数据类型一起使用。当涉及到泛型代码时,我们应该始终记住,它需要针对不同类型的输入参数进行测试,而不仅仅是不同的值。这可能会使测试变得更加复杂,但我们仍然可以轻松修改流行的表驱动测试技术来测试泛型代码。