Interfaces
Reviewed & published by Brayan K
Interfaces are how Go does polymorphism — writing one function that works with many different types. By the end of this lesson you'll define an interface, satisfy it implicitly, store mixed types in one slice, and pull concrete values back out with type assertions and type switches.
Part of the free Go course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- Define an interface as a set of method signatures
- Satisfy an interface implicitly — no "implements" keyword
- Write functions that accept any type sharing a contract
- Hold mixed concrete types in one slice of an interface
- Use type assertions value.(T) safely with the ok flag
- Branch on dynamic type with a type switch
- Use the empty interface and the Stringer pattern
📊 Interface Vocabulary
| Term | Meaning |
|---|---|
| interface | A named set of method signatures — a contract |
| satisfies | A type has all the interface's methods (implicitly) |
| concrete type | A real struct/type that does the actual work |
| interface{} | The empty interface — satisfied by every type (also any) |
| type assertion | v, ok := i.(T) — pull a concrete value out |
Since Go 1.18 you can write any as a built-in alias for interface{}. They mean exactly the same thing.
1️⃣ Defining and Satisfying an Interface
An interface lists the methods a type must have. The crucial Go idea: satisfaction is implicit. You never write implements Speaker — if your type has a Speak() string method, it is a Speaker, full stop. This lets you write a function once and have it accept types that didn't even exist when you wrote it.
package main
import "fmt"
// An interface is a CONTRACT: a list of method names a type must have.
// Any type that has these methods automatically "satisfies" the interface.
// There is NO "implements" keyword in Go — it just works if the methods match.
type Speaker interface {
Speak() string
}
// Dog has a Speak method, so Dog satisfies Speaker — implicitly.
type Dog struct{ Name string }
func (d Dog) Speak() string {
return d.Name + " says Woof"
}
// Cat also satisfies Speaker.
type Cat struct{ Name string }
func (c Cat) Speak() string {
return c.Name + " says Meow"
}
// announce takes ANY Speaker — it doesn't care if it's a Dog or a Cat.
func announce(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
announce(Dog{Name: "Rex"})
announce(Cat{Name: "Lily"})
}Your turn. Give French the one method it needs to satisfy Greeter, then run it.
package main
import "fmt"
type Greeter interface {
Greet() string
}
type French struct{}
// 🎯 YOUR TURN — give French a Greet method so it satisfies Greeter.
func (f French) ___ string { // 👉 Greet()
return "Bonjour"
}
func sayHello(g Greeter) {
fmt.Println(g.Greet())
}
func main() {
sayHello(French{})
// ✅ Expected output: Bonjour
}2️⃣ A Slice of an Interface
Because every type that satisfies Shape can stand in for it, you can put different concrete types into a single []Shape and treat them uniformly. This is the everyday payoff of interfaces: collections of "things that can do X" without caring exactly what each one is.
package main
import "fmt"
type Shape interface {
Area() float64
}
type Rectangle struct{ W, H float64 }
func (r Rectangle) Area() float64 { return r.W * r.H }
type Circle struct{ R float64 }
func (c Circle) Area() float64 { return 3.14159 * c.R * c.R }
func main() {
// A slice of Shape can hold DIFFERENT concrete types together,
// because they all satisfy the same contract.
shapes := []Shape{
Rectangle{W: 3, H: 4},
Circle{R: 2},
Rectangle{W: 5, H: 5},
}
var total float64
for _, s := range shapes {
fmt.Printf("area = %.2f\n", s.Area())
total += s.Area()
}
fmt.Printf("total = %.2f\n", total)
}3️⃣ Type Assertions & Type Switches
Sometimes you have an interface value and need the real type back. A type assertion v, ok := i.(T) gives you the value and a boolean telling you whether the assertion held — always use the two-value form so a wrong guess doesn't panic. When you have several possibilities, a type switch reads far more cleanly.
package main
import "fmt"
type Stringer interface {
String() string
}
type ID int
func (id ID) String() string { return fmt.Sprintf("ID#%d", int(id)) }
// describe inspects what concrete type is inside an interface value.
func describe(i interface{}) {
// Type switch: branch on the dynamic type.
switch v := i.(type) {
case int:
fmt.Printf("an int: %d\n", v)
case string:
fmt.Printf("a string of length %d\n", len(v))
case Stringer:
fmt.Printf("a Stringer: %s\n", v.String())
default:
fmt.Printf("unknown type\n")
}
}
func main() {
describe(42)
describe("hello")
describe(ID(7))
// A type assertion pulls the concrete value back out, with a safety flag.
var any interface{} = "go"
s, ok := any.(string)
fmt.Println(s, ok) // go true
n, ok := any.(int)
fmt.Println(n, ok) // 0 false
}4️⃣ The Empty Interface & Stringer
The empty interface interface{} (or any) is satisfied by everything, which is exactly why fmt.Println can accept any value. One interface you'll meet constantly is fmt.Stringer: give your type a String() string method and fmt will use it automatically when printing.
package main
import "fmt"
// The fmt package looks for a String() string method. If a type has one,
// Println and Printf %v use it automatically. This is the Stringer pattern.
type Temperature struct {
Celsius float64
}
func (t Temperature) String() string {
return fmt.Sprintf("%.1f°C", t.Celsius)
}
func main() {
t := Temperature{Celsius: 21.5}
fmt.Println(t) // 21.5°C (uses String())
fmt.Printf("%v\n", t) // 21.5°C
fmt.Printf("%+v\n", t) // still uses String(): 21.5°C
}🧩 Reorder Challenge
These lines define a Speaker interface, a type that satisfies it, and use it. Put them in the correct order:
A. fmt.Println(r.Speak())
B. func (r Robot) Speak() string { return "Beep" }
C. type Speaker interface { Speak() string }
D. func main() {
E. var r Robot
F. type Robot struct{}
G. }C, F, B, D, E, A, G. Declare the interface, then the concrete type, then the method that satisfies the contract; inside main create a value and call its method. The interface and type definitions live at package level (outside main).
🧠 Quick Recall
Predict the output before revealing the answer.
var any interface{} = 7
s, ok := any.(string)
fmt.Println(s, ok)"" false (an empty string then false). The dynamic type is int, not string, so the assertion fails. The two-value form returns the zero value and ok = false instead of panicking.
2. Does this compile, and what does it print?
type Stringer interface { String() string }
type Coin int
func (c Coin) String() string { return "money" }
var x Stringer = Coin(5)
fmt.Println(x.String())It compiles and prints money. Coin has a String() string method, so it implicitly satisfies Stringer — no declaration needed.
3. What does the type switch print for describe(3.5)?
switch v := i.(type) {
case int: fmt.Println("int", v)
case string: fmt.Println("string", v)
default: fmt.Println("other")
}other — 3.5 is a float64, which matches neither the int nor the string case, so the default branch runs.
Pro Tips
- 💡 Keep interfaces small. The most useful interfaces in Go's standard library have one method (io.Reader, io.Writer, fmt.Stringer). Small contracts are easy to satisfy.
- 💡 Accept interfaces, return structs. Take the most general type you can as a parameter, but return a concrete type so callers get full information.
- 💡 Always use the two-value assertion v, ok := i.(T) unless you are certain — the single-value form panics on a mismatch.
- 💡 Define interfaces where they're used, not where the concrete type lives. The consumer knows what it needs.
Common Errors (and the fix)
- "Dog does not implement Speaker (missing method Speak)" — your method name, casing, or signature doesn't exactly match the interface. Speak() and speak() are different methods in Go.
- "interface conversion: interface {} is int, not string" — a single-value type assertion failed and panicked. Switch to v, ok := i.(T) and check ok.
- Pointer receiver surprise — if your method has a pointer receiver (d *Dog), then Dog{} (a value) does not satisfy the interface; only &Dog{} does. Be consistent with receiver types.
- "too many cases in type switch" isn't an error, but listing a type twice is — each case type must be unique.
Frequently Asked Questions
Q: Why doesn't Go have an implements keyword?
Go uses structural typing for interfaces: if the methods match, the type satisfies the interface automatically. This decouples your types from the interfaces other packages define, so a type can satisfy an interface that was written long after the type itself.
Q: What's the difference between any and interface{}?
None — any is a built-in alias for interface{} added in Go 1.18. Use any for readability in new code.
Q: When should I use a type switch instead of separate interfaces?
Prefer interfaces and polymorphism when behaviour varies by type. Reach for a type switch only when you genuinely must branch on the concrete type — for example, when decoding unknown JSON or formatting values for display.
Q: Can an interface embed another interface?
Yes. type ReadWriter interface { Reader; Writer } combines both method sets. A type satisfies it only if it has every method from both.
Mini-Challenge: Payment Notifier
No blanks this time — just a brief. Define a one-method interface, two types that satisfy it, then range over a slice holding both. This "list of things that share a behaviour" shape appears in almost every real Go codebase.
package main
import "fmt"
func main() {
// 🎯 MINI-CHALLENGE: a payment notifier
// 1. Define an interface Notifier with one method: Notify() string
// 2. Define two types, Email{Address string} and SMS{Number string},
// each with a Notify method returning a sensible message.
// 3. Make a []Notifier holding one of each and range over it,
// printing every Notify() result.
//
// ✅ Example output:
// Emailing sam@example.com
// Texting 0700-123456
// your code here
}🎉 Lesson Complete!
- ✅ An interface is a set of method signatures — a contract
- ✅ Types satisfy interfaces implicitly; there's no implements keyword
- ✅ A []Shape can hold many different concrete types at once
- ✅ Pull values out with v, ok := i.(T) and branch with a type switch
- ✅ interface{}/any accepts everything; fmt.Stringer customizes printing
Practice quiz
What is a Go interface?
- A struct with no fields
- A set of method signatures
- A list of variables
- A package import
Answer: A set of method signatures. An interface is a named set of method signatures — a behavioural contract.
How does a type satisfy an interface in Go?
- With an implements keyword
- By inheriting it
- Implicitly, by having the methods
- By registering it
Answer: Implicitly, by having the methods. Satisfaction is implicit: a type satisfies an interface simply by defining the required methods.
What does the empty interface interface{} (or any) accept?
- Only structs
- Only pointers
- Nothing
- Any value of any type
Answer: Any value of any type. interface{} / any has no method requirements, so every type satisfies it.
Which form safely extracts a concrete type from an interface value?
- v := i.(T)
- v, ok := i.(T)
- v := T(i)
- v := i.T
Answer: v, ok := i.(T). The comma-ok type assertion v, ok := i.(T) returns ok=false instead of panicking on a mismatch.
What does a type switch let you do?
- Convert numbers
- Branch on the dynamic type of an interface value
- Sort a slice
- Allocate memory
Answer: Branch on the dynamic type of an interface value. A type switch (switch v := i.(type)) branches based on the value's concrete type.
Can a []Shape slice hold different concrete types at once?
- No, only one type
- Yes, any type that satisfies Shape
- Only if they are pointers
- Only structs
Answer: Yes, any type that satisfies Shape. An interface slice can hold any mix of concrete types that satisfy the interface.
Which standard interface customises how fmt prints a type?
- fmt.Printer
- fmt.Stringer
- fmt.Formatter only
- io.Writer
Answer: fmt.Stringer. Implementing fmt.Stringer's String() string method customises how fmt prints the value.
What is the zero value of an interface variable?
- 0
- An empty struct
- nil
- false
Answer: nil. An interface that holds no value is nil.
Why are interfaces useful for decoupling code?
- They make code run faster
- Callers depend on behaviour, not concrete types
- They reduce memory
- They add inheritance
Answer: Callers depend on behaviour, not concrete types. Functions accept interfaces, so any type with the right behaviour works — decoupling caller from implementation.
Does Go have an explicit 'implements' keyword?
- Yes
- Only for structs
- No
- Only with generics
Answer: No. Go has no implements keyword; interface satisfaction is structural and implicit.
Continue this course
- Previous: Methods (value vs pointer receivers)
- Next: Error Handling — Go's explicit error return pattern and how to create custom error types