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()
}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"))
}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:
- Function Parameter: The
writeMessagefunction accepts anio.Writerinterface as a parameter - Interface Implementation:
os.Stdoutimplements theio.Writerinterface because it has aWritemethod that matches the interface signature - Automatic Satisfaction: Go automatically recognizes that
os.Stdoutsatisfies theio.Writerinterface - no explicit declaration needed - Method Call: Inside the function, we call the
Writemethod 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.
Interface Example
type scribe struct {
data []byte
}
func (s *scribe) Write(p []byte) (int, error) {
s.data = p
return len(p), nil
}
package main
import (
"fmt"
"io"
)
// section: scribe
type scribe struct {
data []byte
}
func (s *scribe) Write(p []byte) (int, error) {
s.data = p
return len(p), nil
}
// section: scribe
func main() {
s := &scribe{}
writeSomething(s)
fmt.Println(string(s.data)) // Hello
}
func writeSomething(w io.Writer) {
w.Write([]byte("Hello"))
}Let’s examine the key parts of this custom type:
- Custom Type Definition: We define a
Scribestruct that will implement theio.Writerinterface - Method Implementation: The
Writemethod signature matches exactly what theio.Writerinterface requires:Write(p []byte) (n int, err error) - Custom Behavior: Instead of writing to a file or stdout, our implementation stores the data in the struct’s
datafield - Return Values: We return the number of bytes written and
nilfor 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"))
}package main
import (
"fmt"
"io"
)
// section: scribe
type scribe struct {
data []byte
}
func (s *scribe) Write(p []byte) (int, error) {
s.data = p
return len(p), nil
}
// section: scribe
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)
}
package main
// section: scribe
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)
}
// section: scribeAssignability
Remember that when determining assignability, Go is only concerned with the method set of an interface–not the name of the interface:
package main
import "fmt"
// section: interfaces
type Greeter interface {
Greet(string)
}
type Welcomer interface {
Greet(string)
}
// section: interfaces
// section: person
type Person struct{}
func (p *Person) Greet(name string) {
fmt.Println("Hello", name)
}
// section: person
func main() {
// section: assign
p := &Person{}
var g Greeter = p
var w Welcomer = g
g.Greet("world")
w.Greet("tim")
// section: assign
}In the eyes of the Go type system, these two interfaces are equivalent, so anything satisfying one interface will also satisfy the other.
package main
import "fmt"
// section: interfaces
type Greeter interface {
Greet(string)
}
type Welcomer interface {
Greet(string)
}
// section: interfaces
// section: person
type Person struct{}
func (p *Person) Greet(name string) {
fmt.Println("Hello", name)
}
// section: person
func main() {
// section: assign
p := &Person{}
var g Greeter = p
var w Welcomer = g
g.Greet("world")
w.Greet("tim")
// section: assign
}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())
}
package main
import (
"fmt"
"math"
)
// section: code
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())
}
// section: code
/*
// section: output
706.8583470577034
// section: output
*/Output:
package main
import (
"fmt"
"math"
)
// section: code
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())
}
// section: code
/*
// section: output
706.8583470577034
// section: output
*/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 1⁄2 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)
}
package main
import (
"fmt"
"math"
)
// section: code
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))
}
// section: paint
func paint(c Circle) string {
paint := .5 * c.Area()
return fmt.Sprintf("you need %.2f gallons of paint", paint)
}
// section: paint
// section: code
/*
// section: output
706.8583470577034
you need 353.43 gallons of paint
// section: output
*/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)
}
package main
import (
"fmt"
"math"
)
// section: code
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))
}
// section: paint
func paint(c Circle) string {
paint := .5 * c.Area()
return fmt.Sprintf("you need %.2f gallons of paint", paint)
}
// section: paint
// section: code
/*
// section: output
706.8583470577034
you need 353.43 gallons of paint
// section: output
*/Output:
package main
import (
"fmt"
"math"
)
// section: code
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))
}
// section: paint
func paint(c Circle) string {
paint := .5 * c.Area()
return fmt.Sprintf("you need %.2f gallons of paint", paint)
}
// section: paint
// section: code
/*
// section: output
706.8583470577034
you need 353.43 gallons of paint
// section: output
*/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
}
package main
import (
"fmt"
"math"
)
// section: code
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
// section: rectangle
type Rectangle struct {
Height float64
Width float64
}
func (r Rectangle) Area() float64 {
return r.Height * r.Width
}
// section: rectangle
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)
}
// section: code
/*
// section: output
706.8583470577034
you need 353.43 gallons of paint
600
// section: output
*/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)
}
package main
import (
"fmt"
"math"
)
// section: code
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
// section: rectangle
type Rectangle struct {
Height float64
Width float64
}
func (r Rectangle) Area() float64 {
return r.Height * r.Width
}
// section: rectangle
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)
}
// section: code
/*
// section: output
706.8583470577034
you need 353.43 gallons of paint
600
// section: output
*/Output:
package main
import (
"fmt"
"math"
)
// section: code
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
// section: rectangle
type Rectangle struct {
Height float64
Width float64
}
func (r Rectangle) Area() float64 {
return r.Height * r.Width
}
// section: rectangle
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)
}
// section: code
/*
// section: output
706.8583470577034
you need 353.43 gallons of paint
600
// section: output
*/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)
}
package main
import (
"fmt"
"math"
)
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())
// section: error
fmt.Println(paint(r))
// section: error
}
// section: paint
func paint(c Circle) string {
paint := .5 * c.Area()
return fmt.Sprintf("you need %.2f gallons of paint", paint)
}
// section: paint
// section: code
/*
// section: output
cannot use r (type Rectangle) as type Circle in argument to paint
// section: output
*/However, because paint takes a concrete type of Circle as an argument, we are not able to pass a rectangle as an argument:
package main
import (
"fmt"
"math"
)
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())
// section: error
fmt.Println(paint(r))
// section: error
}
// section: paint
func paint(c Circle) string {
paint := .5 * c.Area()
return fmt.Sprintf("you need %.2f gallons of paint", paint)
}
// section: paint
// section: code
/*
// section: output
cannot use r (type Rectangle) as type Circle in argument to paint
// section: output
*/If we do, we’ll receive the following compiler error:
package main
import (
"fmt"
"math"
)
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())
// section: error
fmt.Println(paint(r))
// section: error
}
// section: paint
func paint(c Circle) string {
paint := .5 * c.Area()
return fmt.Sprintf("you need %.2f gallons of paint", paint)
}
// section: paint
// section: code
/*
// section: output
cannot use r (type Rectangle) as type Circle in argument to paint
// section: output
*/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:
package main
import (
"fmt"
"math"
)
// section: code
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
}
// section: sizer
type Sizer interface {
Area() float64
}
// section: sizer
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)
}
// section: code
/*
// section: output
706.8583470577034
you need 353.43 gallons of paint
600
you need 300.00 gallons of paint
// section: output
*/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)
}
package main
import (
"fmt"
"math"
)
// section: code
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
}
// section: sizer
type Sizer interface {
Area() float64
}
// section: sizer
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)
}
// section: code
/*
// section: output
706.8583470577034
you need 353.43 gallons of paint
600
you need 300.00 gallons of paint
// section: output
*/Output:
package main
import (
"fmt"
"math"
)
// section: code
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
}
// section: sizer
type Sizer interface {
Area() float64
}
// section: sizer
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)
}
// section: code
/*
// section: output
706.8583470577034
you need 353.43 gallons of paint
600
you need 300.00 gallons of paint
// section: output
*/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)
}
package main
import (
"fmt"
"math"
)
// section: code
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
// section: string
func (c Circle) String() string {
return fmt.Sprintf("Circle{Radius: %.2f}", c.Radius)
}
// section: string
type Rectangle struct {
Height float64
Width float64
}
func (r Rectangle) Area() float64 {
return r.Height * r.Width
}
// section: string
func (r Rectangle) String() string {
return fmt.Sprintf("Rectangle{Height: %.2f, Width: %.2f}", r.Height, r.Width)
}
// section: string
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)
}
// section: code
/*
// section: 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
// section: output
*/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)
}
package main
import (
"fmt"
"math"
)
// section: code
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
// section: string
func (c Circle) String() string {
return fmt.Sprintf("Circle{Radius: %.2f}", c.Radius)
}
// section: string
type Rectangle struct {
Height float64
Width float64
}
func (r Rectangle) Area() float64 {
return r.Height * r.Width
}
// section: string
func (r Rectangle) String() string {
return fmt.Sprintf("Rectangle{Height: %.2f, Width: %.2f}", r.Height, r.Width)
}
// section: string
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)
}
// section: code
/*
// section: 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
// section: output
*/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 paintpackage main
import (
"fmt"
"math"
)
// section: code
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
// section: string
func (c Circle) String() string {
return fmt.Sprintf("Circle{Radius: %.2f}", c.Radius)
}
// section: string
type Rectangle struct {
Height float64
Width float64
}
func (r Rectangle) Area() float64 {
return r.Height * r.Width
}
// section: string
func (r Rectangle) String() string {
return fmt.Sprintf("Rectangle{Height: %.2f, Width: %.2f}", r.Height, r.Width)
}
// section: string
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)
}
// section: code
/*
// section: 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
// section: output
*/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:
package main
import (
"fmt"
"math"
)
// section: code
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
}
// section: shaper
type Shaper interface {
Sizer // Area() float64
fmt.Stringer // String() string
}
// section: shaper
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)
}
// section: compare
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())
}
// section: compare
// section: code
/*
// section: 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
// section: output
*/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())
}
package main
import (
"fmt"
"math"
)
// section: code
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
}
// section: shaper
type Shaper interface {
Sizer // Area() float64
fmt.Stringer // String() string
}
// section: shaper
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)
}
// section: compare
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())
}
// section: compare
// section: code
/*
// section: 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
// section: output
*/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.8583470577034package main
import (
"fmt"
"math"
)
// section: code
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
}
// section: shaper
type Shaper interface {
Sizer // Area() float64
fmt.Stringer // String() string
}
// section: shaper
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)
}
// section: compare
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())
}
// section: compare
// section: code
/*
// section: 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
// section: output
*/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())
}
package main
import (
"fmt"
"math"
)
// section: code
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
}
// section: shaper
type Shaper interface {
Sizer // Area() float64
fmt.Stringer // String() string
}
// section: shaper
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)
}
// section: compare
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())
}
// section: compare
// section: code
/*
// section: 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
// section: output
*/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}{1 Rob Pike}
{10 Plush Gopher}Desired Output:
User 1 is Rob Pike
Product 10 is Plush GopherUser 1 is Rob Pike
Product 10 is Plush GopherFinally, 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
}
}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())
}
}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")
}
package main
import (
"fmt"
)
type User struct {
First string
Last string
}
func (u User) String() string {
return fmt.Sprintf("User name is %s %s", u.First, u.Last)
}
func main() {
u := User{First: "Rob", Last: "Pike"}
print(u)
print("bob")
}
// section: print
func print(s any) {
stringer, ok := s.(fmt.Stringer)
if ok {
fmt.Println(stringer.String())
return
}
fmt.Println("not a stringer")
}
// section: print
// section: printinline
func printInline(s any) {
if stringer, ok := s.(fmt.Stringer); ok {
fmt.Println(stringer.String())
return
}
fmt.Println("not a stringer")
}
// section: printinlineThis 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")
}
package main
import (
"fmt"
)
type User struct {
First string
Last string
}
func (u User) String() string {
return fmt.Sprintf("User name is %s %s", u.First, u.Last)
}
func main() {
u := User{First: "Rob", Last: "Pike"}
print(u)
print("bob")
}
// section: print
func print(s any) {
stringer, ok := s.(fmt.Stringer)
if ok {
fmt.Println(stringer.String())
return
}
fmt.Println("not a stringer")
}
// section: print
// section: printinline
func printInline(s any) {
if stringer, ok := s.(fmt.Stringer); ok {
fmt.Println(stringer.String())
return
}
fmt.Println("not a stringer")
}
// section: printinlineWatch 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())
}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)
}
}
package main
import (
"encoding/json"
"fmt"
)
// section: main
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)
}
}
// section: mainYou 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)
}
}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")
}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))
}
}
package main
import "fmt"
// section: types
// 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)
}
// section: types
// section: constraint
// 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))
}
}
// section: constraint
// section: main
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
}
// section: mainOur 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)
}
package main
import "fmt"
// section: types
// 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)
}
// section: types
// section: constraint
// 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))
}
}
// section: constraint
// section: main
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
}
// section: mainCalling 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
}
package main
import "fmt"
// section: types
// 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)
}
// section: types
// section: constraint
// 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))
}
}
// section: constraint
// section: main
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
}
// section: mainBe 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.
package main
import "fmt"
// section: content
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:
package main
import (
"fmt"
"io"
"os"
)
func main() {
backed()
empty()
}
// section: content
// section: type
type Stream struct {
io.Writer
}
// section: type
func backed() {
// section: backed
s := Stream{
Writer: os.Stdout,
}
fmt.Fprintf(s, "Hello Gophers!")
// section: backed
}
func empty() {
// section: empty
s := Stream{}
fmt.Fprintf(s, "Hello Gophers!")
// section: empty
}
/*
// section: 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
// section: panic
*/You will need to back the interface with a concrete type to work:
package main
import (
"fmt"
"io"
"os"
)
func main() {
backed()
empty()
}
// section: content
// section: type
type Stream struct {
io.Writer
}
// section: type
func backed() {
// section: backed
s := Stream{
Writer: os.Stdout,
}
fmt.Fprintf(s, "Hello Gophers!")
// section: backed
}
func empty() {
// section: empty
s := Stream{}
fmt.Fprintf(s, "Hello Gophers!")
// section: empty
}
/*
// section: 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
// section: panic
*/However, sometimes in your code, you will forget to do this:
package main
import (
"fmt"
"io"
"os"
)
func main() {
backed()
empty()
}
// section: content
// section: type
type Stream struct {
io.Writer
}
// section: type
func backed() {
// section: backed
s := Stream{
Writer: os.Stdout,
}
fmt.Fprintf(s, "Hello Gophers!")
// section: backed
}
func empty() {
// section: empty
s := Stream{}
fmt.Fprintf(s, "Hello Gophers!")
// section: empty
}
/*
// section: 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
// section: panic
*/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 2package main
import (
"fmt"
"io"
"os"
)
func main() {
backed()
empty()
}
// section: content
// section: type
type Stream struct {
io.Writer
}
// section: type
func backed() {
// section: backed
s := Stream{
Writer: os.Stdout,
}
fmt.Fprintf(s, "Hello Gophers!")
// section: backed
}
func empty() {
// section: empty
s := Stream{}
fmt.Fprintf(s, "Hello Gophers!")
// section: empty
}
/*
// section: 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
// section: panic
*/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)
}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)
}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)
}
package main
import (
"bytes"
"fmt"
"io"
"os"
)
// section: logger
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)
}
// section: logger
// section: good
// Good: accepts an interface, returns a concrete *Logger.
func NewLogger(w io.Writer) *Logger {
return &Logger{output: w, level: InfoLevel}
}
// section: good
// section: bad
// Bad: accepts an interface, but also returns an interface.
func NewLoggerBad(w io.Writer) io.Writer {
return &Logger{output: w, level: InfoLevel}
}
// section: bad
// section: usage-good
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")
}
// section: usage-good
// section: usage-bad
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")
}
}
// section: usage-bad
func main() {
useGood()
useBad()
// The interface parameter really is flexible:
buf := &bytes.Buffer{}
NewLogger(buf).Log("logging into a bytes.Buffer")
fmt.Print(buf.String())
}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}
}
package main
import (
"bytes"
"fmt"
"io"
"os"
)
// section: logger
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)
}
// section: logger
// section: good
// Good: accepts an interface, returns a concrete *Logger.
func NewLogger(w io.Writer) *Logger {
return &Logger{output: w, level: InfoLevel}
}
// section: good
// section: bad
// Bad: accepts an interface, but also returns an interface.
func NewLoggerBad(w io.Writer) io.Writer {
return &Logger{output: w, level: InfoLevel}
}
// section: bad
// section: usage-good
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")
}
// section: usage-good
// section: usage-bad
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")
}
}
// section: usage-bad
func main() {
useGood()
useBad()
// The interface parameter really is flexible:
buf := &bytes.Buffer{}
NewLogger(buf).Log("logging into a bytes.Buffer")
fmt.Print(buf.String())
}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")
}
package main
import (
"bytes"
"fmt"
"io"
"os"
)
// section: logger
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)
}
// section: logger
// section: good
// Good: accepts an interface, returns a concrete *Logger.
func NewLogger(w io.Writer) *Logger {
return &Logger{output: w, level: InfoLevel}
}
// section: good
// section: bad
// Bad: accepts an interface, but also returns an interface.
func NewLoggerBad(w io.Writer) io.Writer {
return &Logger{output: w, level: InfoLevel}
}
// section: bad
// section: usage-good
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")
}
// section: usage-good
// section: usage-bad
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")
}
}
// section: usage-bad
func main() {
useGood()
useBad()
// The interface parameter really is flexible:
buf := &bytes.Buffer{}
NewLogger(buf).Log("logging into a bytes.Buffer")
fmt.Print(buf.String())
}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 allLoggermethods 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}
}
package main
import (
"bytes"
"fmt"
"io"
"os"
)
// section: logger
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)
}
// section: logger
// section: good
// Good: accepts an interface, returns a concrete *Logger.
func NewLogger(w io.Writer) *Logger {
return &Logger{output: w, level: InfoLevel}
}
// section: good
// section: bad
// Bad: accepts an interface, but also returns an interface.
func NewLoggerBad(w io.Writer) io.Writer {
return &Logger{output: w, level: InfoLevel}
}
// section: bad
// section: usage-good
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")
}
// section: usage-good
// section: usage-bad
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")
}
}
// section: usage-bad
func main() {
useGood()
useBad()
// The interface parameter really is flexible:
buf := &bytes.Buffer{}
NewLogger(buf).Log("logging into a bytes.Buffer")
fmt.Print(buf.String())
}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")
}
}
package main
import (
"bytes"
"fmt"
"io"
"os"
)
// section: logger
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)
}
// section: logger
// section: good
// Good: accepts an interface, returns a concrete *Logger.
func NewLogger(w io.Writer) *Logger {
return &Logger{output: w, level: InfoLevel}
}
// section: good
// section: bad
// Bad: accepts an interface, but also returns an interface.
func NewLoggerBad(w io.Writer) io.Writer {
return &Logger{output: w, level: InfoLevel}
}
// section: bad
// section: usage-good
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")
}
// section: usage-good
// section: usage-bad
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")
}
}
// section: usage-bad
func main() {
useGood()
useBad()
// The interface parameter really is flexible:
buf := &bytes.Buffer{}
NewLogger(buf).Log("logging into a bytes.Buffer")
fmt.Print(buf.String())
}Problems with returning the interface:
- The caller can only use
io.Writermethods (justWrite) Logger-specific methods likeSetLevelare 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}
}
package main
import "fmt"
type Config struct {
Host string
Port int
}
// section: bad
// Bad: returns any, providing no type information.
func GetConfigAny() any {
return &Config{Host: "localhost", Port: 8080}
}
// section: bad
// section: good
// Good: returns the concrete type.
func GetConfig() *Config {
return &Config{Host: "localhost", Port: 8080}
}
// section: good
func main() {
// With any, the caller is forced to assert before using fields.
anyCfg := GetConfigAny()
// anyCfg.Host // won't compile: any has no fields
cfg := anyCfg.(*Config) // required, and error-prone
fmt.Println("via any:", cfg.Host)
// With the concrete type, fields are available immediately.
c := GetConfig()
fmt.Println("via concrete:", c.Host)
}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}
}
package main
import "fmt"
type Config struct {
Host string
Port int
}
// section: bad
// Bad: returns any, providing no type information.
func GetConfigAny() any {
return &Config{Host: "localhost", Port: 8080}
}
// section: bad
// section: good
// Good: returns the concrete type.
func GetConfig() *Config {
return &Config{Host: "localhost", Port: 8080}
}
// section: good
func main() {
// With any, the caller is forced to assert before using fields.
anyCfg := GetConfigAny()
// anyCfg.Host // won't compile: any has no fields
cfg := anyCfg.(*Config) // required, and error-prone
fmt.Println("via any:", cfg.Host)
// With the concrete type, fields are available immediately.
c := GetConfig()
fmt.Println("via concrete:", c.Host)
}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()
}
package main
import (
"fmt"
"io"
)
// section: compose
// 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()
}
// section: compose
func main() {
pump(&Pipe{})
}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)
}
package main
import (
"errors"
"fmt"
)
type User struct {
ID int
Name string
}
// section: focused
// 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)
}
// section: focused
// memStore is a concrete type that satisfies UserRepository.
type memStore struct {
users map[int]*User
}
func (m *memStore) FindByID(id int) (*User, error) {
u, ok := m.users[id]
if !ok {
return nil, errors.New("no such user")
}
return u, nil
}
func (m *memStore) Create(user *User) error {
m.users[user.ID] = user
return nil
}
func (m *memStore) Delete(id int) error {
delete(m.users, id)
return nil
}
func main() {
store := &memStore{users: map[int]*User{}}
store.Create(&User{ID: 1, Name: "Ada"})
// DisplayUser only asks for UserFinder, so it works with anything
// that can find a user, not just the full repository.
DisplayUser(1, store)
DisplayUser(2, store)
}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
}
package main
import (
"errors"
"fmt"
)
type User struct {
ID int
Email string
}
// section: bad
// 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
}
// section: bad
// section: good
// 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)
}
// section: good
// store satisfies the two small interfaces, nothing more.
type store struct {
users []*User
}
func (s *store) Validate(u *User) error {
if u.Email == "" {
return errors.New("email is required")
}
return nil
}
func (s *store) Create(u *User) error {
s.users = append(s.users, u)
return nil
}
func main() {
s := &store{}
if err := ValidateAndCreate(&User{ID: 1, Email: "ada@example.com"}, s, s); err != nil {
fmt.Println("error:", err)
}
fmt.Printf("created %d user(s)\n", len(s.users))
if err := ValidateAndCreate(&User{ID: 2}, s, s); err != nil {
fmt.Println("rejected:", err)
}
}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)
}
package main
import (
"errors"
"fmt"
)
type User struct {
ID int
Email string
}
// section: bad
// 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
}
// section: bad
// section: good
// 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)
}
// section: good
// store satisfies the two small interfaces, nothing more.
type store struct {
users []*User
}
func (s *store) Validate(u *User) error {
if u.Email == "" {
return errors.New("email is required")
}
return nil
}
func (s *store) Create(u *User) error {
s.users = append(s.users, u)
return nil
}
func main() {
s := &store{}
if err := ValidateAndCreate(&User{ID: 1, Email: "ada@example.com"}, s, s); err != nil {
fmt.Println("error:", err)
}
fmt.Printf("created %d user(s)\n", len(s.users))
if err := ValidateAndCreate(&User{ID: 2}, s, s); err != nil {
fmt.Println("rejected:", err)
}
}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
}
// section: producer
// 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
}
// section: producerThe 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.
// section: 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.
// section: consumerWhy 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.
package cache
import "testing"
// section: mock
// 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.
// section: mock
func TestCachePutGet(t *testing.T) {
c := New(&mockStorage{data: map[string][]byte{}})
if err := c.Put("k", []byte("v")); err != nil {
t.Fatal(err)
}
got, err := c.Get("k")
if err != nil {
t.Fatal(err)
}
if string(got) != "v" {
t.Fatalf("got %q, want %q", got, "v")
}
}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
}
// section: producer
// 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
}
// section: producerNow 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.
// section: consumer
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.
// section: consumerInterface 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()
}
package main
import (
"errors"
"fmt"
)
// section: good
// 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()
}
// section: good
// section: bad
// 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") }
// section: bad
func main() {
f := &File{name: "notes.txt"}
f.Write([]byte("hello"))
data, _ := ProcessData(f) // only the Reader capability is used
fmt.Printf("good: read %q via a narrow interface\n", data)
var store DataStore = &ReadOnlyStore{}
if err := store.Write([]byte("nope")); err != nil {
fmt.Println("bad:", err)
}
}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") }
package main
import (
"errors"
"fmt"
)
// section: good
// 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()
}
// section: good
// section: bad
// 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") }
// section: bad
func main() {
f := &File{name: "notes.txt"}
f.Write([]byte("hello"))
data, _ := ProcessData(f) // only the Reader capability is used
fmt.Printf("good: read %q via a narrow interface\n", data)
var store DataStore = &ReadOnlyStore{}
if err := store.Write([]byte("nope")); err != nil {
fmt.Println("bad:", err)
}
}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
}
package main
import (
"encoding/json"
"fmt"
"io"
"os"
)
type User struct {
ID int
Name string
}
// DB stands in for something like *sql.DB.
type DB struct{}
// section: narrow
// 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
}
// section: narrow
// section: concrete
// 2. Return a concrete type, not an interface.
type UserService struct {
db *DB
}
func NewUserService(db *DB) *UserService {
return &UserService{db: db}
}
// section: concrete
// section: small
// 3. Keep interfaces small and focused.
type Validator interface {
Validate() error
}
type Saver interface {
Save() error
}
// section: small
// section: compose
// 4. Compose small interfaces into larger ones.
type Reader interface {
Read() ([]byte, error)
}
type Writer interface {
Write([]byte) error
}
type ReadWriter interface {
Reader
Writer
}
// section: compose
func main() {
_ = NewUserService(&DB{})
if err := SaveUser(&User{ID: 1, Name: "Ada"}, os.Stdout); err != nil {
fmt.Println("error:", err)
}
fmt.Println()
}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}
}
package main
import (
"encoding/json"
"fmt"
"io"
"os"
)
type User struct {
ID int
Name string
}
// DB stands in for something like *sql.DB.
type DB struct{}
// section: narrow
// 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
}
// section: narrow
// section: concrete
// 2. Return a concrete type, not an interface.
type UserService struct {
db *DB
}
func NewUserService(db *DB) *UserService {
return &UserService{db: db}
}
// section: concrete
// section: small
// 3. Keep interfaces small and focused.
type Validator interface {
Validate() error
}
type Saver interface {
Save() error
}
// section: small
// section: compose
// 4. Compose small interfaces into larger ones.
type Reader interface {
Read() ([]byte, error)
}
type Writer interface {
Write([]byte) error
}
type ReadWriter interface {
Reader
Writer
}
// section: compose
func main() {
_ = NewUserService(&DB{})
if err := SaveUser(&User{ID: 1, Name: "Ada"}, os.Stdout); err != nil {
fmt.Println("error:", err)
}
fmt.Println()
}3. Small, focused interfaces
// 3. Keep interfaces small and focused.
type Validator interface {
Validate() error
}
type Saver interface {
Save() error
}
package main
import (
"encoding/json"
"fmt"
"io"
"os"
)
type User struct {
ID int
Name string
}
// DB stands in for something like *sql.DB.
type DB struct{}
// section: narrow
// 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
}
// section: narrow
// section: concrete
// 2. Return a concrete type, not an interface.
type UserService struct {
db *DB
}
func NewUserService(db *DB) *UserService {
return &UserService{db: db}
}
// section: concrete
// section: small
// 3. Keep interfaces small and focused.
type Validator interface {
Validate() error
}
type Saver interface {
Save() error
}
// section: small
// section: compose
// 4. Compose small interfaces into larger ones.
type Reader interface {
Read() ([]byte, error)
}
type Writer interface {
Write([]byte) error
}
type ReadWriter interface {
Reader
Writer
}
// section: compose
func main() {
_ = NewUserService(&DB{})
if err := SaveUser(&User{ID: 1, Name: "Ada"}, os.Stdout); err != nil {
fmt.Println("error:", err)
}
fmt.Println()
}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
}
package main
import (
"encoding/json"
"fmt"
"io"
"os"
)
type User struct {
ID int
Name string
}
// DB stands in for something like *sql.DB.
type DB struct{}
// section: narrow
// 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
}
// section: narrow
// section: concrete
// 2. Return a concrete type, not an interface.
type UserService struct {
db *DB
}
func NewUserService(db *DB) *UserService {
return &UserService{db: db}
}
// section: concrete
// section: small
// 3. Keep interfaces small and focused.
type Validator interface {
Validate() error
}
type Saver interface {
Save() error
}
// section: small
// section: compose
// 4. Compose small interfaces into larger ones.
type Reader interface {
Read() ([]byte, error)
}
type Writer interface {
Write([]byte) error
}
type ReadWriter interface {
Reader
Writer
}
// section: compose
func main() {
_ = NewUserService(&DB{})
if err := SaveUser(&User{ID: 1, Name: "Ada"}, os.Stdout); err != nil {
fmt.Println("error:", err)
}
fmt.Println()
}❌ 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}
}
package main
import "fmt"
type User struct {
ID int
}
type (
Config struct{}
Status struct{}
Metrics struct{}
)
// section: return-any
// 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}
}
// section: return-any
// section: premature
// 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} }
// section: premature
// section: large
// 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
}
// section: large
func main() {
fmt.Printf("concrete return: %+v\n", GetUser(1))
fmt.Printf("any return needs asserting: %v\n", GetUserAny(2).(*User))
}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} }
package main
import "fmt"
type User struct {
ID int
}
type (
Config struct{}
Status struct{}
Metrics struct{}
)
// section: return-any
// 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}
}
// section: return-any
// section: premature
// 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} }
// section: premature
// section: large
// 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
}
// section: large
func main() {
fmt.Printf("concrete return: %+v\n", GetUser(1))
fmt.Printf("any return needs asserting: %v\n", GetUserAny(2).(*User))
}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
}
package main
import "fmt"
type User struct {
ID int
}
type (
Config struct{}
Status struct{}
Metrics struct{}
)
// section: return-any
// 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}
}
// section: return-any
// section: premature
// 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} }
// section: premature
// section: large
// 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
}
// section: large
func main() {
fmt.Printf("concrete return: %+v\n", GetUser(1))
fmt.Printf("any return needs asserting: %v\n", GetUserAny(2).(*User))
}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")
}
package main
import "fmt"
type User struct {
Email string
}
// section: before
// 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")
}
// section: before
func main() {
NotifyUser(&User{Email: "ada@example.com"}, &EmailSender{smtpHost: "smtp.example.com"})
}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")
}
package main
import "fmt"
type User struct {
Email string
}
// section: after
// 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")
}
// section: after
func main() {
user := &User{Email: "ada@example.com"}
NotifyUser(user, &EmailSender{smtpHost: "smtp.example.com"})
NotifyUser(user, &SMSSender{apiKey: "secret"})
}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))
}
}
package main
import "testing"
// section: mock
// 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))
}
}
// section: mockSummary: Interface Design Best Practices
- Accept interfaces, return concrete types - Maximize flexibility in parameters, provide clarity in returns
- Keep interfaces small - Aim for 1-3 methods per interface
- Define at point of use - Consumers define interfaces, not producers
- Interface segregation - Don’t force implementations to satisfy unused methods
- Avoid
interface{}/anyin returns - Preserve type safety - Avoid premature abstraction - Create interfaces when you need them, not before
- 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
}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
}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)
}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)
}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]