通过泛型扩展 BookSwap 应用
到目前为止,我们已经看到了如何编写泛型函数并使用泛型编写更简单的测试实用程序。这已经被证明是一种非常强大的机制,为我们提供了灵活性和类型安全性,这是空接口无法实现的。在本节中,我们将学习如何在我们示例的 REST API(BookSwap 应用程序)中使用泛型。
假设 BookSwap 应用程序希望扩展其业务模式,并开始在其常规书籍业务模式之外交换杂志。图11.3 展示了应用程序的新系统图:

前面的示例考虑了 BookSwap 应用程序的单体架构,但同样的考虑也适用于微服务架构。必须在整个应用程序中进行更改以支持新模型,从数据库级别开始:
-
将创建一个
Magazines
数据库表。就像Books
表一样,它将对Users
主键id
具有外键依赖。 -
将创建
MagazineService
以与数据库查询交互。就像BookService
一样,它将支持 upsert、list 和 swap 操作。 -
UserService
将对MagazineService
具有依赖关系,允许它在此服务上执行操作并将信息转发给用户。 -
PostingService
将需要在成功交换Magazine
和Book
时处理它们。由于此服务是外部的,我们可以假设此信息将通过 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
结构体以能够处理 Magazines
。Response
是一个广泛使用的类型,因此它是泛型实现的一个很好的候选者。
我们创建一个 ResponseItemType
自定义约束,其中包含 db.Book
和 db.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.Book
和 db.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 中泛型代码的探索。这个强大的工具使我们能够编写灵活的代码,可以与不同的数据类型一起使用。当涉及到泛型代码时,我们应该始终记住,它需要针对不同类型的输入参数进行测试,而不仅仅是不同的值。这可能会使测试变得更加复杂,但我们仍然可以轻松修改流行的表驱动测试技术来测试泛型代码。