Golang中关于惯用导入和包基数的最佳实践问题

Golang中关于惯用导入和包基数的最佳实践问题 在我维护的一个Web API中,我将查询拆分到它们自己的包中。我喜欢将所有SQL放在一个单独的包中,这样非常清晰。然而,我经常会遇到一种情况:我想在处理器中解析过滤器/值,然后直接将它们传递给我的数据层。由于循环导入,我无法这样做,因为我的控制器导入了查询包,而查询包不能反过来导入控制器。所以,我最终的做法类似这样:

// package controllers
type PossibleFilters struct {
	Filter1 []int    `json:"filter1"`
	Filter2 []int    `json:"filter2"`
	Filter3 []int    `json:"filter3"`
}
func SomeHandler(w http.ResponseWriter, r *http.Request) {
	body, _ := ioutil.ReadAll(r.Body)
	var filters = PossibleFilters{}
	// 显然在实际应用中,需要处理可能的错误...
	_ := json.Unmarshal(body, &filters)
	list := queries.RunQuery(filters.Filter1, filters.Filter2, filters.Filter3)
	sendResponseToClient(list, w)
}

… 然后在我的查询包中:

// package queries
func RunQuery(filter1 []int, filter 2 []int, filter3 []int) []ReturnItem {
	// 基于过滤器运行查询
}

当我的路由/查询有很多过滤器时,这会变得很冗长。如果我能这样做就好了:

// package queries
func RunQuery(filters PossibleFilters) []ReturnItem {
	// 基于过滤器运行查询
}

… 但正如我提到的,这会导致循环导入。实现这一点的惯用方法是什么?我的想法是:

  1. 将过滤器结构体放在它们自己的包中(模型包?)。我喜欢在使用它的处理器之前直接声明结构体的简洁性(由于这个特定API的性质,这些结构体往往不会大量重用,所以放在一个地方很方便)。然而,这会解决我的问题。
  2. 也许我已经有太多包了,应该合并成一个包?正如我提到的,我确实喜欢将查询保留在它们自己的包中的想法,并且我能够在其他包中重用它们。但是,我可能会减少包的数量并解决问题。

我倾向于选择**#1**,但我想知道其他人是怎么做的,以及什么对你最有效。最重要的是,是否有我尚未考虑的其他选项。


更多关于Golang中关于惯用导入和包基数的最佳实践问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

8 回复

我喜欢增加一层间接性,但该方法仍然存在一个问题:每次我添加一个新的可能过滤器时,都必须修改多个函数,并且参数数量最终会变得看起来很可笑(例如在某些情况下,比如报告功能,我可能会有很多种可能的过滤器类型)。此外,这里也在尝试遵循DRY原则。

更多关于Golang中关于惯用导入和包基数的最佳实践问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


你是否考虑过将 PossibleFilters 移动到查询包中?

仅从给出的代码片段来看,我可能无法全面理解,但看起来过滤器在逻辑上与查询相关。如果控制器包已经使用了查询包,那么它可以轻松地通过 var filters = queries.PossibleFilters{} 来实现,而无需额外成本。

是的——过滤器可以有不同类型,我在那个示例函数中主要是复制/粘贴。再次强调——我认为目前将结构体放在查询包中是合理的。它们确实与执行查询相关,因此放在那里是合适的。当然,唯一让我不太满意的是查询类中的 json 结构体标签,但在所有解决方案中,这似乎是相对较好的选择。

是的,有时一个小小的妥协胜过其他方案。放弃结构体标签的便利性(这恰恰是导致最初关注点分离、DRY原则和循环导入问题的原因)可能意味着你必须编写一个自定义的反序列化器。或者采用其他涉及大量代码变更的解决方案。在我看来,为一个小成果付出这么多努力实在不值……而且这个解决方案并非一成不变——你以后仍然可以重构。(是的,我知道,待办事项列表上又多了一个便签……)

这确实有道理,但我唯一的犹豫是:在一个理论上不应该关心数据是来自 JSON 还是其他地方的查询包中嵌入 json 标签,感觉有点不妥。理想情况下,我希望我的 queries 包不知道/不关心它如何被使用,但我可能想得太多了。既然这是一个与查询中的过滤器相关的类型,我认为使用 queries.PossibleFilters 最有意义,我可以接受在查询包中使用 json 标签。

我同意,结构体标签不应属于查询包。

controllers 中创建一个“包装器”函数是否能解决问题?

package controllers
...
type PossibleFilters ...
...
func RunQuery(filters PossibleFilters) []ReturnItem {
	return queries.RunQuery(filter1 []int, filter2 []int, filter3 []int)
}

这样一来,所有与 controllers 相关的内容都保留在 controllers 中,而查询的内部实现则保持在 controllers 之外。并且 queries 将不再依赖于 controllers

我显然对您设计中的内部需求了解不足,但我注意到结构体包含了过滤器1、2、3,它们都是相同类型。您只是为了这个讨论简化了代码,还是这就是“实际”的代码?

我之所以这样问,是因为我很好奇为什么这个过滤器数组被封装在结构体中,而不是切片或映射中。JSON 能否以数组形式传递过滤器?

我想到的是这样:

一个过滤器类型和修改后的 queries 中的 RunQuery(): 其中 RunQuery 定义为:

// package queries

type filter []int

func RunQuery(filters []filter) []ReturnItem {
	// run query based on filters
}

而 controllers 包可以像这样:

func SomeHandler(w http.ResponseWriter, r *http.Request) {
	body, _ := ioutil.ReadAll(r.Body)
	filters := []queries.filter
	_ := json.Unmarshal(body, &filters)
	list := queries.RunQuery(filters)
	sendResponseToClient(list, w)
}

在我看来这非常符合 DRY 原则。而且 JSON 可以传递任意数量的过滤器。同时也不存在循环依赖。

在Go中处理循环导入的惯用方法是创建共享类型包。根据你的场景,最佳实践是将过滤器结构体定义在独立的包中,这样控制器和查询包都可以导入它而不会形成循环依赖。

以下是具体实现示例:

// package filters
package filters

type PossibleFilters struct {
    Filter1 []int `json:"filter1"`
    Filter2 []int `json:"filter2"`
    Filter3 []int `json:"filter3"`
}
// package controllers
package controllers

import (
    "encoding/json"
    "io/ioutil"
    "net/http"
    "yourproject/filters"
    "yourproject/queries"
)

func SomeHandler(w http.ResponseWriter, r *http.Request) {
    body, _ := ioutil.ReadAll(r.Body)
    var filters = filters.PossibleFilters{}
    _ = json.Unmarshal(body, &filters)
    
    list := queries.RunQuery(filters)
    sendResponseToClient(list, w)
}
// package queries
package queries

import "yourproject/filters"

func RunQuery(f filters.PossibleFilters) []ReturnItem {
    // 使用 f.Filter1, f.Filter2, f.Filter3 运行查询
    return []ReturnItem{}
}

这种模式在Go生态系统中很常见,特别是在大型项目中。例如,标准库中的imageimage/color包就采用了类似的设计,其中image/color包定义了颜色模型,供image包和其他图像处理包使用。

另一种变体是使用接口来定义过滤器行为:

// package filters
package filters

type Filter interface {
    GetFilter1() []int
    GetFilter2() []int
    GetFilter3() []int
}

type PossibleFilters struct {
    Filter1 []int `json:"filter1"`
    Filter2 []int `json:"filter2"`
    Filter3 []int `json:"filter3"`
}

func (f PossibleFilters) GetFilter1() []int { return f.Filter1 }
func (f PossibleFilters) GetFilter2() []int { return f.Filter2 }
func (f PossibleFilters) GetFilter3() []int { return f.Filter3 }
// package queries
package queries

import "yourproject/filters"

func RunQuery(f filters.Filter) []ReturnItem {
    // 使用 f.GetFilter1() 等方法
    return []ReturnItem{}
}

这种方法提供了更好的解耦,查询包只需要知道过滤器接口,而不需要知道具体的实现。这在需要多种过滤器类型或需要模拟测试时特别有用。

对于你的具体情况,第一种方法(简单的共享结构体包)通常是最直接和清晰的解决方案。它保持了代码的简洁性,同时解决了循环导入问题,符合Go的"简单性优先"哲学。

回到顶部