0

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.

2
  • 3
    "I was expecting config to implicitly satisfy the iConfig interface but that's not happening." That is actually what is happening, but just because config implements iConfig that doesn't mean that system implements iSystem, the method signatures are different and that is the reason why you're getting the error. The error message is pretty clear about that with the have vs want explanation. To fix it, you can update the signature of the system's getConfig method to return the interface instead of the concrete type. Commented Mar 24, 2019 at 11:27
  • I can't do that because I'm trying to make each package declare its own private interface for the type of object it needs. I have multiple packages each declaring some variation of the methods from the concrete objects they need. If I go down the route of having a single interface again I'll end up with circular imports as before. Commented Mar 24, 2019 at 11:36

2 Answers 2

1

For your case, you may implement three packages for circular imports resolving:

  1. Config package

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
}

  1. System package

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
}
  1. Third-party package

Usage example:

package main

import (
    "fmt"
    // "config"
    // "system"
)

func UseConfig(cfg config.IConfig) {
    fmt.Printf("region=%s\n", cfg.GetRegion())
}

func UseSystem(s system.ISystem) {
    fmt.Printf("region=%s\n", s.GetConfig().GetRegion())
}

func main() {
    cfg := config.New("myregion")
    s := system.New("mysystem", cfg)

    UseConfig(cfg)
    UseSystem(s)
}
Sign up to request clarification or add additional context in comments.

2 Comments

That could work, but with this approach any private methods on config or system would need to become public. Is there any way of keeping those methods private?
@jbrown Not any, but only which you're going to use outside of the package. An interface is a contract of interaction with your application unit, i.e. the way how your package could be used. So, if you want to use a method outside, it must be public.
1

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
```

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.