On this page

Interfaces

14 min read TextCh. 2 — Functions and Structures

Interfaces in Go: Implicit Polymorphism

Go's interfaces are one of its most elegant and powerful features. Unlike Java or C#, in Go you do not declare that a type implements an interface. If a type has the methods an interface requires, it automatically satisfies it. This is known as static duck typing: if it walks like a duck and quacks like a duck, it is a duck.

This approach has profound consequences: the coupling between types and interfaces is minimal, code is more flexible, and you can define interfaces in the consumer (the code that uses them) rather than the provider (the code that defines them).

Defining an Interface

// An interface defines a set of methods
type Writer interface {
    Write(data []byte) (n int, err error)
}

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

// Interfaces can be composed
type ReadWriter interface {
    Reader
    Writer
}

Go's convention is to name single-method interfaces with the method name plus the -er suffix: Reader, Writer, Stringer, Closer, Handler.

Implicit Implementation

type Loggable interface {
    Log() string
}

type Server struct {
    Host string
    Port int
}

// Server implements Loggable automatically — no explicit declaration
func (s Server) Log() string {
    return fmt.Sprintf("Server at %s:%d", s.Host, s.Port)
}

type Database struct {
    URL string
}

func (d Database) Log() string {
    return fmt.Sprintf("Database at %s", d.URL)
}

// Function that accepts any Loggable
func record(l Loggable) {
    fmt.Println("[LOG]", l.Log())
}

func main() {
    s := Server{Host: "localhost", Port: 8080}
    db := Database{URL: "postgres://localhost:5432/mydb"}

    record(s)   // works without Server declaring "implements Loggable"
    record(db)  // same for Database
}

Interfaces as Types: Polymorphism

An interface variable can hold any concrete value that implements the interface:

type Animal interface {
    Sound() string
    Name() string
}

type Dog struct{ name string }
func (d Dog) Sound() string { return "Woof!" }
func (d Dog) Name() string  { return d.name }

type Cat struct{ name string }
func (c Cat) Sound() string { return "Meow!" }
func (c Cat) Name() string  { return c.name }

func makeSound(a Animal) {
    fmt.Printf("%s says: %s\n", a.Name(), a.Sound())
}

func main() {
    animals := []Animal{
        Dog{name: "Rex"},
        Cat{name: "Whiskers"},
        Dog{name: "Max"},
    }

    for _, a := range animals {
        makeSound(a)
    }
}

The Empty Interface: `any` (formerly `interface{}`)

The empty interface interface{} — now with the alias any since Go 1.18 — can hold a value of any type. It is the equivalent of Object in Java or object in C#:

// any can hold any type
func print(v any) {
    fmt.Printf("(%T) %v\n", v, v)
}

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

    // Slice of any
    mixed := []any{1, "two", 3.0, true}
    for _, v := range mixed {
        fmt.Println(v)
    }

    // Map with any values
    config := map[string]any{
        "host":  "localhost",
        "port":  8080,
        "debug": true,
    }
    fmt.Println(config["host"])
}

Use any sparingly. When you use it, you lose type safety. Prefer generics or specific interfaces when possible.

Type Assertions: Extracting the Concrete Type

When you have an interface value and need the concrete type, you use a type assertion:

var s Shape = Circle{Radius: 5}

// Safe form with "comma ok"
c, ok := s.(Circle)
if ok {
    fmt.Printf("It's a circle with radius %.1f\n", c.Radius)
} else {
    fmt.Println("Not a circle")
}

// Direct form (panics if the type is wrong)
c2 := s.(Circle)  // panics if s is not a Circle!
fmt.Println(c2.Radius)

// Always prefer the "comma ok" form

Type Switches: Branch by Type

The type switch is the idiomatic way to handle multiple possible types:

func process(v any) {
    switch x := v.(type) {
    case nil:
        fmt.Println("nil")
    case int:
        fmt.Printf("int: %d (double: %d)\n", x, x*2)
    case int64:
        fmt.Printf("int64: %d\n", x)
    case float64:
        fmt.Printf("float64: %.2f\n", x)
    case string:
        fmt.Printf("string: %q (length: %d)\n", x, len(x))
    case bool:
        if x {
            fmt.Println("true")
        } else {
            fmt.Println("false")
        }
    case []int:
        fmt.Printf("[]int with %d elements\n", len(x))
    case error:
        fmt.Printf("error: %v\n", x)
    default:
        fmt.Printf("unknown type: %T = %v\n", x, x)
    }
}

Important Interfaces from the Standard Library

`fmt.Stringer`

type Stringer interface {
    String() string
}

If your type implements String() string, fmt.Println, fmt.Printf, and similar functions will use that method automatically:

type Point struct{ X, Y int }

func (p Point) String() string {
    return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}

p := Point{3, 4}
fmt.Println(p)        // (3, 4) — uses String()
fmt.Printf("%v\n", p) // (3, 4)
fmt.Printf("%s\n", p) // (3, 4)

`error`

type error interface {
    Error() string
}

Any type with an Error() string method is an error in Go:

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("field %q: %s", e.Field, e.Message)
}

func validateEmail(email string) error {
    if !strings.Contains(email, "@") {
        return &ValidationError{Field: "email", Message: "invalid format"}
    }
    return nil
}

`io.Reader` and `io.Writer`

These two interfaces are the most widely used in the entire standard library:

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

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

Files, network connections, in-memory buffers, gzip compression, encryption — all implement these interfaces. This allows code to work with any source or destination:

// copy accepts any Reader and any Writer
func copy(dst io.Writer, src io.Reader) (int64, error) {
    return io.Copy(dst, src)
}

func main() {
    // copy a string to stdout
    copy(os.Stdout, strings.NewReader("Hello, interfaces!\n"))

    // copy a file to an in-memory buffer
    f, _ := os.Open("data.txt")
    defer f.Close()
    var buf bytes.Buffer
    copy(&buf, f)
}

Interfaces and nil

An interface variable has two components: type and value. An interface is nil only when both are nil:

var r io.Reader  // nil — type nil, value nil

var f *os.File         // nil pointer
var r2 io.Reader = f   // NOT nil! type=*os.File, value=nil

fmt.Println(r == nil)   // true
fmt.Println(r2 == nil)  // false — common gotcha!

This subtlety matters when returning errors:

// INCORRECT: returns a non-nil interface even though p is nil
func getError() error {
    var p *MyError = nil
    return p  // the interface will have type=*MyError, value=nil
}

// CORRECT: returns an explicit nil of interface type
func getError() error {
    return nil
}

With interfaces mastered, in the next lesson we will learn Go's fundamental collections: slices and maps — their differences, operations, and the "comma ok" idiom.

Go interfaces are implicit: no implements keyword
In Go, a type implements an interface automatically if it has all the required methods. There is no implements keyword as in Java or C#. This allows types from external packages to implement your interfaces without modifying them, and your types to implement standard library interfaces automatically.
Prefer small interfaces with one or two methods
Go's philosophy favors small interfaces. The io.Reader interface has only one method: Read(). The fmt.Stringer interface has only String(). Small interfaces are easier to implement, compose, and test. Follow the Interface Segregation Principle (ISP).