ToolPopToolPop
Go · Lesson 8 of 22

Interfaces the Go way (implicit, small, composed)

7 min readUpdated 25 Jun 2026

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.

go
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 Notifier declares it. The packages that provide SMS and Email know nothing about it.

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.

go
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.

go
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.

go
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.

go
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.

Diagram
rendering diagram...

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.

Chai0/1 done

Watching quietly. Tap me if you want a tip.

Go Playground

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. 1

    Predict the output.

    show answer
    // already in the editor