Interfaces in Go: Implicit Implementation and Polymorphism
Chapter 03 of 06

Interfaces in Go: Implicit Implementation and Polymorphism

Implicit interfaces, a payment gateway example, io.Reader and io.Writer, type switches and the nil interface gotcha.

Sabbir Ahmmed3 min read0
On this page

An interface describes behaviour: a set of method signatures. Any type that has those methods satisfies the interface automatically. There is no implements keyword. This "implicit" satisfaction is what makes Go code so easy to extend, mock and test.

Your first interface

animals.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

type Animal interface {
	Speak() string
}

type Dog struct{}
type Cat struct{}

func (Dog) Speak() string { return "Woof" }
func (Cat) Speak() string { return "Meow" }

// Works with ANY type that can Speak
func MakeSound(a Animal) {
	fmt.Println(a.Speak())
}

func main() {
	MakeSound(Dog{}) // Woof
	MakeSound(Cat{}) // Meow
}

We never wrote "Dog implements Animal". Dog has a Speak() string method, so it is an Animal. This is polymorphism: the same function call produces different behaviour depending on the concrete type at runtime.

A real-world example: payment gateways

Interfaces shine when you have several implementations of the same capability. A checkout service should not care whether money moves through Stripe or Razorpay:

payment.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import "fmt"

type PaymentGateway interface {
	Pay(amount float64, currency string) (string, error)
	Refund(paymentID string) error
}

type Stripe struct{}

func (Stripe) Pay(amount float64, currency string) (string, error) {
	return fmt.Sprintf("stripe: charged %.2f %s", amount, currency), nil
}
func (Stripe) Refund(id string) error {
	fmt.Println("stripe: refunded", id)
	return nil
}

type Razorpay struct{}

func (Razorpay) Pay(amount float64, currency string) (string, error) {
	return fmt.Sprintf("razorpay: charged %.2f %s", amount, currency), nil
}
func (Razorpay) Refund(id string) error {
	fmt.Println("razorpay: refunded", id)
	return nil
}

func Checkout(pg PaymentGateway, amount float64, currency string) error {
	receipt, err := pg.Pay(amount, currency)
	if err != nil {
		return fmt.Errorf("checkout failed: %w", err)
	}
	fmt.Println(receipt)
	return nil
}

func main() {
	Checkout(Stripe{}, 100, "USD")
	Checkout(Razorpay{}, 200, "INR")
}

Adding PayPal later means writing one new type. Checkout does not change. In tests, you pass a fakeGateway that records calls instead of hitting a real API. That is dependency inversion without any framework.

Tip
Note Checkout handles the error from Pay. The original version discarded it with result, _ :=, which would silently report success on a failed payment.

Interfaces from the standard library: io.Reader and io.Writer

The most important interfaces in Go are tiny:

go
1
2
3
4
5
6
7
type Reader interface {
	Read(p []byte) (n int, err error)
}

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

Files, network connections, HTTP bodies, buffers, gzip streams and hashes all implement them. That is why this works:

go
1
2
3
4
5
6
7
resp, err := http.Get("https://go.dev")
if err != nil {
	log.Fatal(err)
}
defer resp.Body.Close()

io.Copy(os.Stdout, resp.Body) // copy any Reader into any Writer

io.Copy does not know or care that the source is an HTTP response and the destination is the terminal. It streams in chunks, so it also works for files much larger than memory, unlike reading into a fixed make([]byte, 99999) buffer.

Small interfaces, defined by the consumer

Go interfaces are usually small (one or two methods) and declared where they are used, not next to the implementation. A function that only needs to save users should ask for exactly that:

go
1
2
3
type UserSaver interface {
	Save(ctx context.Context, u User) error
}

Your Postgres repository might have twenty methods, but it still satisfies UserSaver. The proverb is: "Accept interfaces, return structs."

Pointer receivers and interface satisfaction

go
1
2
3
4
5
6
7
8
type Bot interface{ Greet() string }

type EnglishBot struct{}

func (b *EnglishBot) Greet() string { return "Hi there!" } // pointer receiver

var b Bot = &EnglishBot{} // OK
var c Bot = EnglishBot{}  // compile error: Greet has a pointer receiver

If a method has a pointer receiver, only the pointer type has that method in its method set. A common source of "does not implement" errors.

The empty interface, type assertions and type switches

interface{} (or its alias any) has no methods, so every type satisfies it. Use it sparingly. To get the concrete value back, use a type assertion or a type switch:

go
1
2
3
4
5
6
7
8
9
10
11
12
13
var v any = "hello"

s, ok := v.(string) // comma-ok assertion: no panic if wrong
fmt.Println(s, ok)  // hello true

switch x := v.(type) {
case string:
	fmt.Println("string of length", len(x))
case int:
	fmt.Println("int", x)
default:
	fmt.Printf("other: %T\n", x)
}

The nil interface gotcha

An interface value holds two things: a concrete type and a value. It is nil only if both are nil:

go
1
2
3
4
5
6
func find() error {
	var err *MyError = nil
	return err // returns a non-nil error holding a nil *MyError!
}

fmt.Println(find() == nil) // false

Return a literal nil when there is no error, never a typed nil pointer.

Key takeaways

A type satisfies an interface just by having its methods. No implements keyword.
Interfaces enable polymorphism, easy mocking and swappable implementations.
Keep interfaces small and define them where they are consumed.
io.Reader and io.Writer connect almost everything in the standard library.
Pointer-receiver methods belong only to the pointer type's method set.
An interface holding a typed nil pointer is not nil.
#Golang#Interfaces#Design patterns

Last updated on

Discussion0