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.
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
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:
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.
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:
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:
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 Writerio.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:
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
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 receiverIf 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:
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:
func find() error {
var err *MyError = nil
return err // returns a non-nil error holding a nil *MyError!
}
fmt.Println(find() == nil) // falseReturn a literal nil when there is no error, never a typed nil pointer.
Key takeaways
implements keyword.io.Reader and io.Writer connect almost everything in the standard library.nil.Last updated on