返回嵌套结构的接口

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:

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