Engineering Blog

                            

Returning Interfaces

While designing a function signature, we may have to return either an interface or a concrete implementation. Let’s understand why returning an interface is, in many cases, considered a bad practice in Go.

We know that interfaces live, in general, on the consumer side. Let us consider, there is producer package, we define an producerstore struct that implements the producer interface. Meanwhile, we create a NewProducerStore function to return a producer interface. This causes the following problems:-

  • There’s a dependency from the implementation package to the client package in this design.
  • The client package can’t call the NewProducerStore function anymore; otherwise, there would be a cyclic dependency.
  • A possible solution could be to call this function from another package and to inject a Producer implementation to client. However, being obliged to do that means that the design should be challenged.

Hence, in general, returning an interface restricts flexibility because we force all the clients to use one particular type of abstraction.

We can consider following as a good practice in Go,

  • Returning structs instead of interfaces
  • Accepting interfaces if possible

We have the following advantages of accepting interfaces not returning interface

  • Looser coupling, greater flexibility

By accepting interfaces, consumers are not coupled with their dependencies. This retains the flexibility of using any implementation as long as it satisfies the consumer defined interface

  • Easier testing

Testing would also be made simpler as we can easily pass in an mock without having to spin up an actual dependency instance which could be expensive just for the sake of unit testing. We can just have a mock with the appropriate data needed for our test cases.

The given example shows how we can pass interface and return struct:-

type Store struct {
 db *sql.DB
}

func NewDB() *Store {
 return &Store{
  //inilitize database connection
 }
}
func (s *Store) GetUser(userID string) (user.User, error) {
 //return user data
}
func (s *Store) UpdateUser(u user.User) (user.User, error) {
 // return user data after updating
}
package user

type Store interface {
 GetUser(userID string) (User, error)
 UpdateUser(u User) (User, error)
}

type User struct {
 ID    string
 Email string
}

type Service struct {
 Store Store
}

func New(store Store) *Service {
 return &Service{
  Store: store,
 }
}

func (s *Service) UpdateUserPreferences(userID string) (User, error) {
 u, err := s.Store.GetUser(userID)
 if err != nil {
  return User{}, err
 }
 _, err = s.Store.UpdateUser(u)
 if err != nil {
  return User{}, err
 }
}

In above case we gave consumer power to decide the interface and implement their methods as they see fit, and also mocking for test cases will also be easier as we can just pass a mock interface in new function for unit tests. we can call the above function using

user.New(db.NewDB()).UpdateUserPreferences("1")

All in all, in most cases, we shouldn’t return interfaces but concrete implementations. Otherwise, it can make our design more complex due to package dependencies and can restrict flexibility because all the clients would have to rely on the same abstraction. Again, the conclusion is similar to the previous sections: if we know (not foresee) that an abstraction will be helpful for clients, we can consider returning an interface. Otherwise, we shouldn’t force abstractions; they should be discovered by clients. If a client needs to abstract an implementation for whatever reason, it can still do that on the client’s side.

Conclusion:-

We can limit the use of returning the interface type in our implementation to help in testing and reuseablility of our methods. And using concrete return type helps to implement dynamic interface in client side. But we can also go for primitive interface case if we know that the client only need one type of implementation.

References:-

  • 100 Go Mistakes and how to avoid them, Teiva Harsanyi, Manning Publications Co
  • https://bryanftan.medium.com/accept-interfaces-return-structs-in-go-d4cab29a301b
  • https://tutorialedge.net/golang/accept-interfaces-return-structs/
Previous Post
Next Post