循环依赖和接口

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

Cyclic dependencies and interfaces

问题

我是一名长期的Python开发者。我正在尝试使用Go语言,将一个现有的Python应用程序转换为Go。这个应用程序是模块化的,对我来说非常好用。

在创建相同的结构的Go版本时,我发现自己遇到了很多循环导入错误,比我想象的要多得多。在Python中从来没有遇到过任何导入问题。我甚至从未使用过导入别名。所以我可能在Python中有一些不明显的循环导入。我觉得这很奇怪。

无论如何,我迷失了方向,试图在Go中修复这些问题。我读到接口可以用来避免循环依赖。但是我不明白具体怎么做。我也没有找到任何相关的示例。有人可以帮助我吗?

当前的Python应用程序结构如下:

/main.py

/settings/routes.py      包含主要路由的文件,依赖于app1/routes.py、app2/routes.py等
/settings/database.py    类似于connect()的函数,用于打开数据库会话
/settings/constants.py   通用常量

/apps/app1/views.py      URL处理函数
/apps/app1/models.py     应用程序特定的数据库函数,依赖于settings/database.py
/apps/app1/routes.py     应用程序特定的路由

/apps/app2/views.py      URL处理函数
/apps/app2/models.py     应用程序特定的数据库函数,依赖于settings/database.py
/apps/app2/routes.py     应用程序特定的路由

settings/database.py包含了像connect()这样的通用函数,用于打开数据库会话。因此,apps包中的应用程序调用database.connect(),就会打开一个数据库会话。

settings/routes.py也是同样的情况,它包含了允许应用程序将其子路由添加到主路由对象的函数。

settings包更多地涉及函数而不是数据/常量。它包含了被apps包中的应用程序使用的代码,否则这些代码将不得不在所有的应用程序中重复。因此,如果我需要更改路由器类,我只需要更改settings/router.py,应用程序将继续工作而无需修改。

英文:

I am a long time python developer. I was trying out Go, converting an existing python app to Go. It is modular and works really well for me.

Upon creating the same structure in Go, I seem to land in cyclic import errors, a lot more than I want to. Never had any import problems in python. I never even had to use import aliases. So I may have had some cyclic imports which were not evident in python. I actually find that strange.

Anyways, I am lost, trying to fix these in Go. I have read that interfaces can be used to avoid cyclic dependencies. But I don't understand how. I didn't find any examples on this either. Can somebody help me on this?

The current python application structure is as follows:

/main.py

/settings/routes.py      contains main routes depends on app1/routes.py, app2/routes.py etc
/settings/database.py    function like connect() which opens db session
/settings/constants.py   general constants

/apps/app1/views.py      url handler functions
/apps/app1/models.py     app specific database functions depends on settings/database.py
/apps/app1/routes.py     app specific routes

/apps/app2/views.py      url handler functions
/apps/app2/models.py     app specific database functions depends on settings/database.py
/apps/app2/routes.py     app specific routes

settings/database.py has generic functions like connect() which opens a db session. So an app in the apps package calls database.connect() and a db session is opened.

The same is the case with settings/routes.py it has functions that allow apps to add their sub-routes to the main route object.

The settings package is more about functions than data/constants. This contains code that is used by apps in the apps package, that would otherwise have to be duplicated in all the apps. So if I need to change the router class, for instance, I just have to change settings/router.py and the apps will continue to work with no modifications.

答案1

得分: 88

这里有两个高级部分:确定哪些代码放在哪个包中,以及调整你的API以减少包需要承担的依赖关系。

关于设计避免某些导入的API:

  • 为将包连接到彼此的配置函数编写,这样可以在运行时而不是编译时进行。例如,routes不需要导入定义路由的所有包,它可以导出routes.Register,然后main(或每个应用程序中的代码)可以调用它。通常,配置信息可能通过main或专用包传递;过多地分散它可能会导致管理困难。

  • 传递基本类型和interface值。如果你只依赖于某个包的类型名称,也许可以避免导入它。例如,处理[]Page的某些代码可以改为使用文件名的[]string、ID的[]int,或者更通用的接口(如sql.Rows)。

  • 考虑创建只包含纯数据类型和接口的“模式”包,例如将User与从数据库加载用户的代码分离。它不需要依赖太多(也许不需要依赖任何东西),所以你可以从任何地方引用它。Ben Johnson在GopherCon 2016上做了一个闪电演讲,建议按依赖关系组织包。

关于将代码组织成包:

  • 通常情况下,当每个部分都可以单独使用时,将包拆分开来。如果两个功能非常相关,你可以不将它们拆分成包,而是使用多个文件或类型进行组织。大型包也可以,例如Go的net/http

  • 根据主题或依赖关系拆分“杂包”(utilstools)等。否则,你可能会为了使用一两个功能而导入一个庞大的utils包(并承担它的所有依赖关系)。

  • 考虑将可重用的代码“下沉”到与特定用例无关的较低级别包中。如果你有一个包含内容管理系统逻辑和通用HTML处理代码的package page,可以考虑将HTML相关的代码“下沉”到package html中,这样就可以在不导入不相关的内容管理代码的情况下使用它。

在这里,我会重新安排代码,使路由器不需要包含路由:相反,每个应用程序包调用router.Register()方法。这就是Gorilla web toolkit的mux的做法。你的routesdatabaseconstants包听起来像是应该被应用程序代码导入而不是导入它们的低级部分。

通常情况下,尽量构建分层的应用程序。你的高层、特定用例的应用程序代码应该导入低层、更基础的工具,而不是相反。以下是一些更多的想法:

  • 从调用者的角度来看,_包_对于分离独立可用的功能块非常有用。对于你的_内部_代码组织,你可以在包中的源文件之间轻松移动代码。在x/foo.gox/bar.go中定义的符号的初始命名空间只是x包,根据需要拆分/合并文件并不难,尤其是在使用goimports等工具的帮助下。

    标准库的net/http大约有7k行代码(包括注释/空行但不包括测试)。在内部,它被拆分为许多较小的文件和类型。但它是一个包,我认为这是因为没有理由用户只想要,比如,仅仅是cookie处理。另一方面,netnet/url是分开的,因为它们在HTTP之外也有用途。

    如果可以的话,将“下沉”实用程序到独立的库中,使其成为独立的、精心设计的产品,或者将应用程序本身进行清晰的分层(例如,UI位于API之上,API位于核心库和数据模型之上)。同样,“水平”分离可能有助于你在脑海中构建应用程序(例如,UI层分解为用户帐户管理、应用程序核心和管理工具,或者更细粒度的分解)。但是,核心观点是,你可以根据自己的需要自由拆分或不拆分

  • 设置API以在运行时配置行为,这样就不需要在编译时导入它。例如,你的URL路由器可以公开一个Register方法,而不是导入appAappB等,并从每个应用程序中读取var Routes。你可以创建一个myapp/routes包,它导入router和所有视图,并调用router.Register。基本思想是路由器是通用代码,不需要导入应用程序的视图。

    一些组合配置API的方法:

    • 通过interfacefunc传递应用程序行为:http可以传递Handler的自定义实现(当然),还可以传递CookieJarFiletext/templatehtml/template可以接受函数,以便从模板中访问(在FuncMap中)。

    • 如果合适,从包中导出快捷函数:在http中,调用者可以创建并单独配置一些http.Server对象,或者调用http.ListenAndServe(...),它使用全局的Server。这样设计很好,因为所有内容都在一个对象中,调用者可以在一个进程中创建多个Server等,但它也提供了一种在简单的单服务器情况下进行配置的简单方法。

    • 如果必要,可以使用“粗糙”的方法:如果无法将超级优雅的配置系统适应你的应用程序,你不必局限于它:也许对于某些东西,一个具有全局var Conf map[string]interface{}package "myapp/conf"在某些情况下是有用的。但要注意全局配置的缺点。如果你想编写可重用的库,它们不能导入myapp/conf;它们需要在构造函数中接受它们所需的所有信息。全局配置还存在一个风险,即将某个假设硬编码为始终在整个应用程序中具有单个值,而实际上可能不会始终如此;也许今天你有一个单一的数据库配置或HTTP服务器配置,但将来可能不会有。

一些更具体的移动代码或更改定义以减少依赖问题的方法:

  • 将基本任务与应用程序相关的任务分离。我在另一种语言中工作的一个应用程序有一个“utils”模块,混合了通用任务(例如,格式化日期时间或处理HTML)和与应用程序特定的任务(依赖于用户模式等)。但是,用户包导入了utils,导致循环依赖。如果我要将其移植到Go中,我会将依赖于用户的utils“上移”,可能与用户代码一起存放,甚至位于其上方。

  • 考虑拆分杂包。稍微扩展上一个观点:如果两个功能独立(即,如果将一些代码移动到另一个包中仍然可以正常工作)并且从用户的角度来看是无关的,那么它们可以被拆分为两个包。有时捆绑是无害的,但有时会导致额外的依赖关系,或者一个不太通用的包名只会使代码更加清晰。因此,我上面提到的utils可以按主题或依赖关系拆分(例如,strutildbutil等)。如果你以这种方式创建了很多包,我们有goimports来帮助管理它们。

  • 在API中使用基本类型和interface替代需要导入的对象类型。假设你的应用程序中有两个实体具有多对多的关系,比如UserGroup。如果它们位于不同的包中(一个大的“如果”),你不能同时让u.Groups()返回[]group.Groupg.Users()返回[]user.User,因为这需要包相互导入。

    但是,你可以将其中一个或两个返回更改为返回[]uint的ID,或者返回sql.Rows或其他一些interface,你可以在不导入特定对象类型的情况下使用它。根据你的用例,UserGroup等类型可能是如此紧密相关,以至于最好将它们放在一个包中,但如果你决定它们应该是不同的,这是一种方法。

谢谢你详细的问题和后续问题。

英文:

There're two high-level pieces to this: figuring out which code goes in which package, and tweaking your APIs to reduce the need for packages to take on as many dependencies.

On designing APIs that avoid the need for some imports:

  • Write config functions for hooking packages up to each other at run time rather than compile time. Instead of routes importing all the packages that define routes, it can export routes.Register, which main (or code in each app) can call. In general, configuration info probably flows through main or a dedicated package; scattering it around too much can make it hard to manage.

  • Pass around basic types and interface values. If you're depending on a package for just a type name, maybe you can avoid that. Maybe some code handling a []Page can get instead use a []string of filenames or a []int of IDs or some more general interface (sql.Rows) instead. <!-- There's something to extract about layering here. -->

  • Consider having 'schema' packages with just pure data types and interfaces, so User is separate from code that might load users from the database. It doesn't have to depend on much (maybe on anything), so you can include it from anywhere. Ben Johnson gave a lightning talk at GopherCon 2016 suggesting that and organizing packages by dependencies.

On organizing code into packages:

  • As a rule, split a package up when each piece could be useful on its own. If two pieces of functionality are really intimately related, you don't have to split them into packages at all; you can organize with multiple files or types instead. Big packages can be OK; Go's net/http is one, for instance.

  • Break up grab-bag packages (utils, tools) by topic or dependency. Otherwise you can end up importing a huge utils package (and taking on all its dependencies) for one or two pieces of functionality (that wouldn't have so many dependencies if separated out).

  • Consider pushing reusable code 'down' into lower-level packages untangled from your particular use case. If you have a package page containing both logic for your content management system and all-purpose HTML-manipulation code, consider moving the HTML stuff "down" to a package html so you can use it without importing unrelated content management stuff.


Here, I'd rearrange things so the router doesn't need to include the routes: instead, each app package calls a router.Register() method. This is what the Gorilla web toolkit's mux package does. Your routes, database, and constants packages sound like low-level pieces that should be imported by your app code and not import it.

Generally, try to build your app in layers. Your higher-layer, use-case-specific app code should import lower-layer, more fundamental tools, and never the other way around. Here are some more thoughts:

  • Packages are good for separating independently usable bits of functionality from the caller's perspective. For your internal code organization, you can easily shuffle code between source files in the package. The initial namespace for symbols you define in x/foo.go or x/bar.go is just package x, and it's not that hard to split/join files as needed, especially with the help of a utility like goimports.

The standard library's net/http is about 7k lines (counting comments/blanks but not tests). Internally, it's split into many smaller files and types. But it's one package, I think 'cause there was no reason users would want, say, just cookie handling on its own. On the other hand, net and net/url are separate because they have uses outside HTTP.

It's great if you can push "down" utilities into libraries that are independent and feel like their own polished products, or cleanly layer your application itself (e.g., UI sits atop an API sits atop some core libraries and data models). Likewise "horizontal" separation may help you hold the app in your head (e.g., the UI layer breaks up into user account management, the application core, and administrative tools, or something finer-grained than that). But, the core point is, you're free to split or not as works for you.

  • Set up APIs to configure behavior at run-time so you don't have to import it at compile time. So, for example, your URL router can expose a Register method instead of importing appA, appB, etc. and reading a var Routes from each. You could make a myapp/routes package that imports router and all your views and calls router.Register. The fundamental idea is that the router is all-purpose code that needn't import your application's views.

Some ways to put together config APIs:

  • Pass app behavior via interfaces or funcs: http can be passed custom implementations of Handler (of course) but also CookieJar or File. text/template and html/template can accept functions to be accessible from templates (in a FuncMap).

  • Export shortcut functions from your package if appropriate: In http, callers can either make and separately configure some http.Server objects, or call http.ListenAndServe(...) that uses a global Server. That gives you a nice design--everything's in an object and callers can create multiple Servers in a process and such--but it also offers a lazy way to configure in the simple single-server case.

  • If you have to, just duct-tape it: You don't have to limit yourself to super-elegant config systems if you can't fit one to your app: maybe for some stuff a package &quot;myapp/conf&quot; with a global var Conf map[string]interface{} is useful.
    But be aware of downsides to global conf. If you want to write reusable libraries, they can't import myapp/conf; they need to accept all the info they need in constructors, etc. Globals also risk hard-wiring in an assumption something will always have a single value app-wide when it eventually won't; maybe today you have a single database config or HTTP server config or such, but someday you don't.

Some more specific ways to move code or change definitions to reduce dependency issues:

  • Separate fundamental tasks from app-dependent ones. One app I work on in another language has a "utils" module mixing general tasks (e.g., formatting datetimes or working with HTML) with app-specific stuff (that depends on the user schema, etc.). But the users package imports the utils, creating a cycle. If I were porting to Go, I'd move the user-dependent utils "up" out of the utils module, maybe to live with the user code or even above it.

  • Consider breaking up grab-bag packages. Slightly enlarging on the last point: if two pieces of functionality are independent (that is, things still work if you move some code to another package) and unrelated from the user's perspective, they're candidates to be separated into two packages. Sometimes the bundling is harmless, but other times it leads to extra dependencies, or a less generic package name would just make clearer code. So my utils above might be broken up by topic or dependency (e.g., strutil, dbutil, etc.). If you wind up with lots of packages this way, we've got goimports to help manage them.

  • Replace import-requiring object types in APIs with basic types and interfaces. Say two entities in your app have a many-to-many relationship like Users and Groups. If they live in different packages (a big 'if'), you can't have both u.Groups() returning a []group.Group and g.Users() returning []user.User because that requires the packages to import each other.

    However, you could change one or both of those return, say, a []uint of IDs or a sql.Rows or some other interface you can get to without importing a specific object type. Depending on your use case, types like User and Group might be so intimately related that it's better just to put them in one package, but if you decide they should be distinct, this is a way.

<!-- note for later: can think of at least one time in my short experience that it helped to push a "dumb" data type down into a lower-level package separate from much of the functionality using it. that package didn't need to import much (anything in my case), so other packages could import it and use the datatype it defined to communicate. io and errors are arguably examples, with the import-heavier functionality moved off to io/ioutuil, os, etc. however, i'd like to research up more cases and get a sense of how important/preferred this technique is in prod software out there. -->

Thanks for the detailed question and followup.

答案2

得分: 0

可能的部分但不完美的答案:
已经为解决循环依赖问题努力了一年。有一段时间,成功解耦,消除了循环依赖。我的应用程序大量使用插件。同时,它还使用编码/解码库(json和gob)。为此,我有自定义的编组和解组方法,以及json的等效方法。
为了使其正常工作,传递给编解码器的数据结构的完整类型名称(包括包名称)必须相同。编解码器的创建必须在一个包中。这个包被其他包以及插件调用。
只要编解码器包不需要调用任何调用它的包,或者使用方法或方法接口,一切都正常工作。为了能够在插件中使用包中的类型,插件必须与包一起编译。由于我不想在插件的构建中包含主程序,这将破坏插件的目的,因此只有编解码器包包含在插件和主程序中。一切都正常工作,直到我需要从编解码器包调用主程序,在主程序已经调用编解码器包之后。这将导致循环依赖。为了消除这个问题,我可以将编解码器放在主程序中,而不是它自己的包中。但是,由于在编组/解组方法中使用的特定数据类型必须在主程序和插件中相同,我需要为每个插件使用主程序包进行编译。此外,因为我需要主程序调用插件,我需要在主程序中使用插件的接口类型。我从未找到过使这个工作的方法,但我想到了一个可能的解决方案:
首先,将编解码器分离为一个插件,而不仅仅是一个包
然后,从主程序中作为第一个插件加载它。
创建一个注册函数来交换接口和底层方法。所有的编码器和解码器都是通过调用这个插件来创建的。插件通过注册的接口回调到主程序。
主程序和所有插件都使用相同的接口类型包。
然而,实际编码数据的数据类型在主程序中引用时与插件中的数据类型引用的名称不同,但底层类型相同,否则会存在相同的循环依赖。要完成这一部分,需要进行不安全的类型转换。编写了一个小函数来进行强制转换,以使语法更清晰:
(<cast pointer type*>Cast(<pointer to structure, or interface to pointer to structure>))。

编解码器的另一个问题是确保在将数据发送到编码器时进行类型转换,以便编组/解组方法能够识别数据类型名称。为了更容易实现这一点,可以从一个包中导入主程序类型,从另一个包中导入插件类型,因为它们彼此不引用。

这是一个非常复杂的解决方案,但我看不到其他方法可以使其工作。
我还没有尝试过这个解决方案。在完成所有工作之后,可能仍然会出现循环依赖。

[更多内容]

为了避免循环依赖问题,我使用了一种使用指针的不安全类型方法。首先,这是一个带有一个小函数Cast()的包,用于执行不安全的类型转换,以使代码更易读:

package ForcedCast

import (
    "unsafe"
    "reflect"
)

// cast函数用于进行类型转换,以隐藏丑陋的语法
// 使用方式如下:
// <var> = (cast type)(cast(input var))
func Cast(i interface{})(unsafe.Pointer) {
    return (unsafe.Pointer(reflect.ValueOf(i).Pointer()))
}

然后,我使用"interface{}"作为void指针的等效类型:

package firstpackage
type realstruct struct {
     ...
}   

var Data realstruct

// 设置一个函数来调用加载的插件
var calledfuncptr func(interface)

func callingfunc() {

        pluginpath := path.Join(<pathname>, "calledfuncplugin")
        plug, err := plugin.Open(pluginpath)

        rFunc, err := plug.Lookup("calledfunc")
        calledfuncptr = rFunc.(interface{})

        calledfuncptr (&Data)
}


//在一个插件中
//插件不使用主代码的包,使用-buildmode=plugin进行构建
package main

// 相同的结构定义
type realstruct struct {
     ...
}   

var localdataptr *realstruct

func calledfunc(needcast interface{}) {

    localdataptr = (*realstruct)(Cast(needcast))

}

对于任何其他包的交叉类型依赖,使用"interface{}"作为void指针,并根据需要进行适当的类型转换。

这仅在接口{}指向的底层类型在进行类型转换时是相同的情况下才有效。为了使这更容易,我将类型放在一个单独的文件中。在调用包中,它们以包名称开头。然后,我复制类型文件,将包更改为"package main",并将其放在插件目录中,以便构建类型,但不构建包名称。

可能还有一种方法可以处理实际数据值,而不仅仅是指针,但我还没有完全解决这个问题。

我所做的其中一件事是将其转换为接口而不是数据类型指针。这允许您使用插件方法调用包含循环依赖的插件。接口具有指向数据类型的指针,然后您可以使用它从调用者调用包中的方法。

之所以能够工作的原因是数据类型在插件之外是不可见的。也就是说,如果我加载两个插件,它们都是"package main",并且类型在两个插件的"package main"中定义,但是具有相同名称的不同类型,这些类型不会冲突。

但是,如果我将一个公共包放入两个插件中,那个包必须是相同的,并且必须具有编译时的完全路径名。为了适应这一点,我使用Docker容器进行构建,以便我可以强制路径名始终正确,适用于插件之间的任何公共容器。

我承认这种方法很丑陋,但它确实有效。如果由于一个包中的类型使用了另一个包中的类型,然后尝试使用第一个包中的类型,导致循环依赖,那么解决方法是使用一个插件,将两个类型都擦除为interface{}。然后,您可以根据需要在接收方进行类型转换,进行方法和函数调用。

总结:
使用interface{}创建void指针(即无类型指针)。
使用Cast()将它们强制转换为与底层指针匹配的指针类型。使用插件类型本地化,以便在单独的插件中的主程序中的类型和主程序中不冲突。如果在插件之间使用一个公共包,那么所有构建的插件和主程序的路径必须相同。使用plug包加载插件,并交换函数指针。

对于我的问题之一,我实际上是从主程序的一个包调用插件,只是为了能够从调用插件的包回调到主程序的另一个包,避免了两个包之间的循环依赖。我在使用自定义编组方法的json和gob包时遇到了这个问题。我在主程序和其他插件中都使用自定义编组的类型,同时,我希望插件能够独立于主程序构建。为此,我使用了一个包含在主程序和插件中的json和gob编码/解码自定义方法的包。然而,我需要能够从编码器方法中回调到主程序,这给我带来了循环依赖的类型冲突。上述解决方案使用另一个插件来解决循环依赖问题。它确实创建了一个额外的函数调用,但我还没有看到其他解决这个问题的方法。

希望这对解决这个问题有所帮助。

英文:

Possible partial, but ugly answer:
Have struggled with the import cyclic dependency problem for a year. For a while, was able to decouple enough so that there wasn't an import cycle. My application uses plugins heavily. At the same time, it uses encode/decode libraries (json and gob). For these, I have custom marshall and unmarshall methods, and equivalent for json.
For these to work, the full type name including the package name must be identical on data structures that are passed to the codecs. The creation of the codecs must be in a package. This package is called from both other packages as well as from plugins.
Everything works as long as the codec package doesn't need to call out to any package calling it, or use the methods or interfaces to the methods. In order to be able to use the types from the package in the plugins, the plugins have to be compiled with the package. Since I don't want to have to include the main program in the builds for the plugins, which would break the point of the plugins, only the codec package is included in both the plugins and the main program. Everything works up until I need to call from the codec package in to the main program, after the main program has called in to the codec package. This will cause an import cycle. To get rid of this, I can put the codec in the main program instead of its own package. But, because the specific datatypes being used in the marshalling/unmarshalling methods must be the same in the main program and the plugins, I would need to compile with the main program package for each of the plugins. Further, because I need to the main program to call out to the plugins I need the interface types for the plugins in the main program. Having never found a way to get this to work, I did think of a possible solution:
First, separate the codec in to a plugin, instead of just a package
Then, load it as the first plugin from the main program.
Create a registration function to exchange interfaces with underlying methods.
All encoders and decoders are created by calls in to this plugin.
The plugin calls back to the main program through the registered interface.
The main program and all the plugins use the same interface type package for this.
However, the datatypes for the actual encoded data are referenced in the main program
with a different name, but same underlying type than in the plugins, otherwise the same import cycle exists. to do this part requires doing an unsafe cast. Wrote
a little function that does a forced cast so that the syntax is clean:
(<cast pointer type*>Cast(<pointer to structure, or interface to pointer to structure>).

The only other issue for the codecs is to make sure that when the data is sent to the encoder, it is cast so that the marshall/unmarshall methods recognize the datatype names. To make that easier, can import both the main program types from one package, and the plugin types from another package since they don't reference each other.

Very complex workaround, but don't see how else to make this work.
Have not tried this yet. May still end up with an import cycle when everything is done.

[more on this]

To avoid the import cycle problem, I use an unsafe type approach using pointers. First, here is a package with a little function Cast() to do the unsafe typecasting, to make the code easier to read:

package ForcedCast

import (
    &quot;unsafe&quot;
    &quot;reflect&quot;
)

// cast function to do casts with to hide the ugly syntax
// used as the following:
// &lt;var&gt; = (cast type)(cast(input var))
func Cast(i interface{})(unsafe.Pointer) {
    return (unsafe.Pointer(reflect.ValueOf(i).Pointer()))
}

Next I use the &quot;interface{}&quot; as the equivalent of a void pointer:

package firstpackage
type realstruct struct {
     ...
}   

var Data realstruct

// setup a function to call in to a loaded plugin
var calledfuncptr func(interface)

func callingfunc() {

        pluginpath := path.Join(&lt;pathname&gt;, &quot;calledfuncplugin&quot;)
        plug, err := plugin.Open(pluginpath)

        rFunc, err := plug.Lookup(&quot;calledfunc&quot;)
        calledfuncptr = rFunc.(interface{})

        calledfuncptr (&amp;Data)
}


//in a plugin
//plugins don&#39;t use packages for the main code, are build with -buildmode=plugin
package main

// identical definition of structure
type realstruct struct {
     ...
}   

var localdataptr *realstruct

func calledfunc(needcast interface{}) {

    localdataptr = (*realstruct)(Cast(needcast))

}

For cross type dependencies to any other packages, use the "interface{}" as a void pointer and cast appropriately as needed.

This only works if the underlying type that is pointed to by the interface{} is identical wherever it is cast. To make this easier, I put the types in a separate file. In the calling package, they start with the package name. I then make a copy of the type file, change the package to "package main", and put it in the plugin directory so that the types are built, but not the package name.

There is probably a way to do this for the actual data values, not just pointers, but I haven't gotten that to work right.

One of the things I have done is to cast to an interface instead of a datatype pointer. This allows you to send interfaces to packages using the plugin approach, where there is an import cycle. The interface has a pointer to the datatype, and then you can use it for calling the methods on the datatype from the caller from the package that called in to the plugin.

The reason why this works is that the datatypes are not visible outside of the plugin. That is, if I load to plugins, which are both package main, and the types are defined in the package main for both, but are different types with the same names, the types do not conflict.

However, if I put a common package in to both plugins, that package must be identical and have the exact full pathname for where it was compiled from. To accommodate this, I use a docker container to do my builds so that I can force the pathnames to always be correct for any common containers across my plugins.

I did say this was ugly, but it does work. If there is an import cycle because a type in one package uses a type in another package that then tries to use a type from the first package, the approach is to do a plugin that erases both types with interface{}. You can then make method and function calls back and forth doing the casting on the receiving side as needed.

In summary:
Use interface{} to make void pointers (that is, untyped).
Use the Cast() to force them to a pointer type that matches the underlying pointer. Use the plugin type localization so that types in the package main in separate plugins, and in the main program do not conflict If you use a common package between plugins, the path must be identical for all built plugins and the main program. Use the plug package to load the plugins, and exchange function pointers

For one of my issues I'm actually calling from a package in the main program out to a plugin, just to be able to call back to another package in the main program, avoiding the import cycle between the two packages. I ran in to this problem using the json and gob packages with custom marshaller methods. I use the types that are custom marshalled both in my main program, and in other plugins, while at the same time, I want the plugins to be built independent of the main program. I accomplish this by using a package for json and gob encode/decode custom methods that is included both in the main program and the plugins. However, I needed to be able to call back to the main program from the encoder methods, which gave me the import cycle type conflict. The above solution with another plugin specifically to solve the import cycle works. It does create an extra function call, but I have yet to see any other solution to this.

Hope this helps with this issue.

答案3

得分: 0

对于你的问题,我给出了一个更简短的答案(使用接口),这不会影响其他答案的正确性和完整性。下面是一个示例:

UserService 导致循环导入,实际上不应该从 AuthorizationService 中调用它。它只是用来提取用户详细信息,所以我们可以在一个单独的接收方接口 UserProvider 中声明所需的功能:

https://github.com/tzvatot/cyclic-import-solving-exaple/commit/bc60d7cfcbd4c3b6540bdb4117ab95c3f2987389

基本上,提取一个只包含接收方所需功能的接口,并在声明依赖项时使用它,而不是依赖于外部内容。

英文:

A shorter answer to your question (using interface), that does not take away the correctness and completeness of the other answers, is this example:

UserService is causing cyclic import, where it should not really be called from AuthorizationService. It's just there to be able to extract the user details, so we can declare only the desired functionality in a separated receiver-side interface UserProvider:

https://github.com/tzvatot/cyclic-import-solving-exaple/commit/bc60d7cfcbd4c3b6540bdb4117ab95c3f2987389

Basically, extracting an interface that contains only the required functionality on the receiver side, and use it instead of declaring a dependency on something external.

huangapple
  • 本文由 发表于 2013年12月5日 00:12:55
  • 转载请务必保留本文链接:https://go.coder-hub.com/20380333.html
匿名

发表评论

匿名网友

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

确定