Intermediate · Pointers, interfaces and errors · topic 17 of 46 · 40 min
Interfaces: behaviour, not family trees
Explanation
The problem interfaces solve
Imagine a checkout that can charge by UPI, card or wallet. Without interfaces you would write a separate checkout for each, or a big switch. An interface lets you say: "I accept anything that can Pay(amount)". Each payment type implements Pay in its own way, and the checkout code never changes when you add a new one.
type PaymentMethod interface {
Pay(paise int64) error
}
type UPI struct{ VPA string }
type Card struct{ Last4 string }
func (u UPI) Pay(p int64) error {
fmt.Println("UPI", u.VPA, "paid", p)
return nil
}
func (c Card) Pay(p int64) error {
fmt.Println("Card", c.Last4, "paid", p)
return nil
}
func checkout(m PaymentMethod, total int64) error {
return m.Pay(total)
}
func main() {
checkout(UPI{VPA: "asha@okbank"}, 49900)
checkout(Card{Last4: "4242"}, 120000)
}Implicit satisfaction
In Java you write class UPI implements PaymentMethod. Go has no such keyword. A type satisfies an interface just by having all its methods with the exact signatures. This means you can define an interface later, in the package that USES it, for types you did not write.
Small interfaces from the standard library
| Interface | Method | Used by |
|---|---|---|
| fmt.Stringer | String() string | fmt.Println prints your type nicely |
| error | Error() string | every error value in Go |
| io.Reader | Read(p []byte) (n int, err error) | files, HTTP bodies, network connections, strings.Reader |
| io.Writer | Write(p []byte) (n int, err error) | files, os.Stdout, http.ResponseWriter, bytes.Buffer |
| sort.Interface | Len, Less, Swap | sort.Sort (older code) |
Because io.Writer is tiny, one function like fmt.Fprintf(w io.Writer, ...) can write to the screen, a file, an HTTP response or a buffer in memory. That is the power of small interfaces: "the bigger the interface, the weaker the abstraction" is a well-known Go proverb.
type Rupees int64
func (r Rupees) String() string {
return fmt.Sprintf("Rs %d.%02d", r/100, r%100)
}
func main() {
var price Rupees = 49950
fmt.Println(price) // Rs 499.50
}Pointer receivers and interfaces
If a method has a pointer receiver, only the pointer type has that method in its method set. So *T satisfies the interface but T does not. The compiler tells you clearly.
type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ }
type Incrementer interface{ Inc() }
var _ Incrementer = &Counter{} // OK
// var _ Incrementer = Counter{} // compile error: Counter does not implement
// // Incrementer (method Inc has pointer receiver)Interfaces make testing easy
If your code depends on an interface instead of a concrete database or payment gateway, a test can pass in a fake that records calls. No real money moves and no network is needed. This is why Go developers say: accept interfaces, return concrete types (structs).
type fakePay struct{ calls int }
func (f *fakePay) Pay(p int64) error { f.calls++; return nil }
// in a test:
// f := &fakePay{}
// checkout(f, 100)
// if f.calls != 1 { t.Fatal("expected one payment") }Design tips and mistakes
- Keep interfaces small: one to three methods is normal.
- Define the interface where it is used (the consumer), not next to the type that implements it.
- Do not create an interface for every struct "just in case". Add one when you have two implementations, or need a fake for tests.
- Interface names for single methods often end in -er: Reader, Writer, Stringer, Notifier.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Interfaces: behaviour, not family trees · 1/7
Interfaces describe behaviour: a list of methods a type must have.
Prefer a video by a teacher? Find free videos on “golang interfaces explained implicit implementation io.Reader io.Writer” on YouTube ↗
In real life: Notifications by SMS, email or WhatsApp
A bank sends OTP and alert messages by several channels. The alert code depends only on a Notifier interface with one method. New channels are added as new types, and tests use a fake notifier that just stores messages in a slice, so no real SMS is sent during testing.
package main
import "fmt"
type Notifier interface {
Notify(to, msg string) error
}
type SMS struct{}
type Email struct{ From string }
func (SMS) Notify(to, msg string) error {
fmt.Println("SMS to", to+":", msg)
return nil
}
func (e Email) Notify(to, msg string) error {
fmt.Println("Email from", e.From, "to", to+":", msg)
return nil
}
func alertAll(n Notifier, users []string, msg string) {
for _, u := range users {
if err := n.Notify(u, msg); err != nil {
fmt.Println("failed for", u, err)
}
}
}
func main() {
alertAll(SMS{}, []string{"98XXXXXX10"}, "Rs 2,000 debited")
alertAll(Email{From: "alerts@bank.example"}, []string{"asha@example.com"}, "Statement ready")
}Practice (0/4 done)
type Shape interface{ Area() float64 }. Which type satisfies Shape?
What does this print? type Temp float64 func (t Temp) String() string { return fmt.Sprintf("%.1f°C", float64(t)) } ... fmt.Println(Temp(36.6))
Why does this not compile? type Saver interface{ Save() } type Doc struct{} func (d *Doc) Save() {} var s Saver = Doc{}
Define an interface Shape with Area() float64 and Perimeter() float64. Implement it for Rect and Circle (use math.Pi). Write func describe(s Shape) that prints both values with 2 decimals, and call it with a slice of mixed shapes.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- any, type assertions and type switchesPointers, interfaces and errors · same module
- Errors as values: wrap, Is and AsPointers, interfaces and errors · same module
- InterfacesJava Programming (Core to Spring Boot) · Inheritance, polymorphism and interfaces
- Inheritance, interfaces, mixins and extensionsFlutter and Dart · Null safety and object-oriented Dart
- Interfaces: contracts for behaviourC# and .NET · Inheritance, interfaces and records
- Go language interview questionsCareers and interviews
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
How does a type declare that it implements an interface?
Answer: It does not; having the methods is enough
Go interfaces are satisfied implicitly.
Which method makes a type a fmt.Stringer?
Answer: String() string
fmt.Stringer has one method: String() string.
What does "accept interfaces, return structs" mean?
Answer: Functions take small interfaces as parameters and return concrete types
Taking interfaces makes code flexible and testable; returning concrete types keeps it clear.
Which interface does os.Stdout, a file and http.ResponseWriter all satisfy?
Answer: io.Writer
They all have Write([]byte) (int, error).
How many interfaces can a class implement?Related
Which statement about implicit interfaces is true?Related
Answer: Every class defines an interface other classes can implement
Any class can be implemented.
When should a method use a pointer receiver?Related
Answer: When it must change the receiver or the struct is large
Pointer receivers let methods modify the original and avoid copying big structs.
From Structs and methods
When is an interface value equal to nil?Related
Answer: When both its type and value parts are unset
A typed nil pointer inside an interface still makes the interface non-nil.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 3 more topics in “Pointers, interfaces and errors” before its exam: any, type assertions and type switches, Errors as values: wrap, Is and As, panic and recover: only for the truly unexpected.
- Module 4 exam: Pointers, interfaces and errors · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Packages, testing and generics. 46 topics left in the course before the final exam.