Right, let’s talk about interface types. Forget the intimidating jargon for a second. An interface is, at its heart, a contract. It’s a named set of method signatures—a promise that a certain type will have these specific methods with these specific inputs and outputs. It doesn’t care about the state (the struct fields), it doesn’t care about the implementation details (how you get the job done), it only cares about behavior (what you can do).

Think of it like a universal remote. You don’t need to know if your TV is an OLED or a ancient plasma; if it has a Power() method that takes no arguments and returns an error, the universal remote (the interface) can talk to it. This is the magic of decoupling. You write code that operates on the capability, not the concrete type.

Here’s the simplest form of one. We’ll define the contract for something that can Speak.

// Speaker is the contract for something that can make a sound.
type Speaker interface {
    Speak() string
}

That’s it. We’ve defined an interface type called Speaker. Any type—and I mean any type, a struct, a string, an int, you name it—that has a method Speak() string automatically, implicitly satisfies the Speaker interface. There’s no implements keyword. You don’t ask for permission. If you have the method, you fulfill the contract. Go’s interfaces are duck-typed at compile time: “If it quacks like a Speaker, it is a Speaker.”

Let’s create a couple of concrete types that satisfy this.

// Dog is a concrete type.
type Dog struct {
    Name string
}

// Speak is a method on the Dog type. This means Dog now satisfies the Speaker interface.
func (d Dog) Speak() string {
    return "Woof! My name is " + d.Name
}

// Robot is a completely unrelated concrete type.
type Robot struct {
    Model string
}

// Speak is a method on the Robot type. Robot *also* satisfies Speaker.
func (r Robot) Speak() string {
    return "Beep boop. I am model " + r.Model
}

// A function that accepts the INTERFACE, not a concrete type.
func Greet(s Speaker) {
    fmt.Println(s.Speak())
}

func main() {
    spot := Dog{Name: "Spot"}
    r2d2 := Robot{Model: "R2-D2"}

    Greet(spot)  // "Woof! My name is Spot"
    Greet(r2d2)  // "Beep boop. I am model R2-D2"
}

See what happened there? The Greet function doesn’t know or care about Dog or Robot. It only knows about the Speaker contract. This is incredibly powerful for writing flexible and testable code.

The Empty Interface: interface{} (and its modern alias, any)

This is where newcomers often panic. interface{} is an interface that has zero methods. Read that again. By the rules we just established, if an interface requires zero methods, then every type in existence satisfies it. It’s the ultimate universal remote, but one that can only press a button called “accept anything.”

// This function will literally accept any value Go can create.
func TakeTheWildCard(i interface{}) {
    fmt.Printf("I got a value of type: %T\n", i)
}

func main() {
    TakeTheWildCard(42)          // int
    TakeTheWildCard("hello")     // string
    TakeTheWildCard(spot)        // main.Dog
}

It’s not magic, it’s mayhem. You give up type safety. To do anything useful with the value inside, you need to use a type assertion or type switch to figure out what it actually is. Use any (introduced in Go 1.18) when you genuinely need a container for truly arbitrary data, like in JSON parsing, but treat it like a hazardous materials suit: necessary for a dirty job, but you don’t want to wear it all the time.

Composing Larger Interfaces

This is one of the most elegant features of Go’s interfaces. You can build larger contracts by embedding smaller ones. The standard library does this everywhere. The io.ReadWriter is the classic example.

// ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {
    Reader
    Writer
}

But what are Reader and Writer? They’re also interfaces!

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

So ReadWriter is an interface that requires all the methods of Reader and all the methods of Writer. It’s composition, and it’s beautiful. You can define your own interfaces by mixing and matching existing ones. If your new SuperProcessor type has Read, Write, and a new Process() method, it automatically satisfies Reader, Writer, ReadWriter, and your own new Processor interface, all without you explicitly stating it.

The Nil Receiver Pitfall

Here’s a classic “foot gun” that gets everyone. An interface value is two things: a concrete type and a concrete value. Both must be nil for the interface itself to be completely nil.

type BadDog struct {
    Name string
}

// This method has a pointer receiver: *BadDog
func (d *BadDog) Speak() string {
    if d == nil {
        return "<nil>"
    }
    return "Woof! I'm " + d.Name
}

func main() {
    var concreteDog *BadDog = nil // a nil pointer to a BadDog
    var speaker Speaker = concreteDog // assigning the nil pointer to an interface

    fmt.Println(speaker == nil) // false - Wait, what?!
    fmt.Println(speaker.Speak()) // "<nil>" - This actually works.
}

Why is speaker not nil? Because the interface value now holds a concrete type (*BadDog) and a value (nil). The interface itself is not nil; it knows exactly what type it’s holding, even if the value of that type is nil. This is a common source of bugs when checking for nil inside functions that accept interfaces. You must check the inside of the interface. The Speak method works because we handled the nil receiver inside it—a best practice for methods with pointer receivers.

The takeaway? Always be mindful of whether you’re dealing with a nil concrete value assigned to an interface. An interface value is only truly nil if both its type and value components are nil.