Golang将我的程序拆分为子包后出现了“不允许的导入循环”。

huangapple go评论94阅读模式
英文:

Golang "import cycle not allowed" after splitting up my program into subpackages

问题

我有一个庞大的Go程序,分布在包文件夹的根目录下的50多个杂项Go文件中。我知道这被认为是糟糕的做法,所以我决定将程序拆分为一些子包以便更好地组织。

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

我在网上进行了一些研究,并找到了一些很好的资源,比如这个很好的StackOverflow问题,试图解释如何从高层次上解决这个问题。

抱歉,但是这篇文章对我来说太高深了,我想知道是否有人能够详细解释一下我特定代码情况下的确切解决方案,而且最好用更简单的语言来解释,面向完全初学者的Go语言。

关于我的代码组织方式和功能的简要描述:

  • 它使用3种不同的协议(Twitch.tv、Discord和自定义WebSocket服务器)连接到3个不同的服务器。

  • 为每种服务器类型创建3个子包,然后在main.go文件中初始化它们似乎是显而易见的。

  • 每个子包不仅仅是一个接口;它包含一组全局变量(用于跟踪连接和其他事物)和一堆函数。(请注意,如果需要,我可以重构代码,使其全部包含在一个巨大的接口中。)

  • 大部分时间,子包从各自的服务器接收消息并将消息发送回各自的服务器,因此子包大部分时间都是相互独立的。

  • 但是,有时Twitch.tv模块需要向Discord服务器发送消息,而Discord服务器需要向Twitch.tv服务器发送消息。因此,Discord服务器需要能够调用Twitch.tv子包内的“Send()”函数,而Twitch.tv子包需要能够调用Discord子包的“Send()”函数!这就是我的循环依赖问题所在。

英文:

I have a large Go program that is spread across 50+ miscellaneous Go files in the root of my package folder. I know that this is considered terrible, so I've decided to embark upon splitting up the program into some subpackages for better organization.

Unfortunately, after splitting off the logical parts of my programs into subpackages, I'm running into the dreaded "import cycle not allowed" error. This is because the Go compiler refuses to compile anything with circular imports. But the different logical parts of my program need to communicate with each other...

I've done some research online and found some excellent resources, like this excellent StackOverflow question that attempts to explain what to think about to solve this problem at a high level.

My apologies, but this post is way over my head, and I was wondering if someone could spell out an exact solution for my specific code situation, and hopefully in simpler language aimed at a complete beginner to Go.

A brief description of how my code is organized and what it does:

  • It connects to 3 different servers using 3 different protocols (Twitch.tv, Discord, and a custom WebSocket server).
  • It seems obvious to make 3 subpackages, one for each server type, and then initialize all of them in a main.go file.
  • Each subpackage is not just an interface; it contains a collection of global variables (that track the connection + other things) and a bunch of functions. (Note that I can refactor this such that its all contained within one giant interface, if necessary.)
  • 95% of the time, the subpackages receive messages from their individual servers and send messages back to their individual servers, so the subpackages are mostly compartmentalized.
  • However, sometimes the Twitch.tv module needs to send a message to the Discord server, and the Discord server needs to send a message to the Twitch.tv server. So the Discord server needs to be able to call the "Send()" functions inside the Twitch.tv subpackage, and the Twitch.tv subpackage needs to be able to call the "Send()" function of the Discord subpackage! So this is where my circular problem comes from.

答案1

得分: 2

看起来你想把你的协议特定代码放在单独的包中。
如果你不想进行太多的重构,我建议你创建一个名为dispatcher的包。每个服务器都导入dispatcher包,并为特定的协议注册一个处理程序。当它需要调用另一个服务器时,只需通过dispatcher发送一条消息到指定的处理程序。

英文:

It looks like you want to keep your protocol specific code in separate packages.
If you don't want much refactor, I'd suggest you to create a package with dispatcher. Each server imports dispatcher package and register a handler for specific protocol. When it needs to call another server, just send a message via dispatcher to specified handler.

答案2

得分: 2

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

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

在创建实例之后,您可以为每个实例设置其他客户端。这样它们就可以发送到它们需要发送的任何地方,而不会出现循环依赖。您甚至可以将所有客户端放入一个结构体中,以便更容易进行注入。

例如:

// pkg sender
type Sender interface {
    Send(m Message) error // 或者根据需要进行修改
}

type Message struct {
    // 消息的内容
}

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
    // 其他字段...
}

func New() *TwitchClient {
    return new(TwitchClient) // 或者根据需要进行修改
}

func (t *TwitchClient) Send(m sender.Message) error {
    // 发送 Twitch 消息...

    // 需要发送 Discord 消息吗?
    t.Dispatcher.DiscordClient.Send(m)
}
英文:

In addition to the channel-based approaches proposed by TechSphinX and Oleg, you can use an interface-based approach and simple dependency injection.

You can use a setup function, probably in or called from main(), that creates instances of each service client. These should each implement Send() and have fields for the other clients they need to use. Create a Sender interface in its own package, and put your message struct in there as well.

After creating the instances, you can then set the other clients on each instance. This way they can send to whatever they need to send to, without circular dependencies. You can even put all the clients into a struct to make the injection easier.

For example:

// 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)
}

答案3

得分: 1

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

通信通道

在主程序中创建一个或多个通道,并在初始化时将其提供给两个包。然后它们可以在不知道对方存在的情况下相互通信。

英文:

Tailored to your particular case:

From what you describe the only reason for the packages to import each other is that they need to call each others Send() functions.

Channels to communicate

Create channel(s) in main and give it to both packages on init. Then they can communicate with each other without knowing of each others existence.

答案4

得分: 1

听起来,服务器/协议包本身是有用的,而从一种服务器发送消息到另一种服务器的要求是你特定应用程序的一个特性。换句话说,服务器/协议包不需要彼此发送消息,而是你的应用程序需要。

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

你也可以在main包中做这个,但我发现app包更有用。(我的main包通常只是一个main.go文件。)

英文:

It sounds like the server/protocol packages are useful on their own, and the requirement to send a message from one kind of a server to another kind is a feature of your specific application. In other words, the server/protocol packages don't need to send messages to each other, your application needs to.

I usually put application-specific functionality into an app package. Package app can import all your protocol packages.

You can also do this in package main, but I've found an app package to be a more useful instrument. (My main package is usually just the single main.go file.)

huangapple
  • 本文由 发表于 2017年8月10日 17:09:31
  • 转载请务必保留本文链接:https://go.coder-hub.com/45609236.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定