There is an idiom in go

Accept interface return struct

By following this simple rule (where it makes sense), there are several things that comes naturally.

  • Mocking becomes easier because of dependency injection
  • Structs can be extended and reused when other interfaces are needed.

All this is possible because Go uses dynamic typing to satisfy the interfaces. So consider that before you dump your EE Java fu on me.

Common counter arguments

Preemptive interface anti pattern

If we define interfaces too early, we might be prematurely plan for complexity.

Exposing unexported struct

If you come from a Java background, you might find it natural to map into a interface implementation paradigm, as follows:

type FooInterface interface {
    DoFoo()
}

type fooImpl struct{
    X int
}

func New(x int) FooInterface {
    return &fooImpl{X:x}
}

func (f *fooImpl) DoFoo() {}

You might find that returning FooInterface makes more sense since we are returning an exported type. However, as long as the fields and methods are exported, it doesn’t matter if the actual struct is unexported.

That is to say, suppose we have


// in package A
func BetterNew(x int) *fooImpl {
    return &fooImpl{X:x}
}

// In package B
func main() {
    f := A.BetterNew(0)
    x := f.X // we can access the exported fields
    f.DoFoo() // we can call exported methods
}

So we do not need to worry about exporting only the interface to other packages.

Shorter interfaces

The most powerful interface is io.Reader()

In Go, the smaller the interface, the more useful it is. Referring to the code before this. It is not wrong per se to write code like the above. The issue is the code becomes less reusable and extensible. Suppose now we want to have a new interface


type BarDoer interface {
    DoBar()
}

func myCoolFunc(b BarDoer) {
    b.DoBar()
}

If we had the first snippet where we returned the interface, we would not be able to reuse it and pass to myCoolFunc. If we however had the second snippet, we could add a new method that satisfies the BarDoer interface and we’re good.

There are other ways to get around this paradigm as well. If you insist on returning an interface from a constructor, you could have another constructor and an interface that composes both interface but I think it is unnecessary boilerplate and perhaps you want to return to Java.