Search code examples
godependencies

Best way to handle decoupling in Go with similar structs in two different packages but subitem in struct making it difficult?


I'm relatively new to go and have been doing a massive rewrite trying to reduce my dependency graph as much as possible. I'm pretty happy with where I got it but there is this one part I am not sure how to best handle. If the answer is, "You are going to have that dependency between the two", that's fine too, I'm just looking for a good approach, not expecting miracles.

So below I have two packages, a & b, and they both have identical structs. Normally you could convert one to the other in main but each has a subitem which is also a struct and that's stopping Go from allowing it even though the subitems have identical signatures.

One way would be to just reference A.TzConfig in the struct in B and let there be a dependency but that's what I am trying to get rid of.

I guess another way is to create an interface and then get the values of Loc through methods, I think that would work but I haven't tried it yet as that means creating methods for something that's just a structure of data (the actual structure has a lot of items, I reduced it to the essentials for simplicity here) which seems like overkill.

I could move TzConfig into a third module so they would both reference that instead of one referencing the other and that's about all I have thought of.

So my question is, from someone with a real experience, what would be the best way to deal with this scenario in go?

I should mention that the reason they have duplicated structs was just because I was trying to break the dependency between them, the original code just had the struct in one package and the other package referencing it.

package a

type Cfg struct {
    Addr                 string
    Loc                  TzConfig
}

type TzConfig struct {
    String string
    TZ     *time.Location `validate:"noDescent"`
}

func GetCfg() Cfg {
    t, _ := time.LoadLocation(`MST`)
    return Cfg{
        Addr: "abc",
        Host: "a.bc.d",
        Loc:  config.TzConfig{
            String: "MST",
            TZ:     t,
        },
    }
}
package b

type Cfg struct {
    Addr                 string
    Loc                  TzConfig
}

type TzConfig struct {
    String string
    TZ     *time.Location `validate:"noDescent"`
}

func DoSomethingWithConfig(c Cfg) {
    fmt.Println(c)
}
package main

main() {
     c := a.GetCfg()
     d := b.DoSomethingWithConfig(b.Cg(c))
     fmt.Println(d)
}

Solution

  • IMHO, the suggestion provided by @BurakSerdar are completely fine and fits very well for your scenario. I've rewritten the code in this way.

    package common

    package common
    
    import "time"
    
    type Cfg struct {
        Addr string
        Loc  TzConfig
    }
    
    type TzConfig struct {
        String string
        TZ     *time.Location `validate:"noDescent"`
    }
    

    Here, you should put the common structs, functions, methods, and so on.

    package a

    package a
    
    import (
        "dependencies/common"
        "time"
    )
    
    type Cfg struct {
        common.Cfg
        Host string
    }
    
    func GetCfg() Cfg {
        t, _ := time.LoadLocation(`MST`)
        return Cfg{
            Cfg: common.Cfg{
                Addr: "abc",
                Loc: common.TzConfig{
                    String: "MST",
                    TZ:     t,
                },
            },
            Host: "a.bc.d",
        }
    }
    

    Here, you have the specific code related to the package a that inherits the shared code from the common package, as you can see in the import section.

    Please note that I used the structs embedding feature to get the shared fields defined within the common package.

    package b

    package b
    
    import (
        "dependencies/common"
        "fmt"
    )
    
    func DoSomethingWithConfig(c common.Cfg) string {
        return fmt.Sprint(c)
    }
    

    Here, there is nothing special to mention.

    package main

    package main
    
    import (
        "dependencies/a"
        "dependencies/b"
        "fmt"
    )
    
    func main() {
        c := a.GetCfg()
        d := b.DoSomethingWithConfig(c.Cfg)
        fmt.Println(d)
    }
    

    Here, the code should be pretty straightforward. I imported packages a and b to exploit their functionalities.

    Again, I'd like to be clear that this is a subjective topic, hence there isn't a silver bullet solution. To me, looks neat and clear. I would have chosen this way for sure. Let me know and thanks!