【问题标题】:Golang "import cycle not allowed" after splitting up my program into subpackages将我的程序拆分为子包后,Golang“不允许导入周期”
【发布时间】:2018-01-18 10:31:51
【问题描述】:

我有一个大型 Go 程序,它分布在我的包文件夹根目录中的 50 多个其他 Go 文件中。我知道这被认为很糟糕,所以我决定着手将程序拆分为一些子包以便更好地组织。

不幸的是,在将程序的逻辑部分拆分为子包后,我遇到了可怕的“不允许导入循环”错误。这是因为 Go 编译器拒绝使用循环导入编译任何内容。但是我程序的不同逻辑部分需要相互通信......

我在网上做了一些研究,发现了一些优秀的资源,比如this excellent StackOverflow question,它试图解释如何从高层次上解决这个问题。

我很抱歉,但是这篇文章让我有点过头了,我想知道是否有人可以针对我的特定代码情况给出一个确切的解决方案,并希望以更简单的语言针对完全的 Go 初学者。

简要说明我的代码的组织方式及其作用:

  • 它使用 3 种不同的协议(Twitch.tv、Discord 和自定义 WebSocket 服务器)连接到 3 台不同的服务器。
  • 制作 3 个子包,每个服务器类型一个,然后在 main.go 文件中初始化所有子包似乎很明显。
  • 每个子包不仅仅是一个接口;它包含一组全局变量(跟踪连接+其他事物)和一堆函数。 (请注意,如有必要,我可以对其进行重构,使其全部包含在一个巨大的界面中。)
  • 95% 的情况下,子包从各自的服务器接收消息并将消息发送回各自的服务器,因此子包大多是分隔的。
  • 但是,有时 Twitch.tv 模块需要向 Discord 服务器发送消息,而 Discord 服务器需要向 Twitch.tv 服务器发送消息。所以 Discord 服务器需要能够调用 Twitch.tv 子包内的“Send()”函数,而 Twitch.tv 子包需要能够调用 Discord 子包的“Send()”函数!所以这就是我的循环问题的来源。

【问题讨论】:

标签: go


【解决方案1】:

您似乎希望将特定于协议的代码保存在单独的包中。 如果您不想进行太多重构,我建议您使用dispatcher 创建一个包。每个服务器导入dispatcher 包并为特定协议注册一个处理程序。当它需要调用另一个服务器时,只需通过dispatcher向指定的处理程序发送消息即可。

【讨论】:

    【解决方案2】:

    除了 TechSphinX 和 Oleg 提出的基于通道的方法之外,您还可以使用基于接口的方法和简单的依赖注入。

    您可以使用可能在main() 中或从main() 调用的设置函数来创建每个服务客户端的实例。这些都应该实现Send(),并为他们需要使用的其他客户端提供字段。在自己的包中创建一个Sender 接口,并将您的消息结构也放入其中。

    创建实例后,您可以在每个实例上设置其他客户端。这样他们就可以发送到他们需要发送到的任何地方,而无需循环依赖。您甚至可以将所有客户端放入一个结构中,以使注入更容易。

    例如:

    // pkg sender
    type Sender interface {
        Send(m Message) error // or whatever it needs to be
    }
    
    type Message struct {
        // Whatever goes in a message
    }
    
    type Dispatcher struct {
        TwitchClient Sender
        DiscordClient Sender
        WebClient Sender
    }
    
    // pkg main
    func setup() {
        d := sender.Dispatcher{
            TwitchClient: twitch.New(),
            DiscordClient: discord.New(),
            WebClient: web.New(),
        }
        d.TwitchClient.Dispatcher = d
        d.DiscordClient.Dispatcher = d
        d.WebClient.Dispatcher = d
    }
    
    // pkg twitch
    type TwitchClient struct {
        Dispatcher sender.Dispatcher
        // other fields ...
    }
    
    func New() *TwitchClient {
        return new(TwitchClient) // or whatever
    }
    
    func (t *TwitchClient) Send(m sender.Message) error {
        // send twitch message...
    
        // Need to send a Discord message?
        t.Dispatcher.DiscordClient.Send(m)
    }
    

    【讨论】:

    • 非常好的评论,谢谢。因此,传递指向所有子包的“全局”指针结构称为“依赖注入”,这不是我以前听过的术语。我的第一直觉是创建一个包“globals”而不是包“sender”,并让它包含一个代表每个(整个)客户端的指针结构,并传递它而不仅仅是发送者。但我想我瞥见那是行不通的,因为每个子模块仍然必须导入其他子模块才能了解指针类型是什么。
    • 该界面使您可以防止软件包相互依赖。而且,它们不是全局变量。它们是结构实例的字段。理论上,您可以拥有 Dispatcher 结构的全局实例,但全局变量通常是 Bad Thing™,因此使用 dependency injection
    【解决方案3】:

    根据您的具体情况量身定制:

    根据您的描述,包相互导入的唯一原因是它们需要相互调用Send() 函数。

    沟通渠道

    在 main 中创建通道并在初始化时将其提供给两个包。然后他们可以在不知道彼此存在的情况下相互交流。

    【讨论】:

      【解决方案4】:

      听起来服务器/协议包本身很有用,从一种服务器向另一种服务器发送消息的要求是您特定应用程序的一项功能。换句话说,服务器/协议包不需要互相发送消息,你的应用程序需要。

      我通常将特定于应用程序的功能放入应用程序包中。包应用可以导入你所有的协议包。

      您也可以在 package main 中执行此操作,但我发现 app package 是一种更有用的工具。 (我的主包通常只是一个 main.go 文件。)

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-10-12
        • 1970-01-01
        • 1970-01-01
        • 2019-03-30
        • 1970-01-01
        • 2022-11-18
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多