Facebook Linkedin Twitter
Posted Tue Dec 20, 2022 •  Reading time: 5 minutes

A Tour of Interfaces in the Standard Library

The Go standard library is a great place to learn about interfaces. You can both see how the Go team designs and use interfaces.

Let’s start by looking at some statistics. I wrote a short Go program that collects information

Running it with Go version 1.19.4 shows that the average interface size is 2.19 methods and the maximal interface size is 29. From this we can learn that interfaces are indeed small, following the Go proverb of “The bigger the interface, the weaker the abstraction.”. The huge interface with 29 methods is reflect.Type which is a union of every Go type.

Let’s start with the humble error which is defined as:

type error interface {
	Error() string
}

It’s pretty easy to define your own error and use it:

type AuthError struct {
	Login     string
	Reason    string
	RequestID string
}

func (e *AuthError) Error() string {
	return e.Reason
}

func (s *UserStore) Login(ctx context.Context, login, passwd string) (*User, error) {
	if !s.validPasswd(login, passwd) {
		err := AuthError{
			Login:     login,
			Reason:    "bad password",
			RequestID: ctxlib.RequestID(ctx),
		}
		return nil, &err
	}

	// Code redacted
	return &User{}, nil
}

Once you get an error, you can use errors.As to extract the underlying value and see more details about an error.

The next two we can look at are io.Reader and io.Writer, which are the building blocks for anything IO. io.Reader is defined as:

type Reader interface {
	Read(p []byte) (n int, err error)
}

I came to Go from Python, the read method there can be roughly translated to:

type Reader interface {
	Read(n int) (p []byte, err error)
}

I wondered why the Go team decided to use the version where the []byte is passed to be filled. The answer, like many things in Go, is performance. If io.Reader was written the Python way, every call to Read had to allocate a []byte since you don’t know what the user will do with it. In the Go version of io.Reader it’s up to the user. They can allocate a []byte on every call, or re-use the same []byte every time. Do the interface you write allow the user to make such decisions?

The io package also defines several interfaces that let your type signal it can handle copying of data more efficiently. The io.Copy function calls copyBuffer under the hood which has the following code:

func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
	// If the reader has a WriteTo method, use it to do the copy.
	// Avoids an allocation and a copy.
	if wt, ok := src.(WriterTo); ok {
		return wt.WriteTo(dst)
	}
	// Similarly, if the writer has a ReadFrom method, use it to do the copy.
	if rt, ok := dst.(ReaderFrom); ok {
		return rt.ReadFrom(src)
	}

    // Redacted
    ...
}

This way io.Copy can accept a wide range of types that implement io.Writer as destination and io.Reader and source. But, if the underlying destination implement io.WriterTo, copyBuffer will use it for more efficient copying. The same goes for the source implementing io.ReaderFrom.

You can use this lesson in your code as well. Accept interfaces that are implemented by many types (and are usually small), and on runtime check if they implement a more specialized interface. This can be done for efficiency or for extra capabilities. See http.Flusher for example on how to return chunked encoding.

Interfaces can be also used to change how the standard library handle your types. For example, if you want your type to have a special representation by the fmt package, you need to implement the fmt.Stringer interface. For example:


type Role byte

const (
	Reader Role = iota + 1
	Writer
	Admin
)

// String implements fmt.Stringer
func (r Role) String() string {
	switch r {
	case Reader:
		return "reader"
	case Writer:
		return "writer"
	case Admin:
		return "admin"
	}

	return fmt.Sprintf("<Role %d>", r)
}

Can you guess why you shouldn’t use %s or %v in the return fmt.Sprintf("<Role %d>", r) line? Check your favorite linter for an answer :)

If you have enum-like types in your code, can use the stringer tool to generate code for you. I recommend trying to use stringer and then studying its output, there are some good lessons there as well.

And if you want even finer control on how your types are printed, see the fmt.Formatter and fmt.GoStringer interfaces.

The encoding/json package let you define custom marshaling and un-marshaling using the json.Marshaler and json.Unmarshaler interface. For example:

type Unit string

const (
	Celcius    Unit = "c"
	Fahrenheit Unit = "f"
	Kelvin     Unit = "k"
)

// Value if a measurement value
type Value struct {
	Amount float64 `json:"value"`
	Unit   Unit    `json:"unit"`
}

func (v Value) MarshalJSON() ([]byte, error) {
	// Step 1: Convert to a type encoding/json can handle
	s := fmt.Sprintf("%f%s", v.Amount, v.Unit)
	// Step 2: Use json.Marshal
	return json.Marshal(s)
	// Step 3: There is no step 3
}

func (v *Value) UnmarshalJSON(data []byte) error {
	var a float64
	var u Unit

	// Trim ""
	s := string(data[1 : len(data)-1])
	if _, err := fmt.Sscanf(s, "%f%s", &a, &u); err != nil {
		return err
	}

	v.Amount = a
	v.Unit = u
	return nil
}

Another usage for interfaces is to give the user an ability to override implementation. This can be used when mocking errors.

Supposed you have an API client that looks like:

type APIClient struct {
	c       http.Client
	baseURL string
}

func (c *APIClient) Health() error {
	url := fmt.Sprintf("%s/health", c.baseURL)
	resp, err := c.c.Get(url)

	if err != nil {
		return err
	}

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf(resp.Status)
	}
	return nil
}

If you’d like to test the client for errors, you need to inject the error somewhere. Looking at http.Client, you see it has a Transport field of type RoundTripper - which is an interface. In your tests, you can change the Transport field to your own implementation of RoundTripper that will err. Probably something along the lines of:

type errTripper struct {
	code int
}

// RoundTrip implements http.RoundTripper
func (e *errTripper) RoundTrip(*http.Request) (*http.Response, error) { // HL
	return &http.Response{StatusCode: e.code}, nil
}

func TestHealthError(t *testing.T) {
	c := APIClient{}
	c.c.Transport = &errTripper{http.StatusInternalServerError} // HL

	err := c.Health()
	if err == nil {
		t.Fatal(err)
	}
}

There’s much more to learn from interfaces in the standard library, here are a few place you can explore:

  • The examples in the sort package
  • http.HandlerFunc, which is a function implementing the http.Handler
  • [testing.TB][tb] is the interface common to T, B, and F. I use in utility function that are called both from tests and benchmarks.
  • flag.Value allow you to use your types with the flag package

And there are many more!

What is your favorite standard library interface? What did you learn from it? I’d love to hear from you at miki@353solutions.com Happy holidays!