An interface is a set of method signatures. Any type that has all those methods automatically satisfies the interface — no declarations, no implements keyword, nothing. This concept is small but it’s the engine that makes Go’s standard library and frameworks so flexible.

If you’ve used Java or C#, this will feel different. In those languages a class must explicitly declare it implements an interface. In Go, interface satisfaction is implicit. You don’t have to know which interfaces exist when you write a type.

The simplest interface

Let’s start small.

package main

import "fmt"

type Greeter interface {
    Greet() string
}

type Friend struct {
    Name string
}

func (f Friend) Greet() string {
    return "Hey " + f.Name + "!"
}

type Doctor struct {
    Name string
}

func (d Doctor) Greet() string {
    return "Good morning, Dr. " + d.Name + "."
}

func sayHello(g Greeter) {
    fmt.Println(g.Greet())
}

func main() {
    f := Friend{Name: "Mani"}
    d := Doctor{Name: "Patel"}

    sayHello(f)
    sayHello(d)
}
Hey Mani!
Good morning, Dr. Patel.

What’s happening:

  1. Greeter is an interface. It says: “any type with a method named Greet that returns a string is a Greeter.”
  2. Friend and Doctor are unrelated structs. Both have Greet() methods, so both satisfy Greeterautomatically. We didn’t declare it.
  3. sayHello(g Greeter) accepts anything that implements Greeter. It doesn’t know or care whether you pass a Friend, Doctor, or anything else with the right method.

Compare this to Java, where Friend would have to write class Friend implements Greeter. Go’s approach lets you write a type today, and have it satisfy an interface someone else defines tomorrow.

A more realistic example

Interfaces shine when one function needs to work with many different types. Imagine calculating areas of different shapes:

package main

import "fmt"

type Shape interface {
    Area() float64
}

type Rectangle struct {
    Width, Height float64
}

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

type Circle struct {
    Radius float64
}

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

type Triangle struct {
    Base, Height float64
}

func (t Triangle) Area() float64 {
    return 0.5 * t.Base * t.Height
}

func totalArea(shapes []Shape) float64 {
    total := 0.0
    for _, s := range shapes {
        total += s.Area()
    }
    return total
}

func main() {
    shapes := []Shape{
        Rectangle{Width: 4, Height: 5},
        Circle{Radius: 3},
        Triangle{Base: 6, Height: 4},
    }

    fmt.Printf("Total area: %.2f\n", totalArea(shapes))
}
Total area: 60.27

The totalArea function doesn’t know about rectangles, circles, or triangles. It only knows about Shape. Add a Pentagon next month? Just give it an Area() method and totalArea works for it instantly. That’s the power of interfaces.

Standard library interfaces

Go’s standard library is built around small interfaces that give you huge leverage. Two you’ll meet constantly:

fmt.Stringer

Anything with a String() string method satisfies fmt.Stringer. When you pass it to fmt.Println, Go calls that method to format it nicely:

package main

import "fmt"

type User struct {
    Name string
    Age  int
}

func (u User) String() string {
    return fmt.Sprintf("%s (age %d)", u.Name, u.Age)
}

func main() {
    u := User{Name: "Mani", Age: 30}
    fmt.Println(u)
}
Mani (age 30)

Without the String() method, Println would have printed {Mani 30}. By implementing one method, User got pretty-printing for free.

error

The error type is itself an interface — its definition is just:

type error interface {
    Error() string
}

Any type with an Error() method that returns a string is an error. We’ll see this in action when we cover error handling.

The empty interface

An interface with no methods is satisfied by every type. It’s written interface{}, or in modern Go, the alias any:

package main

import "fmt"

func describe(x any) {
    fmt.Printf("Value: %v, Type: %T\n", x, x)
}

func main() {
    describe(42)
    describe("hello")
    describe(true)
    describe([]int{1, 2, 3})
}
Value: 42, Type: int
Value: hello, Type: string
Value: true, Type: bool
Value: [1 2 3], Type: []int

any is useful for things like fmt.Println (which has to accept anything). But avoid it in your own code unless you really need it — you lose type safety, and Go can’t help you catch mistakes at compile time.

Type assertions

When a function takes an any, sometimes you need to recover the original type. The type assertion does that:

// inside main()
var x any = "hello"

s := x.(string)
fmt.Println(s, len(s))
hello 5

If the type doesn’t match, the program crashes. The safer “comma ok” form lets you handle it gracefully:

// inside main()
var x any = 42

if s, ok := x.(string); ok {
    fmt.Println("It's a string:", s)
} else {
    fmt.Println("Not a string")
}
Not a string

Type switches

When you might have several types, a type switch is cleaner than chained assertions:

package main

import "fmt"

func describe(x any) {
    switch v := x.(type) {
    case int:
        fmt.Println("Integer:", v*2)
    case string:
        fmt.Println("String of length", len(v))
    case bool:
        fmt.Println("Boolean:", !v)
    default:
        fmt.Println("Unknown type")
    }
}

func main() {
    describe(10)
    describe("hello")
    describe(true)
    describe(3.14)
}
Integer: 20
String of length 5
Boolean: false
Unknown type

x.(type) is special syntax that’s only valid inside switch. In each case, v has the type of that case.

Interface design wisdom

A famous Go proverb: “The bigger the interface, the weaker the abstraction.”

Most useful Go interfaces have one method, sometimes two. Examples from the standard library:

  • io.Reader — one method: Read(p []byte) (n int, err error)
  • io.Writer — one method: Write(p []byte) (n int, err error)
  • fmt.Stringer — one method: String() string
  • error — one method: Error() string

Small interfaces are easy to satisfy, easy to mock, easy to test, and compose well. When you start writing your own interfaces, lean small.

Another Go proverb: “Accept interfaces, return structs.” Functions should accept interface types (so they’re flexible about what they’re given), but return concrete struct types (so callers know exactly what they got).

What’s next

That wraps up the core of the language. You can store data, control flow, organize code with functions and methods, and write flexible code with interfaces. The next section covers two essentials of every real program: error handling and packages.

Toggle theme (T)