The Secret Life of Go: Interfaces

Published: (December 2, 2025 at 12:46 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

The Power of Implicit Contracts

Tuesday morning brought fog. Ethan descended to the archive carrying coffee and a small box of biscotti.
Eleanor looked up. “Italian today?”
“The baker said biscotti are designed to fit perfectly into coffee cups—form following function.”
She smiled. “Perfect. Today we’re talking about interfaces—Go’s way of defining what something can do, regardless of what it is.”

A Simple Example

package main

import "fmt"

type Dog struct {
    Name string
}

func (d Dog) Speak() string {
    return "Woof!"
}

type Cat struct {
    Name string
}

func (c Cat) Speak() string {
    return "Meow!"
}

func main() {
    dog := Dog{Name: "Buddy"}
    cat := Cat{Name: "Whiskers"}

    fmt.Println(dog.Name, "says:", dog.Speak())
    fmt.Println(cat.Name, "says:", cat.Speak())
}

Output

Buddy says: Woof!
Whiskers says: Meow!

Two different types—Dog and Cat. Both have a Speak() method that returns a string, yet they are unrelated types.

Defining an Interface

package main

import "fmt"

type Speaker interface {
    Speak() string
}

type Dog struct {
    Name string
}

func (d Dog) Speak() string {
    return "Woof!"
}

type Cat struct {
    Name string
}

func (c Cat) Speak() string {
    return "Meow!"
}

func MakeItSpeak(s Speaker) {
    fmt.Println(s.Speak())
}

func main() {
    dog := Dog{Name: "Buddy"}
    cat := Cat{Name: "Whiskers"}

    MakeItSpeak(dog)
    MakeItSpeak(cat)
}

Output

Woof!
Meow!

Speaker defines a contract: any type that has a Speak() string method satisfies the interface. The function MakeItSpeak accepts any Speaker, and both Dog and Cat qualify.

Note: In Go, you never explicitly declare that a type implements an interface. If the methods match, the implementation is implicit.

Why Implicit?

  • Flexibility: You can define an interface that existing types already satisfy, even if those types were written before your interface existed.
  • No upfront coupling: Types aren’t forced to declare relationships, keeping code loosely coupled.

Real‑World Example: Writing to Different Destinations

package main

import (
    "fmt"
    "os"
)

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

type ConsoleWriter struct{}

func (cw ConsoleWriter) Write(data []byte) (int, error) {
    n, err := fmt.Println(string(data))
    return n, err
}

func WriteMessage(w Writer, message string) {
    w.Write([]byte(message))
}

func main() {
    console := ConsoleWriter{}
    WriteMessage(console, "Hello, Console!")

    file, _ := os.Create("output.txt")
    defer file.Close()
    WriteMessage(file, "Hello, File!")
}

The Writer interface has a single method Write([]byte) (int, error).

  • ConsoleWriter implements it.
  • os.File from the standard library also implements it, even though it was written long before our interface existed.

Thus, WriteMessage works with any type that satisfies Writer—console, file, network connection, in‑memory buffer, etc.

Polymorphism with Interfaces

package main

import "fmt"

type Shape interface {
    Area() float64
    Perimeter() float64
}

type Rectangle struct {
    Width  float64
    Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return 3.14159 * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * 3.14159 * c.Radius
}

func PrintShapeInfo(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}

func main() {
    rect := Rectangle{Width: 5, Height: 3}
    circle := Circle{Radius: 4}

    PrintShapeInfo(rect)
    PrintShapeInfo(circle)
}

Output

Area: 15.00, Perimeter: 16.00
Area: 50.27, Perimeter: 25.13

Shape requires Area() and Perimeter(). Both Rectangle and Circle satisfy it, so PrintShapeInfo can operate on any Shape without caring about the concrete type.

This is similar to duck typing, but Go checks the implementation at compile time, providing type safety.

The Empty Interface (any)

package main

import "fmt"

func PrintAnything(v any) {
    fmt.Println(v)
}

func main() {
    PrintAnything(42)
    PrintAnything("hello")
    PrintAnything(true)
    PrintAnything([]int{1, 2, 3})
}

Output

42
hello
true
[1 2 3]

any is an alias for the empty interface interface{}. Since every type has at least zero methods, every type satisfies it. Use it sparingly, as it removes compile‑time type safety.

Type Assertions

package main

import "fmt"

func Describe(v any) {
    // Type assertion with comma‑ok
    if str, ok := v.(string); ok {
        fmt.Printf("String: %s (length %d)\n", str, len(str))
        return
    }

    if num, ok := v.(int); ok {
        fmt.Printf("Integer: %d (doubled: %d)\n", num, num*2)
        return
    }

    fmt.Printf("Unknown type: %T\n", v)
}

func main() {
    Describe("hello")
    Describe(42)
    Describe(true)
}

Output

String: hello (length 5)
Integer: 42 (doubled: 84)
Unknown type: bool

If you omit the ok check (str := v.(string)), the program panics when the assertion fails.

Type Switches

package main

import "fmt"

func Describe(v any) {
    switch val := v.(type) {
    case string:
        fmt.Printf("String: %s (length %d)\n", val, len(val))
    case int:
        fmt.Printf("Integer: %d (doubled: %d)\n", val, val*2)
    case bool:
        fmt.Printf("Boolean: %t\n", val)
    default:
        fmt.Printf("Unknown type: %T\n", val)
    }
}

func main() {
    Describe("hello")
    Describe(42)
    Describe(true)
    Describe(3.14)
}

Output

String: hello (length 5)
Integer: 42 (doubled: 84)
Boolean: true
Unknown type: float64

The switch val := v.(type) construct cleanly handles multiple possible concrete types.

Interface Hierarchy (From Specific to General)

  • Specific Interface

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

    Only types that implement Read([]byte) (int, error) satisfy Reader.

  • Empty Interface (any)

    type any interface{}

    Every type satisfies this, making it the most general interface. Use it only when you truly need to accept values of any type.

Back to Blog

Related posts

Read more »