Interfaces the Go way (implicit, small, composed)
If you come from Java or C#, Go interfaces feel like you are missing something. There is no implements keyword. No declaration that says "this struct satisfies that interface". A type just has the methods, and Go figures out the rest. This lesson is about how that simple decision changes your design.
Declaring an interface
An interface is a set of method signatures. Any type with all those methods satisfies it. No declaration required.
type Notifier interface {
Notify(msg string) error
}
type SMS struct{ phone string }
func (s SMS) Notify(msg string) error { /* send sms */ return nil }
type Email struct{ to string }
func (e Email) Notify(msg string) error { /* send email */ return nil }Both SMS and Email satisfy Notifier. Neither of them said so. The compiler checks the methods exist and is happy.
Senior rule: interfaces in Go belong to the consumer, not the producer. The package that needs
Notifierdeclares it. The packages that provideSMSand
This is the opposite of Java, where the producer declares "I implement Sendable" and every consumer has to import that interface. In Go, the consumer defines what shape it needs and any matching type slots in.
Keep them small, then compose
Idiomatic Go interfaces have one to three methods. The standard library is the proof.
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface { // composition by embedding
Reader
Writer
}io.Reader is one method. So is io.Writer, fmt.Stringer, error. The smaller the interface, the more types satisfy it, the more places you can use it. io.ReadWriter is literally Reader + Writer. Files satisfy it. Network connections satisfy it. Buffers satisfy it. Composition gives you a vocabulary, not a hierarchy.
Senior rule: the bigger the interface, the weaker the abstraction. One method is the dream. Three is fine. Ten is a code smell.
The empty interface and any
interface{} is an interface with zero methods. Every type satisfies it. Since Go 1.18, the alias any means the same thing and reads nicer.
func log(v any) {
fmt.Println(v)
}
log(42)
log("upi-ref")
log(Order{ID: "o1"})Useful for logging, generic-ish containers, and bridging to untyped JSON. Overuse turns Go into JavaScript. Reach for it sparingly.
Type assertions, type switches, and the nil trap
Once you have an interface value, you sometimes need the concrete type back. Use a type assertion with the comma-ok form. When the value could be one of several types, a type switch is cleaner than a chain of assertions.
var v any = "razorpay"
s, ok := v.(string) // comma-ok, no panic if wrong
switch x := v.(type) {
case string:
fmt.Println("string:", x)
case int:
fmt.Println("int:", x)
default:
fmt.Println("unknown")
}Without ok, a wrong assertion panics. Always use comma-ok unless you control both ends and a panic is the right behaviour.
The nil-interface trap is the one to memorise. An interface value has two parts, a type and a value, and it is nil only when both are nil. Returning a typed nil pointer as an error means the caller's if err != nil check passes when you did not want it to.
func DoWork() error {
var e *MyError // typed nil
return e // interface is NOT nil, err != nil is true
}Senior rule: when an error-typed return has no error, return the literal nil, never a nil pointer of some concrete error type. This same trap shows up with any interface return. Whenever you see a "but I checked, it was nil" bug on a Go project, this is the first thing to suspect.
The runtime cost of an interface call is one extra pointer indirection compared to a direct method call, which the compiler often inlines away anyway. So "should I use an interface here" is almost always an API design question, not a performance question. Reach for an interface when you need to swap implementations (tests, mocks, multiple backends) or when several unrelated types need the same shape of contract.
Quick reference
- Implicit satisfaction
- A type satisfies an interface just by having the right methods. No declaration needed.
- Small interface
- An interface with one to three methods. The Go standard library default.
- Interface composition
- Embedding interfaces inside other interfaces to build up larger contracts.
- any
- Alias for interface{}. Matches every type. Use sparingly.
- Type assertion
- v.(T) extracts the concrete type from an interface value. Use comma-ok form.
- Type switch
- switch x := v.(type) for dispatching on the concrete type behind an interface.
Next, errors. Where Go's "no exceptions" philosophy gets its full justification.
Watching quietly. Tap me if you want a tip.
Go cannot run natively in a browser. Run copies your code and opens go.dev/play ; paste and click Run there.
Try this (0 of 1 done)
- 1
Predict the output.
show answer
// already in the editor