Right, so you’ve seen interfaces before. You declare one, you explicitly state that your new struct implements that interface, you pat yourself on the back for writing good, clean, object-oriented code. Go toss that implements keyword in the bin. We don’t do that here.

Go’s approach is different. It’s implicit. A type satisfies an interface simply by implementing the interface’s method set. No ceremony, no declaration of intent. If it has the methods, it is the interface. This is sometimes called “structural typing” or “duck typing” – if it quacks like a Duck, it’s a Duck, and we don’t need to see its birth certificate.

This isn’t just a syntactic quirk; it’s a philosophical shift. It means the author of a type doesn’t need to predict and declare every single interface it might ever satisfy. You can define a new interface tomorrow that describes the methods your existing type – written yesterday – already implements. Your type is now magically compatible with code that uses your new interface. This decoupling is one of Go’s most powerful features for creating flexible and scalable abstractions.

Let’s get our hands dirty. Here’s a classic starter example.

// Define an interface. Note: it's just a method set.
type Speaker interface {
    Speak() string
}

// Define a concrete type. Nothing special here.
type Dog struct {
    Name string
}

// Here's the magic: Dog now implements Speaker because it has a Speak method.
// No `implements` keyword needed.
func (d Dog) Speak() string {
    return fmt.Sprintf("Woof! I'm %s", d.Name)
}

// A function that accepts the interface.
func Greet(s Speaker) {
    fmt.Println(s.Speak())
}

func main() {
    d := Dog{Name: "Rex"}
    Greet(d) // This works perfectly. Dog is a Speaker.
}

See? The Dog type never once mentioned the Speaker interface. It just went about its business, implementing a method. The Greet function defines what it needs in terms of the interface, and our Dog fits the bill. The connection is made automatically by the compiler.

The Zero-Value Pitfall

Here’s where the “witty and direct” part meets “honest about rough edges.” This implicit magic is fantastic until you have to deal with a nil receiver. Consider this:

type Cat struct {
    Name string
}

// Method with a value receiver.
func (c Cat) Speak() string {
    return fmt.Sprintf("Meow. I am %s", c.Name)
}

// Method with a pointer receiver. This is more common for methods that need to modify the struct.
func (c *Cat) ChangeName(newName string) {
    c.Name = newName
}

func main() {
    var s Speaker

    // This is fine. c is a value, and it has the Speak method.
    c := Cat{Name: "Whiskers"}
    s = c
    fmt.Println(s.Speak()) // "Meow. I am Whiskers"

    // This is ALSO fine. The compiler automatically takes the address of c for us.
    s = &c
    fmt.Println(s.Speak()) // "Meow. I am Whiskers"

    // This is a PROBLEM.
    var nilCat *Cat // nilCat is a nil pointer to a Cat
    s = nilCat      // This compiles! *Cat has the Speak method too.

    // The following line will cause a panic: runtime error: invalid memory address or nil pointer dereference
    fmt.Println(s.Speak())
}

Why did that happen? Because the method set of the pointer type *Cat includes the methods of both *Cat and Cat (a value of type *Cat can obviously access the methods of the underlying Cat). So *Cat satisfies the Speaker interface. But when we call Speak() on a nil pointer, the method call is valid, but the method tries to access c.Name and boom – it dereferences a nil pointer.

The best practice here is ruthless: either check for nil inside every method that can be called on a pointer type, or don’t use pointer types to satisfy interfaces unless you’re absolutely sure the pointer will never be nil. This is a sharp edge the language gives you. You have to handle it yourself.

The Empty Interface: interface{} (a.k.a. any)

This is the logical conclusion of implicit interfaces. If an interface is satisfied by having methods, what satisfies an interface that has no methods? That’s right, everything.

// This function accepts absolutely anything.
func TakeAnything(v interface{}) {
    fmt.Printf("I got a value of type: %T\n", v)
}

func main() {
    TakeAnything(42)          // int
    TakeAnything("hello")     // string
    TakeAnything([]float64{}) // []float64
}

The empty interface interface{} is Go’s way of saying “I don’t care what this is.” It’s a escape hatch, a way to deal with unknown or dynamic types. Under the hood, it’s a two-word structure holding the value’s type and the value itself (or a pointer to it). You’ll see it used extensively in places like fmt.Print or JSON unmarshaling.

Just remember: to get your actual data back out, you’ll need a type assertion or type switch. It’s not magic, it’s just a very permissive container.

func WasItAString(v interface{}) {
    if str, ok := v.(string); ok {
        fmt.Println("It was a string:", str)
    } else {
        fmt.Println("Nope, not a string.")
    }
}

The implicit interface is the backbone of Go’s polymorphism. It lets you write incredibly flexible and decoupled code without inheriting a mess of brittle type hierarchies. It demands a bit more vigilance from you, the programmer, especially around nil values, but in return, it gives you a simplicity and power that feels almost like a superpower once you get used to it.