Mat Ryer

April 23, 2021

Tiny abstractions: Function hooks in Go

Function hooks are single method interfaces without the interface.

// GetPersonFunc is a func that gets a Person by ID.
type GetPersonFunc func(ctx context.Context, personID string) *Person

I use them;

  • when my service code is still young, before any firm abstractions have emerged - see Tiny abstraction example
  • when I want to plug into a third-party API in the loosest possible way
  • to get the time
  • and sometimes just to make a nice API

Getting the time

Before I get into a more meaty example, I'll quickly show you how I get the time when I'm writing Go code:

type Game struct {
  // now gets the time.
  now func() time.Time
}

func (g *Game) update() {
  rightNow := g.now()
  // ...
}

I could call time.Now inside the update method, but abstracting it like this gives me two advantages:

  1. I can plug in my own function in test code to control time
  2. I also make it clear that this object makes use of the time, rather than hiding its use inside a method body

This same technique can be used to build a loose integration between ideas in code. Perhaps before they've had time to mature into something more official, or perhaps forever. I still have some lying around in quite mature code. 

Tiny abstraction example

A simple example of this technique is where a service I'm writing needs to be able to do something that obviously doesn't belong within its own remit. 

For example, when adding a comment, the CommentsService likes to store the author's ID, display name, and photo URL.

Do we define an interface to handle people related things like this? Maybe, maybe not. We're not sure yet.

For now, let's just make a little hook:

// CommentsService provides commentary capabilities.
type CommentsService struct {
    // GetPerson gets a person.
    GetPerson func(ctx context.Context, personID string) *Person
}

Here, GetPerson is a field just like any other, except that it has a special type; a function that takes a context and a string, and returns a *Person.

We can make a CommentsService in test code:

comments := &CommentsService{
  GetPerson: func(ctx context.Context, personID string) (*Person, error) {
    if personID == "1" { 
      return &Person{ID: "123", Name: "Jonathan Batman"}, nil
    }
    return nil, errNotFound
  },
}

Here we inject a function that returns a Person when personID is "1" and nothing otherwise. This allows us to simulate an authentication system, before we have a real one.

Better still, you'd use a real implementation (yeah even in test code), but maybe we've deferred decisions about the structure of our project for the time being.

Let's say our Server just has a little temporary method on it:

type Server struct {
  // server related things
    
  db *datastore.Thing
}

func (s *server) getPerson(ctx context.Context, personID string) 
                                                        (*Person, error) {
  doc, err := s.datastore.Get("person", personID)
  if err != nil {
    if err == datastore.NotFound {
      return nil, errNotFound
    }
    return nil, errors.Wrap(err, "datastore")
  }
  return docToPerson(ctx, doc)
}    

On reflection, this code is pretty typical of the kind of thing I end up writing a lot. I don't mind doing it, because it makes me think about what I want to happen in each error case. Sometimes it's okay if something doesn't exist, other times the user will need to know it's a 404.

Now in my setup code, I can use this implementation in my services:

commentsService.GetPerson = srv.getPerson
reactionsService.GetPerson = srv.getPerson
filesService.GetPerson = srv.getPerson

type func or not?

In the example above, the signature of the function hook was defined inline where the struct itself was being defined. I recommend that people keep this pattern for a while, before jumping into defining types.

Once an idea is more firm in the system, you might like to formalise these function hooks, and give them a little context.

You can do this in Go by creating a function type:

// GetPersonFunc is a func that gets a Person by ID.
type GetPersonFunc func(ctx context.Context, personID string) *Person

GetPersonFunc is a type (like a struct, a string, or an int) that describes a function signature. This type gets its own comment, allowing you to explain any non-obvious details about what you're expecting this function to do.

Our CommentsService from the Tiny abstractions example above would now look like this:

// CommentsService provides commentary capabilities.
type CommentsService struct {
    // GetPerson gets a person.
    GetPerson GetPersonFunc
}

This pattern may be familiar: it's used when walking file trees with filepath.Walk

If you're at the point of adding function types in this way and you notice a couple are related, you may want to consider if it's time to migrate to an interface.

I am a big fan of this technique; letting the code reveal the abstractions, especially in cases where there are no clear concepts yet.

What do you think?

-Mat   @matryer

p.s. I think it needs an image in the body in order to look nice when sharing, so here's an image that kind of represents small abstractions in a way, doesn't it?

cubes-3381438_1920.jpg