I'm trying to break circular dependencies in a reasonably sized golang project by using interfaces. I have some nested structs like this:
// these are in one package...
type config struct {
region string
}
type system struct {
name string
config config
}
func (s system) getName() string {
return s.name
}
func (s system) getConfig() config {
return s.config
}
func (c config) getRegion() string {
return c.region
}
And in another package that wants to use them I'm declaring some corresponding nested interfaces:
type iConfig interface {
getRegion() string
}
type iSystem interface {
getName() string
getConfig() iConfig
}
// and has functions like
func New(system iSystem) {
fmt.Printf("region=%s", system.getConfig().getRegion())
}
But when I try to use them like this:
theSystem := system{
name: "testName",
config:config{
region:"testRegion",
},
}
New(theSystem) // doesn't work
I get an error:
cannot use theSystem (type system) as type iSystem in argument to New:
system does not implement iSystem (wrong type for getConfig method)
have getConfig() config
want getConfig() iConfig
It seems that because my concrete system
struct returns a concrete type config
Go doesn't think it satisifes the iConfig
interface - even though I can use config
via the iConfig
interface directly. I was expecting config
to implicitly satisfy the iConfig
interface but that's not happening. How can I fix this?
Here's a link to the Go Playground.
For your case, you may implement three packages for circular imports resolving:
Obtains a configuration for the application and provides it in a convenient way for its components.
package config
type IConfig interface {
GetRegion() string
}
type config struct {
region string
}
func New(region string) IConfig {
return &config{
region: region,
}
}
func (c config) GetRegion() string {
return c.region
}
Produces a system based on the configuration of your application.
package system
import (
// "config"
)
type ISystem interface {
GetName() string
GetConfig() config.IConfig
}
type system struct {
name string
config config.IConfig
}
func New(name string, cfg config.IConfig) ISystem {
return &system{
name: name,
config: cfg,
}
}
func (s system) GetName() string {
return s.name
}
func (s system) GetConfig() config.IConfig {
return s.config
}
Usage example:
package main
import (
"fmt"
// "config"
// "system"
)
func UseConfig(cfg config.IConfig) {
fmt.Printf("region=%s
", cfg.GetRegion())
}
func UseSystem(s system.ISystem) {
fmt.Printf("region=%s
", s.GetConfig().GetRegion())
}
func main() {
cfg := config.New("myregion")
s := system.New("mysystem", cfg)
UseConfig(cfg)
UseSystem(s)
}
An interface is a signature/contract which other types can comply to.
The signature can include one or more method signatures, which means method names, arguments (include the types) and the return arguments. If it doesn't include any method, it is actually the infamous interface{}
type, which every type complies to.
To comply to an interface a type must strictly implement the complete signature, including all passed and returned arguments and their types.
Interfaces and structures are different types.
Therefore, in the following example Type2
does not implement Intf2
:
type Intf1 interface{
Bar()
}
type Type1 struct {}
func (s SomeType) Bar() {}
type Intf2 interface{
Foo() Intf1
}
// Type2 does not implement Intf2
type Type2 struct {}
func (s Type2) Foo() Type1 {}
// Won't compile
var _ Intf2 = Type2{}
Because the go compiler considers the signature of the method Foo
on Intf2
to be different because the return type is different. The compiler does not infer that the return type also implements the interface, because it will bring a lot of complexity to do so.
If you want this example to work, you'd need to change Type2
to this:
// Type2 does not implement Intf2
type Type2 struct {}
func (s Type2) Foo() Intf1 {}
This also applies to passed arguments, not only return arguments.
Now, regarding circular dependencies, I suggest you expose your interfaces in a third package, which acts as the glue and the top level package. One of the common things to do is to have a main package which composes with interfaces to achieve its main purpose.
Example:
pkg config
type Source interface{}
type Parser interface{}
pkg parsers
pkg jsonparser
implements config.Parser
pkg yamlparser
implements config.Parser
pkg sources
pkg filesource
implements config.Source
```