Timo Savola

2018-10-13

Adding methods to Go interfaces

This article is about a known technique, but quick googling didn't bring up any concise texts on the subject. Anyway, I hope this elaborates on it.

Default implementations

Interface types cannot be expanded with new methods without breaking backward-compatibility (unless the programming language supports default method implementations). That can be worked around by providing a concrete base type, and stating in documentation that all implementations should inherit its methods. New interface methods can be implemented in the base type, insofar a sensible default exists.

In Go, it looks like this:

package api

// Handler implementations MUST inherit DefaultHandler's methods.
type Handler interface {
    HandleThis(This)
    HandleThat(That)
}

type DefaultHandler struct{}

func (DefaultHandler) HandleThis(This) {}
func (DefaultHandler) HandleThat(That) {}
package user

import "api"

type MyHandler struct {
    api.DefaultHandler
}

The problem with this is that it's a social contract. It depends on all developers reading and obeying the documentation, and not making mistakes.

Fortunately, Go has a trick which can be used to enforce inheritance:

package api

type Handler interface {
    HandleThis(This)
    HandleThat(That)
    handler() // Unexported method can only be implemented in this package.
}

type DefaultHandler struct{}

func (DefaultHandler) HandleThis(This) {}
func (DefaultHandler) HandleThat(That) {}
func (DefaultHandler) handler() {}

No downstream type can satisfy this Handler interface without inheriting methods from DefaultHandler (or embedding a nil Handler). The human condition doesn't enter into it.

Naming conflicts

But there is still a possibility of breakage when the interface is expanded: an implementation may already have a method with the same name. If the signatures differ, the breakage is detected during compilation and people are immediately unhappy. On the other hand, if the signatures match, the semantics might not, and it may take time before people get really unhappy.

I can think of three solutions. The lightweight and fragile approach is to document a reserved namespace:

package api

// Handler might be expanded with new methods between major releases.
// The method names will have the "Handle" prefix, so don't use it in
// your own method names.
type Handler interface {
    HandleThis(This)
    HandleThat(That)
    handler()
}

The heavy-weight approach is to check namespace violations at every site which accepts the interface. It could be done by scanning the implementation type's fields using reflection. (I didn't write an example of that. It would be over the top.)

The final approach is to not use interfaces for injecting custom behavior like this. A struct with function fields can be expanded without problems.