Go Experience Report: Interfaces with Methods that Return Themselves

I want to describe a scenario where there currently isn’t a good solution in Go (at least that I am aware of). Let’s say you have something like the template.Template type with methods like Template.Funcs:

type Template struct {
  // contains filtered or unexported fields

func (t *Template) Funcs(funcMap FuncMap) *Template

If we wanted to use an interface for this type, it is impossible to express in Go now. At first you would think that you could use an interface like this:

type Funcser interface {
  Funcs(FuncMap) Funcser

But if you try it out in Go it won’t work. While our template.Template type appears to match this definition, template.Funcs method returns the *template.Template type, NOT the Funcser type, and that means it wont implement this interface.

Truthfully, I’m not certain that every implementation of generics would solve this issue, nor do I feel that generics are required to solve the issue, but if generics are added to Go I’d love to see an implementation that somehow makes this situation a little better.

Followup - A More Concrete Example

It was pointed out (and I agreed) that this post is a little short on details. I’m going to try to expand a bit here in order to give a better example of why this matters and how it can affect a real application.

According to the Experience Reports wiki:

The best experience reports tell: (1) what you wanted to do, (2) what you actually did, and (3) why that wasn’t great, illustrating those by real concrete examples, ideally from production use.

I’ll start with these three points, as I think they are enough to really illustrate the experience.

(1) what you wanted to do

Using GORM is where this comes up most frequently, but it has happened with other packages in the past. In GORM you typically create queries by chaining something like this:

db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)

My issue is that the second I introduce this code into my application, I can’t easily test it because I can’t replace the *gorm.DB type with an interface. I now need either a live database connection for my tests, or I need to write a fairly large wrapper around GORM. That is, I can’t write code like this:

type WhereOrFinder interface {
  func Where(...) WhereOrFinder
  func Or(...) WhereOrFinder
  func Find(...) *gorm.DB

func admins(db WhereOrFinder) []User {
  var users []Users
  err := db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users).Error
  return users, err

And then test it with a mock.

Now before I move on, I’m guessing that most people are going to stop here and say, “Woah, you returned *gorm.DB on that Find method! Why not return it for all of the methods?” This doens’t work because then the chained methods would no longer be using the mock. I can create a mock like this:

type mockDB struct {}
func (m mockDB) Where(...) mockDB { return m }
func (m mockDB) Or(...) mockDB { return m }
func (m mockDB) Find(...) *gorm.DB { return &gorm.DB{Error: someError} }

And it will work because my code doesn’t leave the mock until the final result is returned, but if I were to instead create a mock like this:

type WhereOrFinder interface {
  func Where(...) *gorm.DB
  func Or(...) *gorm.DB
  func Find(...) *gorm.DB
type mockDB struct {}
func (m mockDB) Where(...) *gorm.DB { 
  // If I return a gorm.DB here the rest of my code will interact 
  // with a REAL gorm db, not my mock!
func (m mockDB) Or(...) *gorm.DB { return ... }
func (m mockDB) Find(...) *gorm.DB { return &gorm.DB{Error: someError} }

You could make the argument that occasionally I will want to hit a live DB in my tests and I agree, but I don’t want to do that for literally every single test that uses gorm.DB. It slows things down, and it means I need to do some sort of DB “reset” (or rollback) after each test to make sure I start with a clean slate. In an ideal world I would instead be testing with a mock.

(2) what you actually did

One of two things:

I tend to end up going with Option 1 most often because it is clear what is going on and that we use GORM, whereas option 2 requires me to figure out all the methods we might use w/ GORM, make sure other devs know to add new ones they need if they aren’t in the mock, and so on.

(3) why that wasn’t great, illustrating those by real concrete examples, ideally from production use.

Interfaces are, in my opinion, great in Go because they are easy to use and because someone creating a type doesn’t have to go out of their way to define all the interfaces it implements. That is, I don’t have to write something like:

type Dog implements <Sortable, Stringer, ...>

Duck typing and the simplicity it brings to Go’s interfaces are (again imo) what allows the “accept interfaces, return structs” mantra to work.

Having to create a wrapper or simply not use an interface isn’t great because it is directly contradictory to this point. When we have a type that returns itself in a method, package developers must explicitly define the interface themselves, or users of the type will need to create an often large wrapper to enable that behavior. You can see this in the example I linked above for a GORM wrapper - the code there is required to overwrite every single method we might use in the original gorm.DB type and update them all to return a specific interface instead. It works, but it just never felt ideal.

It is probably also worth noting that this is a big part of the reason why I tend to push back when someone wants to use method chaining in Go instead of another approach like functional options.

Learn Web Development with Go!

Sign up for my mailing list and I'll send you a FREE sample from my course - Web Development with Go. The sample includes three chapters from the book, and over 2.5 hours of screencasts.

You will also receive notifications when I release new articles, along with other freebies that I only share with my mailing list.

Avatar of Jon Calhoun
Written by
Jon Calhoun

Jon Calhoun is a full stack web developer who also teaches about Go, web development, algorithms, and anything programming related. He also consults for other companies who have development needs. (If you need some development work done, get in touch!)

Jon is a co-founder of EasyPost, a shipping API that many fortune 500 companies use to power their shipping infrastructure, and prior to founding EasyPost he worked at google as a software engineer.

Related articles

Spread the word

Did you find this page helpful? Let others know about it!

Vote on Hacker News

Sharing helps me continue to create both free and premium Go resources.

Want to discuss the article?

See something that is wrong, think this article could be improved, or just want to say thanks? I'd love to hear what you have to say!

You can reach me via email or via twitter.

Recent Articles Mini-Series Tags About Me Go Courses

©2018 Jonathan Calhoun. All rights reserved.