Interfaces And Polymorphism In Go: Understanding Go's Approach To Interfaces And Dynamic Behavior In Type Systems

Overview

Interfaces in Go provide a way to specify the behavior of an object: If something can do this, then it can be used here. This chapter will take a look at how to use interfaces to abstract that behavior. Concepts such as the Empty Interface, satisfying multiple interfaces, and asserting for behavior will be covered. Additionally, this chapter will cover the difference between value and pointer receivers and how they affect the ability to satisfy an interface.

Interfaces

The larger the interface, the weaker the abstraction. – Rob Pike

Interfaces allow us to specify behavior. They are about doing, not being.

Unlike other languages interfaces in Go are implicitly not explicitly defined.

Interfaces are found all around the standard library.

Concrete Types Vs. Interfaces

Imagine you own a venue, such as a night club. You want to have music at the venue. Do you require that the entertainer be a Beatle or would any musician do?

That is the difference between a concrete type and an interface.

A Beatle is a concrete type. You are either a Beatle or you are not. There are only two people alive that can fulfill that requirement.

A musician, however, is anyone who can play an instrument.

Why Interfaces?

Interfaces allow us to abstract code to make it more reusable, extensible and more testable.

For example, imagine a function that takes Beatles, but only calls the Play method on it.

func entertain(b Beatle) {
 b.Play()
}

In order for someone to play at our venue they must be a Beatle.

Using an interface allows us to open that function up to a larger group of potential types that can implement the interface.

type Entertainer interface {
 Play()
}

func entertain(e Entertainer) {
 e.Play()
}

Now we can write our own implementation of the Entertainer interface when writing tests to make them easier to work with. We also open up our application to allow for more than the two living Beatles to play our venue. We can now have actors, dancers, any musician, or anything that implements the Entertainer interface.

Here is what the final program would look like using an interface:


package main

import "fmt"

type Entertainer interface {
	Play()
}

type Beatle struct {
	Name       string
	Instrument string
}

func (b Beatle) Play() {
	fmt.Printf("%s plays %s", b.Name, b.Instrument)
}

func main() {
	b := Beatle{Name: "Ringo", Instrument: "Drums"}
	entertain(b)
}

func entertain(e Entertainer) {
	e.Play()
}

Duck Typing Or Structural Typing In Go

In most OO (object oriented) langauges you have to explicity declare that you are implementing an interface.

For example, in C# you have to list the interfaces your class implements:

interface IMyInterface {
 void Behaviour();
}

class MyObject : IMyInterface { // <<<
 public void Behaviour() { }
}

However, in Go this is implicit.

Provided a type implements all the behaviors specified in the interface, it can be said to implement that interface.

The compiler will check to make sure a type is acceptable and will report an error if it does not.

Sometimes this is called Duck Typing, but since it happens at compile-time in Go, it is called Structural typing.

This has some interesting side effects:

  • The concrete type does not need to know about your interface
  • You are able to write interfaces for concrete types that already exist
  • You can write interfaces for other people’s types, or types that appear in other packages

Defining An Interface

You can create an interface with the interface keyword.

type MyInterface interface {}
  • Interfaces define behavior, therefore they are only a collection of methods.
  • Interfaces can have zero, one, or many methods.
type MyInterface interface {
    Method1()
    Method2() error
    Method3() (string, error)
    .
    .
    .
}

It is important to note that interfaces are a collection of methods, not fields.

// valid
type Writer interface {
 Write(p []byte) (int, error)
}

// invalid
type Emailer interface {
 Email string
}

Writer Interface

One of the most well known interfaces in Go is the io.Writer interface.

type Writer interface {
 Write(p []byte) (int, error)
}

The io.Writer interface in the standard library requires the implementation of a Write method that matches the signature of Write(p []byte) (n int, err error).

Using Interfaces

Because interfaces are types, we can receive them as arguments to functions.


package main

import (
	"io"
	"os"
)

func main() {
	writeSomething(os.Stdout)
	// Hello
}

func writeSomething(w io.Writer) {
	w.Write([]byte("Hello"))
}

Let’s break down what’s happening in this example:

  1. Function Parameter: The writeMessage function accepts an io.Writer interface as a parameter
  2. Interface Implementation: os.Stdout implements the io.Writer interface because it has a Write method that matches the interface signature
  3. Automatic Satisfaction: Go automatically recognizes that os.Stdout satisfies the io.Writer interface - no explicit declaration needed
  4. Method Call: Inside the function, we call the Write method on the interface, which gets dispatched to the actual implementation

This demonstrates the power of Go’s implicit interface satisfaction - we can pass any type that implements the required methods.

os.Stdout is an os.File which defines the Write method.

Interface Example


type scribe struct {
	data []byte
}

func (s *scribe) Write(p []byte) (int, error) {
	s.data = p
	return len(p), nil
}

Let’s examine the key parts of this custom type:

  1. Custom Type Definition: We define a Scribe struct that will implement the io.Writer interface
  2. Method Implementation: The Write method signature matches exactly what the io.Writer interface requires: Write(p []byte) (n int, err error)
  3. Custom Behavior: Instead of writing to a file or stdout, our implementation stores the data in the struct’s data field
  4. Return Values: We return the number of bytes written and nil for the error, following the interface contract

By implementing the Write method with the proper signature, we don’t have to explicitly declare our type as an io.Writer - the compiler automatically recognizes that our type satisfies the interface.

Implement The Write Interface


package main

import (
	"fmt"
	"io"
)

type scribe struct {
	data []byte
}

func (s *scribe) Write(p []byte) (int, error) {
	s.data = p
	return len(p), nil
}


func main() {
	s := &scribe{}
	writeSomething(s)
	fmt.Println(string(s.data)) // Hello
}

func writeSomething(w io.Writer) {
	w.Write([]byte("Hello"))
}

Multiple Interfaces

Because interfaces are implemented implicitly, it means that types can implement many interfaces without even realizing it.


type scribe struct {
	data []byte
}

// implements the io.Writer interface
func (s *scribe) Write(p []byte) (int, error) {
	s.data = p
	return len(p), nil
}

// implements the fmt.Stringer interface
func (s scribe) String() string {
	return string(s.data)
}

Assignability

Remember that when determining assignability, Go is only concerned with the method set of an interface–not the name of the interface:


type Greeter interface {
	Greet(string)
}

type Welcomer interface {
	Greet(string)
}

In the eyes of the Go type system, these two interfaces are equivalent, so anything satisfying one interface will also satisfy the other.


p := &Person{}
var g Greeter = p
var w Welcomer = g

g.Greet("world")
w.Greet("tim")

Inheritance Vs. Composition Walthrough

Polymorphism is a key piece of OOP design. It allows for objects of different types to be accessed through a common interface.

In most OO languages this is done with inheritance: a base Shape type declares an area behavior, and each concrete shape (Circle, Rectangle) inherits from it and overrides that behavior.

Java:

abstract class Shape {
    abstract double area();
}

class Circle extends Shape {
    double radius;
    double area() { return Math.PI * radius * radius; }
}

class Rectangle extends Shape {
    double width, height;
    double area() { return width * height; }
}

C#:

abstract class Shape {
    public abstract double Area();
}

class Circle : Shape {
    public double Radius;
    public override double Area() => Math.PI * Radius * Radius;
}

class Rectangle : Shape {
    public double Width, Height;
    public override double Area() => Width * Height;
}

C++:

class Shape {
public:
    virtual double area() const = 0;
    virtual ~Shape() = default;
};

class Circle : public Shape {
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area() const override { return 3.14159 * radius * radius; }
};

class Rectangle : public Shape {
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    double area() const override { return width * height; }
};

All three use inheritance to accomplish this.

In this walkthrough, we’ll see how we can accomplish the same behavior in Go by using interfaces and composition instead.

Shapes

If we take common shapes such as a Circle and a Rectangle, they share common behaviors. These behaviors are:

  • Area
  • String

Area is how large is the object on a two dimensional plane, and String is how do we describe that shape in a humanized way.

Let’s start by creating just a Circle and printing out the area:


type Circle struct {
	Radius float64
}

func (c Circle) Area() float64 {
	return math.Pi * c.Radius * c.Radius
}

func main() {
	c := Circle{Radius: 15}
	fmt.Println(c.Area())
}

Output:


706.8583470577034

Painting

If we wanted to paint a shape, we would need to know what the area is. For this example, we’ve decided that it takes 12 gallon of paint per square foot to paint a shape. Let’s add the paint function that will calculate and print out how much paint we need for a given shape:


func paint(c Circle) string {
	paint := .5 * c.Area()
	return fmt.Sprintf("you need %.2f gallons of paint", paint)
}

This is what the entire code looks like now:


type Circle struct {
	Radius float64
}

func (c Circle) Area() float64 {
	return math.Pi * c.Radius * c.Radius
}

func main() {
	c := Circle{Radius: 15}
	fmt.Println(c.Area())

	fmt.Println(paint(c))
}

func paint(c Circle) string {
	paint := .5 * c.Area()
	return fmt.Sprintf("you need %.2f gallons of paint", paint)
}

Output:


706.8583470577034
you need 353.43 gallons of paint

Another Shape

Now let’s add a Rectangle:


type Rectangle struct {
	Height float64
	Width  float64
}

func (r Rectangle) Area() float64 {
	return r.Height * r.Width
}

This is what the entire code looks like now:


type Circle struct {
	Radius float64
}

func (c Circle) Area() float64 {
	return math.Pi * c.Radius * c.Radius
}

type Rectangle struct {
	Height float64
	Width  float64
}

func (r Rectangle) Area() float64 {
	return r.Height * r.Width
}


func main() {
	c := Circle{Radius: 15}
	fmt.Println(c.Area())

	fmt.Println(paint(c))

	r := Rectangle{Height: 20, Width: 30}
	fmt.Println(r.Area())
}

func paint(c Circle) string {
	paint := .5 * c.Area()
	return fmt.Sprintf("you need %.2f gallons of paint", paint)
}

Output:


706.8583470577034
you need 353.43 gallons of paint
600

The Problem With Paint

Now that we have two different shapes, we want to be able to re-use the paint method:


func paint(c Circle) string {
	paint := .5 * c.Area()
	return fmt.Sprintf("you need %.2f gallons of paint", paint)
}

However, because paint takes a concrete type of Circle as an argument, we are not able to pass a rectangle as an argument:


fmt.Println(paint(r))

If we do, we’ll receive the following compiler error:


cannot use r (type Rectangle) as type Circle in argument to paint

Both Circle and Rectangle share the same behavior of Area. To make use of the paint function for both of those types, we’ll need to introduce an interface.

Sizer Interface

We can create an interface called Sizer that declares only one behavior:


type Sizer interface {
	Area() float64
}

This now allows us to pass either a Circle or Rectangle, as they both satisfy the Sizer interface

This is what the entire code looks like now:


type Circle struct {
	Radius float64
}

func (c Circle) Area() float64 {
	return math.Pi * c.Radius * c.Radius
}

type Rectangle struct {
	Height float64
	Width  float64
}

func (r Rectangle) Area() float64 {
	return r.Height * r.Width
}

type Sizer interface {
	Area() float64
}


func main() {
	c := Circle{Radius: 15}
	fmt.Println(c.Area())

	fmt.Println(paint(c))

	r := Rectangle{Height: 20, Width: 30}
	fmt.Println(r.Area())

	fmt.Println(paint(r))
}

func paint(s Sizer) string {
	paint := .5 * s.Area()
	return fmt.Sprintf("you need %.2f gallons of paint", paint)
}

Output:


706.8583470577034
you need 353.43 gallons of paint
600
you need 300.00 gallons of paint

Humanizing

Now that we have our first behavior problem solved, we want to move to the next. We originally stated that we want to humanize the shapes, or describe them. This behavior will be created by adding a String method to each type:


func (c Circle) String() string {
	return fmt.Sprintf("Circle{Radius: %.2f}", c.Radius)
}

func (r Rectangle) String() string {
	return fmt.Sprintf("Rectangle{Height: %.2f, Width: %.2f}", r.Height, r.Width)
}

This is what the entire code looks like now:


type Circle struct {
	Radius float64
}

func (c Circle) Area() float64 {
	return math.Pi * c.Radius * c.Radius
}

func (c Circle) String() string {
	return fmt.Sprintf("Circle{Radius: %.2f}", c.Radius)
}


type Rectangle struct {
	Height float64
	Width  float64
}

func (r Rectangle) Area() float64 {
	return r.Height * r.Width
}

func (r Rectangle) String() string {
	return fmt.Sprintf("Rectangle{Height: %.2f, Width: %.2f}", r.Height, r.Width)
}


type Sizer interface {
	Area() float64
}

func main() {
	c := Circle{Radius: 15}
	fmt.Println(c)
	fmt.Println(c.Area())

	fmt.Println(paint(c))

	r := Rectangle{Height: 20, Width: 30}
	fmt.Println(r)
	fmt.Println(r.Area())

	fmt.Println(paint(r))
}

func paint(s Sizer) string {
	paint := .5 * s.Area()
	return fmt.Sprintf("you need %.2f gallons of paint", paint)
}

Fmt.Stringer

You may have noticed that by adding a String method to each type, we have now satisfied the fmt.Stringer interface. This means we can now print the types much nicer.

Output:


Circle{Radius: 15.00}
706.8583470577034
you need 353.43 gallons of paint
Rectangle{Height: 20.00, Width: 30.00}
600
you need 300.00 gallons of paint

Building Up Interfaces

Next, we’ll create another function called compare that takes two shapes, and compares them. The compare function will need to know both the Area, and how to describe the shape (String). Because of this, we’ll need to create another interface. This interface will be built up of two other interfaces:


type Shaper interface {
	Sizer        // Area() float64
	fmt.Stringer // String() string
}

We can now use the shaper interface as arguments to our compare function:


func compare(v1 Shaper, v2 Shaper) {
	if v1.Area() > v2.Area() {
		fmt.Println(v1.String(), "is the largest with an area of", v1.Area())
		return
	}
	fmt.Println(v2.String(), "is the largest with an area of", v2.Area())
}

Output:


Circle{Radius: 15.00}
706.8583470577034
you need 353.43 gallons of paint
Rectangle{Height: 20.00, Width: 30.00}
600
you need 300.00 gallons of paint
Circle{Radius: 15.00} is the largest with an area of 706.8583470577034

Final Program

As you can see, while we don’t have inheritance, we can still share and reuse code by composing interfaces.

Here is the entire program:


type Circle struct {
	Radius float64
}

func (c Circle) Area() float64 {
	return math.Pi * c.Radius * c.Radius
}

func (c Circle) String() string {
	return fmt.Sprintf("Circle{Radius: %.2f}", c.Radius)
}

type Rectangle struct {
	Height float64
	Width  float64
}

func (r Rectangle) Area() float64 {
	return r.Height * r.Width
}

func (r Rectangle) String() string {
	return fmt.Sprintf("Rectangle{Height: %.2f, Width: %.2f}", r.Height, r.Width)
}

type Sizer interface {
	Area() float64
}

type Shaper interface {
	Sizer        // Area() float64
	fmt.Stringer // String() string
}


func main() {
	c := Circle{Radius: 15}
	fmt.Println(c)
	fmt.Println(c.Area())

	fmt.Println(paint(c))

	r := Rectangle{Height: 20, Width: 30}
	fmt.Println(r)
	fmt.Println(r.Area())

	fmt.Println(paint(r))

	compare(c, r)
}

func paint(s Sizer) string {
	paint := .5 * s.Area()
	return fmt.Sprintf("you need %.2f gallons of paint", paint)
}

func compare(v1 Shaper, v2 Shaper) {
	if v1.Area() > v2.Area() {
		fmt.Println(v1.String(), "is the largest with an area of", v1.Area())
		return
	}
	fmt.Println(v2.String(), "is the largest with an area of", v2.Area())
}

Exercise (15 Mins)

Currently the User and Product structs don’t print pretty. Using what you have learned about the fmt.Stringer interface, make them print they way the program specifies below.

Current output:


{1 Rob Pike}
{10 Plush Gopher}

Desired Output:


User 1 is Rob Pike
Product 10 is Plush Gopher

Finally, loop through both types as Slugger's and print them out:

1-Rob-Pike
10-Plush%20Gopher

package main

import (
	"fmt"
	"net/url"
)

type User struct {
	ID    int
	First string
	Last  string
}

func (u User) Slug() string {
	return url.PathEscape(fmt.Sprintf("%d-%s-%s", u.ID, u.First, u.Last))
}

// #TODO:
// Satisfy the stringer interface (https://golang.org/pkg/fmt/#Stringer)
// so that the User struct will print
// User <ID> is <First> <Last>
//
// example:
//      User 1 is Rob Pike

type Product struct {
	ID   int
	Name string
}

// #TODO:
// Satisfy the stringer interface (https://golang.org/pkg/fmt/#Stringer)
// so that the Product struct will print
// Product <ID> is <Name>
//
// example:
//      User 1 is Rob Pike

func (p Product) Slug() string {
	return url.PathEscape(fmt.Sprintf("%d-%s", p.ID, p.Name))
}

// #TODO: Create an interface called `Slugger` that matches the behavior of the
//  `Slug` function that both the Product and User type have declared

func main() {
	u := User{ID: 1, First: "Rob", Last: "Pike"}
	fmt.Println(u)

	p := Product{ID: 10, Name: "Plush Gopher"}
	fmt.Println(p)

	// #TODO: Declare a slice of `Slugger` and add the previously declared user and product to the slice

	for _, s := range sluggers {
		// #TODO: Print out the `Slug` for each item

	}
}

Solution


package main

import (
	"fmt"
	"net/url"
)

type User struct {
	ID    int
	First string
	Last  string
}

func (u User) String() string {
	return fmt.Sprintf("User %d is %s %s", u.ID, u.First, u.Last)
}

func (u User) Slug() string {
	return url.PathEscape(fmt.Sprintf("%d-%s-%s", u.ID, u.First, u.Last))
}

type Product struct {
	ID   int
	Name string
}

func (p Product) String() string {
	return fmt.Sprintf("Product %d is %s", p.ID, p.Name)
}

func (p Product) Slug() string {
	return url.PathEscape(fmt.Sprintf("%d-%s", p.ID, p.Name))
}

// Create an interface called `Slugger` that declares a function called `Slug() string`
type Slugger interface {
	Slug() string
}

func main() {
	u := User{ID: 1, First: "Rob", Last: "Pike"}
	fmt.Println(u)

	p := Product{ID: 10, Name: "Plush Gopher"}
	fmt.Println(p)

	sluggers := []Slugger{u, p}
	for _, s := range sluggers {
		fmt.Println(s.Slug())
	}
}

The Empty Interface

All the interfaces we’ve seen so far have declared one or more methods.

If you declare an interface with zero methods, then every type will satisfy that interface.

In Go we use the empty interface to represent “anything”.

// generic empty interface:
interface{}

// a named empty interface:
type foo interface{}

The `any` Type Alias

Starting with Go 1.18, Go introduced the any type alias as a cleaner way to represent the empty interface.

// Before Go 1.18:
var x interface{}

// Go 1.18 and later:
var x any

The any type is simply a type alias for interface{}:

// Defined in the universe block:
type any = interface{}

This means any and interface{} are completely interchangeable - they represent the exact same type.

Why `any` Is Better

The any type alias provides several benefits:

  • Readability: Much cleaner and easier to read than interface{}
  • Familiarity: Similar to other languages (TypeScript, Python, etc.)
  • Consistency: Aligns with Go’s move toward more expressive type syntax with generics
// Old style - still works but less readable:
func printAnything(val interface{}) {
    fmt.Println(val)
}

// New style - much cleaner:
func printAnything(val any) {
    fmt.Println(val)
}

Standard library functions now commonly use any in their signatures, such as json.Marshal(), fmt.Printf(), and others.

Using The Empty Interface


package main

import (
	"fmt"
)

func main() {
	print(42)
	print("Ringo Rules")
	print(3.14159)
	// int: 42
	// string: Ringo Rules
	// float64: 3.14159
}

func print(i any) {
	// %T - prints the type of the value
	// %v - prints the value in a default format
	fmt.Printf("%T: %v\n", i, i)
}

The Problem With Empty Interfaces

interface{} says nothing. – Rob Pike

It is considered bad practice in Go to overuse the empty interface. You should always try to accept either a concrete type or a non-empty interface.

While there are valid reasons to use an empty interface, the downsides should be considered first:

  • No type information
  • Runtime panics are very possible
  • Difficult code (to test, understand, document, etc…)

Type Assertions

Type assertions can be used to check, and possibly convert to another type or interface like this:

object.(type)

Like maps, type assertion returns an optional second boolean value that can be used to confirm whether the assertion was successful or not.


func print(s any) {
	stringer, ok := s.(fmt.Stringer)
	if ok {
		fmt.Println(stringer.String())
		return
	}
	fmt.Println("not a stringer")
}

This can be condensed into:


func printInline(s any) {
	if stringer, ok := s.(fmt.Stringer); ok {
		fmt.Println(stringer.String())
		return
	}
	fmt.Println("not a stringer")
}

Watch Out For Panics

If you do not check the second, optional boolean value from the type check you risk your application panicking.


package main

import "fmt"

func main() {
	print(1)
}

func print(s any) {
	st := s.(fmt.Stringer)
	fmt.Println(st.String())
}
panic: interface conversion: int is not fmt.Stringer: missing method String

NOTE: A panic can crash your entire application.

Switching On Type

Using a switch statement and the type keyword, we can handle values differently depending on their type.


type Beatle struct {
	Name       string
	Instrument string
}

func (b Beatle) String() string {
	s, _ := json.Marshal(b)
	return string(s)
}

func main() {
	print(42)
	print("Ringo Rules")
	print(3.141592)
	print(Beatle{Name: "John", Instrument: "Guitar"})
	// INT: 42
	// Ringo Rules
	// FLOAT: 3.141592
}

func print(i any) {
	switch t := i.(type) {
	case int:
		t = t + 3
		fmt.Printf("INT: %d\n", t)
	case float64:
		fmt.Printf("FLOAT: %f\n", t)
	case fmt.Stringer:
		fmt.Printf("%T\n", t)
		fmt.Println(t.String())
	default:
		fmt.Println(t)
	}
}

You can optionally assign the result of the type assertion to a variable, which will become the asserted type:

switch typedObject := anInterface.(type)

Avoiding Empty Interface

While it can be tempting to just use an empty interface for your arguments, you are defeating the type safety that comes built in to the language.

If we take the following example, it may look fine on the surface to use the empty interface:


package main

import (
	"errors"
	"fmt"
)

type Ack struct {
	Data []byte
}

func (a Ack) Ack() string {
	return string(a.Data)
}

type Syn struct {
	Data []byte
}

func (s Syn) Syn() string {
	return string(s.Data)
}

// This uses the empty interface, and is not desirable.
func processMessages(msg any) error {
	switch m := msg.(type) {
	case Ack:
		fmt.Println("processing Ack message: ", m.Ack())
		return nil
	case Syn:
		fmt.Println("processingSyn message: ", m.Syn())
		return nil
	default:
		return errors.New("unknown message type!!!!")
	}
}

func main() {
	a := Ack{Data: []byte("ack message")}
	s := Syn{Data: []byte("syn message")}
	if err := processMessages(a); err != nil {
		fmt.Println(err)
	}
	if err := processMessages(s); err != nil {
		fmt.Println(err)
	}

	// allows to send anything, and that's not ok
	if err := processMessages("foo"); err != nil {
		fmt.Println(err)
	}
}

No Empty Interface

However, we can also create our own interface, and ensure that all our types satisfy that interface, such that we no longer have to use the empty interface and allow anything to be passed to our function.


package main

import (
	"errors"
	"fmt"
)

// Messenger ensures we are a `message`
type Messenger interface {
	Message()
}

type Ack struct {
	Data []byte
}

func (a Ack) Message() {}
func (a Ack) Ack() string {
	return string(a.Data)
}

type Syn struct {
	Data []byte
}

func (s Syn) Message() {}
func (s Syn) Syn() string {
	return string(s.Data)
}

// This now uses the `messenger` interface, which is more desirable.
func processMessages(msg Messenger) error {
	switch m := msg.(type) {
	case Ack:
		fmt.Println("processing Ack message: ", m.Ack())
		return nil
	case Syn:
		fmt.Println("processingSyn message: ", m.Syn())
		return nil
	default:
		return errors.New("unknown message type!!!!")
	}
}

func main() {
	a := Ack{Data: []byte("ack message")}
	s := Syn{Data: []byte("syn message")}
	if err := processMessages(a); err != nil {
		fmt.Println(err)
	}
	if err := processMessages(s); err != nil {
		fmt.Println(err)
	}
	// This is no longer possible: string does not implement Messenger,
	// so the following line would not compile.
	// processMessages("foo")

}

No Empty Interface With Generics

The marker interface works, but it forces us to bolt an otherwise empty Message() method onto every type just to satisfy the constraint.

Since Go 1.18, generics give us another option. We can define a constraint that lists the allowed types directly, so Ack and Syn stay clean:


// Message is still an interface, but it is used as a constraint: no
// marker method is required, so Ack and Syn stay clean. Belonging to
// the type set is what satisfies it.
type Message interface {
	Ack | Syn
}

// Go cannot type-switch on a type parameter directly, so we convert to
// any for dispatch. Note the difference from the empty-interface version:
// there we *accepted* any (the anti-pattern); here the parameter stays
// constrained and any is only an internal implementation detail.
func processMessages[T Message](msg T) {
	switch m := any(msg).(type) {
	case Ack:
		fmt.Println("processing Ack message:", m.Ack())
	case Syn:
		fmt.Println("processing Syn message:", m.Syn())
	default:
		// A union constraint is not an exhaustive sum type: add a member
		// without a case here and it silently does nothing. Fail loudly.
		panic(fmt.Sprintf("unhandled message type %T", m))
	}
}

Our types no longer carry a marker method:


// Notice there is no marker method: Ack and Syn stay clean.
type Ack struct {
	Data []byte
}

func (a Ack) Ack() string {
	return string(a.Data)
}

type Syn struct {
	Data []byte
}

func (s Syn) Syn() string {
	return string(s.Data)
}

Calling processMessages with an unsupported type is now a compile-time error, not a runtime default case:


func main() {
	processMessages(Ack{Data: []byte("ack message")})
	processMessages(Syn{Data: []byte("syn message")})

	// The compiler rejects anything outside the constraint:
	// processMessages("foo") // string does not satisfy Message
}

Be honest about what this is: Message is still an interface, and the body converts to any to run the type switch. The difference is that we no longer accept any (the anti-pattern from the previous slide); the parameter stays constrained, and any is just an internal implementation detail. A union constraint is also not an exhaustive sum type, so keep a default case to fail loudly if a new member is added without a matching branch.

Which approach you reach for depends on the shape of the problem. A marker interface is open: any type in any package can add the method and satisfy it later. A union constraint is closed: the set of valid types is fixed where the constraint is defined. One trade-off to note: the union Ack | Syn accepts Ack and Syn values but not *Ack or *Syn pointers, whereas the marker interface (with value receivers) accepts both. When you own all the types and want the compiler to guarantee the set is complete, the union constraint gets you there without the empty marker method.

Interface Zero Value

Because interfaces aren’t backed by any actual implementation the zero value of an interface is nil.


type Foo interface {
	Greet() string
}

func main() {
	var f Foo
	fmt.Println(f.Greet())
}

While that code compiles, it will panic because the interface doesn’t have an implementation backing it.

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x108d663]

Backing Interfaces

More commonly, you will be backing an interface with your concrete type.

Given the following type definition:


type Stream struct {
	io.Writer
}

You will need to back the interface with a concrete type to work:


s := Stream{
	Writer: os.Stdout,
}
fmt.Fprintf(s, "Hello Gophers!")

However, sometimes in your code, you will forget to do this:


s := Stream{}
fmt.Fprintf(s, "Hello Gophers!")

The code will compile, but result in a run-time panic:


panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x18 pc=0x109e92e]

goroutine 1 [running]:
main.(*Stream).Write(0xc00008e1f0, 0xc0000b2010, 0xe, 0x10, 0x0, 0x0, 0xc00008e1f0)
        <autogenerated>:1 +0x2e
fmt.Fprintf(0x10ebd20, 0xc00008e1f0, 0x10cf8da, 0xe, 0x0, 0x0, 0x0, 0xe, 0x0, 0x0)
        /usr/local/Cellar/go/1.14.1/libexec/src/fmt/print.go:205 +0xa5
main.empty(...)
        /Users/corylanou/projects/gopherguides/learn/_training/fundamentals/interfaces/src/zero-backed.go:36
main.main()
        /Users/corylanou/projects/gopherguides/learn/_training/fundamentals/interfaces/src/zero-backed.go:11 +0xf1
exit status 2

Pointer Receivers

Methods defined on pointer receivers can only be accessed when using a pointer. However, methods defined on a value type can be access by both pointers and values.


package main

import "fmt"

type User struct {
	First string
	Last  string
}

func (u *User) String() string {
	return fmt.Sprintf("%s %s", u.First, u.Last)
}

func pretty(v fmt.Stringer) {
	fmt.Println(v.String())
}

func main() {
	u := User{First: "Rob", Last: "Pike"}
	pretty(u)
}
cannot use u (type User) as type fmt.Stringer in argument to pretty:
  User does not implement fmt.Stringer (String method has pointer receiver)

Value Receivers


package main

import "fmt"

type User struct {
	First string
	Last  string
}

func (u User) String() string {
	return fmt.Sprintf("%s %s", u.First, u.Last)
}

func pretty(v fmt.Stringer) {
	fmt.Println(v.String())
}

func main() {
	u := &User{First: "Rob", Last: "Pike"}
	pretty(u)
}

Value receivers can satisfy both value and pointer methods.

Interface Design Best Practices

When designing interfaces in Go, following established best practices will make your code more maintainable, testable, and idiomatic. Let’s explore the key principles that guide effective interface design.

Accept Interfaces, Return Concrete Types

One of the most important idioms in Go interface design is: “Accept interfaces, return concrete types”.

This principle means:

  • Function parameters: Accept interfaces to maximize flexibility
  • Return values: Return concrete types to provide clarity and avoid limiting callers

Why This Matters

Flexibility: When you accept an interface, callers can pass any type that satisfies the interface, making your function more reusable.

Type Safety: When you return concrete types, callers know exactly what they’re getting and can access all methods without type assertions.

Composability: Callers can wrap or compose your concrete return values into their own interfaces as needed.

Good Example: Accept Interface, Return Concrete

Take a Logger that satisfies io.Writer but also has its own SetLevel method:


type Level int

const (
	InfoLevel Level = iota
	DebugLevel
)

type Logger struct {
	output io.Writer
	level  Level
}

func (l *Logger) SetLevel(level Level) {
	l.level = level
}

func (l *Logger) Log(msg string) {
	fmt.Fprintf(l.output, "[%d] %s\n", l.level, msg)
}

// Write lets a Logger satisfy io.Writer.
func (l *Logger) Write(p []byte) (int, error) {
	return l.output.Write(p)
}

The constructor accepts an io.Writer interface but returns the concrete *Logger:


// Good: accepts an interface, returns a concrete *Logger.
func NewLogger(w io.Writer) *Logger {
	return &Logger{output: w, level: InfoLevel}
}

Because it returns *Logger, the caller can reach every method directly, including SetLevel:


func useGood() {
	// Accepts any io.Writer, so it works with files, buffers, etc.
	logger := NewLogger(os.Stdout)

	// Returns *Logger, so every Logger method is available directly.
	logger.SetLevel(DebugLevel)
	logger.Log("good: SetLevel worked without a type assertion")
}

In this example:

  • The function accepts io.Writer, so it works with files, buffers, network connections, or any custom writer
  • It returns *Logger, so callers have access to all Logger methods without type assertions
  • Callers can still define their own interfaces if needed: type MyLogger interface { Log(string) }

Bad Example: Returning Interfaces

Now return the same Logger as an io.Writer instead:


// Bad: accepts an interface, but also returns an interface.
func NewLoggerBad(w io.Writer) io.Writer {
	return &Logger{output: w, level: InfoLevel}
}

The caller only sees io.Writer, so SetLevel is out of reach without a type assertion:


func useBad() {
	logger := NewLoggerBad(os.Stdout)

	// logger.SetLevel(DebugLevel) // won't compile: io.Writer only has Write

	// The caller is forced to assert back to the concrete type.
	if l, ok := logger.(*Logger); ok {
		l.SetLevel(DebugLevel) // awkward, and easy to get wrong
		l.Log("bad: SetLevel only reachable after a type assertion")
	}
}

Problems with returning the interface:

  • The caller can only use io.Writer methods (just Write)
  • Logger-specific methods like SetLevel are hidden
  • Recovering them forces the caller into error-prone type assertions

Avoid Returning `interface{}` Or `any`

Returning empty interfaces (interface{} or any) defeats Go’s type safety and should be avoided except in very specific cases like json.Unmarshal.

Returning any gives the caller no type information, so they are forced to assert before using the value:


// Bad: returns any, providing no type information.
func GetConfigAny() any {
	return &Config{Host: "localhost", Port: 8080}
}

Returning the concrete type gives the caller immediate access to every field:


// Good: returns the concrete type.
func GetConfig() *Config {
	return &Config{Host: "localhost", Port: 8080}
}

Keep Interfaces Small

The bigger the interface, the weaker the abstraction. – Rob Pike

The best interfaces in Go are small, typically containing 1-3 methods. Small interfaces are:

  • Easier to implement: Less work to satisfy the interface
  • More reusable: More types can satisfy them
  • More composable: Can be combined to create larger interfaces
  • Better for testing: Simpler to mock

Small Interface Examples From The Standard Library

Go’s standard library demonstrates this principle beautifully. io.Reader, io.Writer, and io.Closer each declare a single method, and fmt.Stringer declares just String() string.

Because they are so small, they compose. A type that satisfies all three small interfaces automatically satisfies io.ReadWriteCloser:


// The standard library favors tiny interfaces: io.Reader, io.Writer,
// and io.Closer each declare a single method. They compose into
// io.ReadWriteCloser without any of them knowing about the others.
//
//	type ReadWriteCloser interface {
//	    Reader
//	    Writer
//	    Closer
//	}

// Pipe satisfies all three small interfaces, so it satisfies the
// composed io.ReadWriteCloser as well.
type Pipe struct {
	data []byte
	pos  int
}

func (p *Pipe) Write(b []byte) (int, error) {
	p.data = append(p.data, b...)
	return len(b), nil
}

func (p *Pipe) Read(b []byte) (int, error) {
	if p.pos >= len(p.data) {
		return 0, io.EOF
	}
	n := copy(b, p.data[p.pos:])
	p.pos += n
	return n, nil
}

func (p *Pipe) Close() error {
	p.data = nil
	return nil
}

// pump accepts the composed interface, but only because it was
// built from small, reusable pieces.
func pump(rwc io.ReadWriteCloser) {
	io.WriteString(rwc, "small interfaces compose")
	data, _ := io.ReadAll(rwc)
	fmt.Printf("read back: %q\n", data)
	rwc.Close()
}

Good Example: Small, Focused Interfaces

Define one interface per capability, compose them when a type needs several, and let each function ask for only what it uses:


// Small, single-purpose interfaces.
type UserFinder interface {
	FindByID(id int) (*User, error)
}

type UserCreator interface {
	Create(user *User) error
}

type UserDeleter interface {
	Delete(id int) error
}

// Compose them when a type needs multiple behaviors.
type UserRepository interface {
	UserFinder
	UserCreator
	UserDeleter
}

// Functions accept only the behavior they need.
func DisplayUser(id int, finder UserFinder) {
	user, err := finder.FindByID(id)
	if err != nil {
		fmt.Println("not found:", err)
		return
	}
	fmt.Println("found:", user.Name)
}

Bad Example: Large Kitchen-Sink Interface

A single interface with ten methods is hard to implement, hard to mock, and forces implementations to provide features they don’t need:


// Bad: too many methods, too many responsibilities. Every implementation
// must provide all ten, and every mock must too.
type UserManager interface {
	FindByID(id int) (*User, error)
	FindByEmail(email string) (*User, error)
	FindAll() ([]*User, error)
	Create(user *User) error
	Update(user *User) error
	Delete(id int) error
	Validate(user *User) error
	SendEmail(user *User, subject, body string) error
	HashPassword(password string) string
	CheckPermission(user *User, action string) bool
}

Better approach: break it into focused interfaces, and let each function accept only what it needs:


// Better: small interfaces, each with one responsibility.
type UserValidator interface {
	Validate(user *User) error
}

type UserCreator interface {
	Create(user *User) error
}

// A function accepts only what it needs.
func ValidateAndCreate(u *User, v UserValidator, c UserCreator) error {
	if err := v.Validate(u); err != nil {
		return err
	}
	return c.Create(u)
}

Define Interfaces At The Point Of Use

In Go, interfaces should be defined by the consumer, not the producer. This is opposite to many other languages.

Producer: The package that provides the concrete implementation Consumer: The package that uses the functionality

The Consumer-Defined Interface Pattern

The producer package provides a concrete implementation and defines no interface at all:


// Package storage is the producer. It provides a concrete
// implementation and defines NO interface of its own.
package storage

import "fmt"

type FileStore struct {
	files map[string][]byte
}

func NewFileStore() *FileStore {
	return &FileStore{files: map[string][]byte{}}
}

func (f *FileStore) Save(key string, data []byte) error {
	f.files[key] = data
	return nil
}

func (f *FileStore) Load(key string) ([]byte, error) {
	data, ok := f.files[key]
	if !ok {
		return nil, fmt.Errorf("no such key: %s", key)
	}
	return data, nil
}

func (f *FileStore) Delete(key string) error {
	delete(f.files, key)
	return nil
}

The consumer package defines its own interface listing only the methods it needs. The producer’s FileStore satisfies it without ever importing the consumer:


// Package cache is the consumer. It defines its own interface
// containing only the methods it actually uses.
package cache

// Storage lists only what cache needs (no Delete).
type Storage interface {
	Save(key string, data []byte) error
	Load(key string) ([]byte, error)
}

type Cache struct {
	storage Storage
}

func New(s Storage) *Cache {
	return &Cache{storage: s}
}

func (c *Cache) Put(key string, data []byte) error {
	return c.storage.Save(key, data)
}

func (c *Cache) Get(key string) ([]byte, error) {
	return c.storage.Load(key)
}

// storage.FileStore satisfies this interface without importing cache
// or knowing the interface exists.

Why Consumer-Defined Interfaces?

Decoupling: The producer doesn’t need to know about all possible use cases

Focused interfaces: Each consumer defines only what they need

No dependencies: The producer has no dependency on interface packages

Easier testing: Consumers create minimal mock interfaces


// The consumer creates a minimal mock. It only implements Save and
// Load, because that is all the Storage interface requires.
type mockStorage struct {
	data map[string][]byte
}

func (m *mockStorage) Save(key string, data []byte) error {
	m.data[key] = data
	return nil
}

func (m *mockStorage) Load(key string) ([]byte, error) {
	return m.data[key], nil
}

// No Delete needed: cache never calls it.

Bad Example: Producer-Defined Interface

Here the producer defines the interface upfront (the traditional OOP approach), listing every method its FileStore supports:


// Bad: the producer defines the interface upfront (traditional OOP style),
// forcing every consumer to depend on all five methods.
package storage

type Storage interface {
	Save(key string, data []byte) error
	Load(key string) ([]byte, error)
	Delete(key string) error
	List() ([]string, error)
	Clear() error
}

type FileStore struct {
	files map[string][]byte
}

func NewFileStore() *FileStore {
	return &FileStore{files: map[string][]byte{}}
}

func (f *FileStore) Save(key string, data []byte) error { f.files[key] = data; return nil }
func (f *FileStore) Load(key string) ([]byte, error)    { return f.files[key], nil }
func (f *FileStore) Delete(key string) error            { delete(f.files, key); return nil }
func (f *FileStore) Clear() error                       { clear(f.files); return nil }

func (f *FileStore) List() ([]string, error) {
	keys := make([]string, 0, len(f.files))
	for k := range f.files {
		keys = append(keys, k)
	}
	return keys, nil
}

Now the consumer is forced to depend on that whole interface, even though it only calls Save and Load:


package cache

import "producerdefined/storage"

type Cache struct {
	// Forced to use the producer's wide interface, even though a cache
	// only ever calls Save and Load.
	storage storage.Storage
}

func New(s storage.Storage) *Cache {
	return &Cache{storage: s}
}

func (c *Cache) Put(key string, data []byte) error { return c.storage.Save(key, data) }
func (c *Cache) Get(key string) ([]byte, error)    { return c.storage.Load(key) }

// Problems:
//  1. Tight coupling to the storage package.
//  2. Cache depends on methods it never uses (Delete, List, Clear).
//  3. Any mock for testing must implement all five methods.

Interface Segregation Principle

The Interface Segregation Principle states: “Clients should not be forced to depend on methods they do not use.”

This means creating specific, focused interfaces rather than forcing implementations to satisfy unused methods.

Good Example: Segregated Interfaces

Split capabilities into separate interfaces. A type can implement all of them, but each function depends only on the capability it uses:


// Good: separate interfaces for separate capabilities.
type Reader interface {
	Read() ([]byte, error)
}

type Writer interface {
	Write(data []byte) error
}

type Closer interface {
	Close() error
}

// File implements all three, but callers depend only on what they use.
type File struct {
	name string
	data []byte
}

func (f *File) Read() ([]byte, error) { return f.data, nil }
func (f *File) Write(data []byte) error {
	f.data = data
	return nil
}
func (f *File) Close() error { return nil }

// ProcessData only needs to read.
func ProcessData(r Reader) ([]byte, error) {
	return r.Read()
}

Bad Example: Forcing Unused Methods

One fat interface forces every implementation to support operations it cannot honor. A read-only store is stuck returning errors from Write and Delete:


// Bad: one fat interface forces every implementation to support everything.
type DataStore interface {
	Read() ([]byte, error)
	Write(data []byte) error
	Delete() error
}

// ReadOnlyStore is forced to implement Write and Delete it cannot honor.
type ReadOnlyStore struct{}

func (r *ReadOnlyStore) Read() ([]byte, error) { return []byte("data"), nil }
func (r *ReadOnlyStore) Write([]byte) error    { return errors.New("read-only store") }
func (r *ReadOnlyStore) Delete() error         { return errors.New("read-only store") }

Common Interface Design Patterns

Let’s review common patterns you’ll encounter in Go code:

✅ Good Patterns

1. Accept narrow interfaces


// 1. Accept a narrow interface: only the method you use.
func SaveUser(u *User, w io.Writer) error {
	data, err := json.Marshal(u)
	if err != nil {
		return err
	}
	_, err = w.Write(data)
	return err
}

2. Return concrete types


// 2. Return a concrete type, not an interface.
type UserService struct {
	db *DB
}

func NewUserService(db *DB) *UserService {
	return &UserService{db: db}
}

3. Small, focused interfaces


// 3. Keep interfaces small and focused.
type Validator interface {
	Validate() error
}

type Saver interface {
	Save() error
}

4. Interface composition


// 4. Compose small interfaces into larger ones.
type Reader interface {
	Read() ([]byte, error)
}

type Writer interface {
	Write([]byte) error
}

type ReadWriter interface {
	Reader
	Writer
}

❌ Anti-Patterns To Avoid

1. Returning interface{} or any unnecessarily


// Bad: returning any loses all type information.
func GetUserAny(id int) any {
	return &User{ID: id}
}

// Good: return the concrete type.
func GetUser(id int) *User {
	return &User{ID: id}
}

2. Premature interface abstraction


// Bad: creating an interface before a second implementation exists.
type EarlyUserService interface {
	GetUser(id int) *User
}

type EarlyUserServiceImpl struct{}

func (EarlyUserServiceImpl) GetUser(id int) *User { return &User{ID: id} }

// Good: start concrete. Add an interface later, when a second
// implementation actually shows up.
type UserService struct{}

func (UserService) GetUser(id int) *User { return &User{ID: id} }

3. Large, unfocused interfaces


// Bad: one interface with too many responsibilities.
type Service interface {
	Start() error
	Stop() error
	Restart() error
	Configure(cfg Config) error
	GetStatus() Status
	GetMetrics() Metrics
	Health() error
	// ...and more
}

// Good: break it into small, focused interfaces.
type Starter interface {
	Start() error
}

type Stopper interface {
	Stop() error
}

type HealthChecker interface {
	Health() error
}

When To Use Interfaces

Use interfaces when you need:

  • Abstraction for testing: Mock dependencies in tests
  • Multiple implementations: Different behaviors with same contract
  • Decoupling: Reduce dependencies between packages
  • Standard library compatibility: Work with io.Reader, io.Writer, etc.

Don’t use interfaces when:

  • Only one implementation exists and will ever exist
  • The interface would have many methods (>5 is a smell)
  • You’re “designing for the future” - YAGNI (You Aren’t Gonna Need It)

Practical Example: Refactoring To Interfaces

Let’s see a complete example of refactoring from concrete types to interfaces:

Before: Tightly coupled to concrete type


// Before: NotifyUser is tightly coupled to the concrete *EmailSender.
// You cannot swap in SMS, and testing means standing up real SMTP.
type EmailSender struct {
	smtpHost string
}

func (e *EmailSender) Send(to, subject, body string) error {
	fmt.Printf("email to %s via %s: %s\n", to, e.smtpHost, subject)
	return nil
}

func NotifyUser(user *User, email *EmailSender) error {
	return email.Send(user.Email, "Welcome", "Welcome to our service")
}

After: Accepting an interface

Define the interface at the point of use, and NotifyUser works with any implementation, EmailSender, SMSSender, or anything else:


// Define the interface at the point of use (consumer-defined).
type Notifier interface {
	Send(to, subject, body string) error
}

// Concrete implementations.
type EmailSender struct {
	smtpHost string
}

func (e *EmailSender) Send(to, subject, body string) error {
	fmt.Printf("email to %s via %s: %s\n", to, e.smtpHost, subject)
	return nil
}

type SMSSender struct {
	apiKey string
}

func (s *SMSSender) Send(to, subject, body string) error {
	fmt.Printf("sms to %s: %s\n", to, subject)
	return nil
}

// NotifyUser accepts the interface, so it works with any implementation.
func NotifyUser(user *User, notifier Notifier) error {
	return notifier.Send(user.Email, "Welcome", "Welcome to our service")
}

Testing becomes trivial, because a tiny mock satisfies the interface, no SMTP or SMS gateway required:


// Because NotifyUser accepts an interface, testing it needs only a
// tiny mock, no SMTP server or SMS gateway.
type MockNotifier struct {
	sentTo []string
}

func (m *MockNotifier) Send(to, subject, body string) error {
	m.sentTo = append(m.sentTo, to)
	return nil
}

func TestNotifyUser(t *testing.T) {
	mock := &MockNotifier{}
	user := &User{Email: "test@example.com"}

	if err := NotifyUser(user, mock); err != nil {
		t.Fatal(err)
	}

	if len(mock.sentTo) != 1 {
		t.Fatalf("expected 1 message sent, got %d", len(mock.sentTo))
	}
}

Summary: Interface Design Best Practices

  1. Accept interfaces, return concrete types - Maximize flexibility in parameters, provide clarity in returns
  2. Keep interfaces small - Aim for 1-3 methods per interface
  3. Define at point of use - Consumers define interfaces, not producers
  4. Interface segregation - Don’t force implementations to satisfy unused methods
  5. Avoid interface{}/any in returns - Preserve type safety
  6. Avoid premature abstraction - Create interfaces when you need them, not before
  7. Compose interfaces - Build larger interfaces from smaller ones when needed

Following these practices will make your Go code more idiomatic, maintainable, and testable.

Pretty Print

In Go, the way to pretty print anything is to implement the fmt.Stringer interface.

You can learn more about pretty printing and other ways to implement custom pretty printing in our Gopher Guides TV video: Formatting Custom Types in Go with the fmt package.

Exercise (15 Mins)

The following code example has 4 different encode methods. Using the existing code, create a single encode method that can take any type, and call the appropriate encoding method as needed.


package main

import (
	"encoding/json"
	"fmt"
	"strconv"
)

func main() {
	var result []byte
	result = encodeString("hello")
	fmt.Println(string(result))

	result = encodeInt(25)
	fmt.Println(string(result))

	result = encodeMap(map[string]string{"color": "green", "temperature": "95", "speed": "100"})
	fmt.Println(string(result))

	result = encodeMarshaler(Email{name: "gopher", domain: "golang.org"})
	fmt.Println(string(result))
}

// #TODO: Create a general function called `encode` that can take any type and call the corresponding
// encoder based on the corresponding type or interface
func encode(v interface{}) {
	var result []byte

	// Hint: a switch statement on type is very useful here...

	// Check for a string
	// Check for an int
	// Check for a map[string]string
	// Check to see if it already supports marshaling (it defines the json.Marshaler interface)

	fmt.Println(string(result))
}

// Nothing to do beyond this point.

func encodeMarshaler(m json.Marshaler) []byte {
	b, err := m.MarshalJSON()
	if err != nil {
		panic(fmt.Sprintf("error encoding %T: %s\n", m, err))
	}
	return b
}

func encodeMap(m map[string]string) []byte {
	js := "{"
	for k, v := range m {
		js = js + `"` + k + `":"` + v + `" `
	}
	js = js + "}"
	return []byte(js)
}

func encodeString(s string) []byte {
	fmt := `{"value":"` + s + `"}"`
	return []byte(fmt)
}

func encodeInt(i int) []byte {
	fmt := `{"value":"` + strconv.Itoa(i) + `"}"`
	return []byte(fmt)
}

type Email struct {
	name   string
	domain string
}

func (e Email) MarshalJSON() ([]byte, error) {
	return []byte(fmt.Sprintf("{\"email\":\"%s@%s\"}", e.name, e.domain)), nil
}

Solution


package main

import (
	"encoding/json"
	"fmt"
	"strconv"
)

func main() {
	encode("hello")
	encode(25)
	encode(map[string]string{"color": "green", "temperature": "95", "speed": "100"})
	encode(Email{name: "gopher", domain: "golang.org"})
}

func encode(v interface{}) {
	var result []byte
	switch t := v.(type) {
	case json.Marshaler:
		result = encodeMarshaler(t)
	case map[string]string:
		result = encodeMap(t)
	case string:
		result = encodeString(t)
	case int:
		result = encodeInt(t)
	default:
		fmt.Printf("unable to encode type %T\n", t)
	}
	fmt.Println(string(result))
}

func encodeMarshaler(m json.Marshaler) []byte {
	b, err := m.MarshalJSON()
	if err != nil {
		panic(fmt.Sprintf("error encoding %T: %s\n", m, err))
	}
	return b
}

func encodeMap(m map[string]string) []byte {
	js := "{"
	for k, v := range m {
		js = js + `"` + k + `":"` + v + `" `
	}
	js = js + "}"
	return []byte(js)
}

func encodeString(s string) []byte {
	fmt := `{"value":"` + s + `"}"`
	return []byte(fmt)
}

func encodeInt(i int) []byte {
	fmt := `{"value":"` + strconv.Itoa(i) + `"}"`
	return []byte(fmt)
}

type Email struct {
	name   string
	domain string
}

func (e Email) MarshalJSON() ([]byte, error) {
	return []byte(fmt.Sprintf("{\"email\":\"%s@%s\"}", e.name, e.domain)), nil
}

Stretch Exercise

Implement the sort.Interface interface on the Persons type:

type Interface interface {
 // Len is the number of elements in the collection.
 Len() int
 // Less reports whether the element with
 // index i should sort before the element with index j.
 Less(i, j int) bool
 // Swap swaps the elements with indexes i and j.
 Swap(i, j int)
}

Sort the people by Age:


package main

import (
	"fmt"
	"sort"
)

type Person struct {
	Name string
	Age  int
}

type Persons []Person

// implement sort.Interface on Persons

func main() {
	var people = Persons{
		{
			Name: "Bill",
			Age:  55,
		},
		{
			Name: "John",
			Age:  92,
		},
		{
			Name: "Megan",
			Age:  4,
		},
	}
	fmt.Printf("unsorted: %+v\n", people)
	sort.Sort(people)
	fmt.Printf("sorted: %+v\n", people)
}

See the sort.Interface documentation

Solution


package main

import (
	"fmt"
	"sort"
)

type Person struct {
	Name string
	Age  int
}

type Persons []Person

// Len is the number of elements in the collection.
func (p Persons) Len() int {
	return len(p)
}

// Less reports whether the element with
// index i should sort before the element with index j.
func (p Persons) Less(i, j int) bool {
	return p[i].Age < p[j].Age
}

// Swap swaps the elements with indexes i and j.
func (p Persons) Swap(i, j int) {
	p[i], p[j] = p[j], p[i]
}

func main() {
	var people = Persons{
		{
			Name: "Bill",
			Age:  55,
		},
		{
			Name: "John",
			Age:  92,
		},
		{
			Name: "Megan",
			Age:  4,
		},
	}
	fmt.Printf("unsorted: %+v\n", people)
	sort.Sort(people)
	fmt.Printf("sorted: %+v\n", people)
}

Notice how we swap without an additional variable:

p[i], p[j] = p[j], p[i]