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.