Polyglot
Languages Go interfaces
Go § interfaces

Methods and interfaces

Go does not have classes; it has types with methods and interfaces. Methods are functions with a receiver; the receiver is the type the method is attached to. Interfaces specify a set of method signatures; a type implements an interface implicitly by having the required methods (no implements keyword). The mechanism is structural — any type with the right method set satisfies an interface, regardless of declaration. The combination — methods on any type, structural interfaces, the empty interface for runtime polymorphism, embedding for composition — is the substance of Go’s polymorphism story.

Methods

A method is a function with a receiver:

type Counter struct {
    n int
}

func (c *Counter) Increment() {
    c.n++
}

func (c Counter) Get() int {
    return c.n
}

c := Counter{}
c.Increment()                                    // *Counter method called on Counter value
c.Increment()
fmt.Println(c.Get())                             // 2

The receiver appears between func and the method name. The two forms:

  • Value receiver (c Counter) — receives a copy of the value.
  • Pointer receiver (c *Counter) — receives a pointer; admits modification.

Methods may be defined on any type defined in the same package — including non-struct types:

type Celsius float64

func (c Celsius) Fahrenheit() float64 {
    return float64(c)*9/5 + 32
}

t := Celsius(20)
fmt.Println(t.Fahrenheit())                      // 68

A method cannot be defined on a type from another package; the conventional defence is to declare a new type:

// type StringSlice []string                      // OK; new type
// type Person struct{ ... }                      // OK

Value vs pointer receivers

The conventional discipline:

  • Use a pointer receiver if the method modifies the receiver.
  • Use a pointer receiver for large structs (avoid the copy).
  • Use a value receiver for small, immutable values.
  • Be consistent within a type — all methods on one type conventionally use the same form.
type Account struct {
    Balance int
}

// Pointer receiver — modifies the account:
func (a *Account) Deposit(amount int) {
    a.Balance += amount
}

// Pointer receiver for consistency:
func (a *Account) Get() int {
    return a.Balance
}

The method set

A type’s method set is the set of methods callable on values of that type. The rules:

  • For a value type T: the method set contains all methods with receiver T.
  • For a pointer type *T: the method set contains all methods with receiver T or *T.
type Counter struct{ n int }

func (c Counter) Get() int { return c.n }
func (c *Counter) Increment() { c.n++ }

var c Counter
c.Get()                                          // value receiver — OK
c.Increment()                                    // pointer receiver — Go automatically takes &c

var p *Counter = &c
p.Get()                                          // OK; *Counter has Get
p.Increment()                                    // OK

The compiler automatically takes the address (&c) for pointer-receiver method calls on addressable values. For non-addressable values (e.g., map elements), the call fails:

m := map[string]Counter{}
// m["key"].Increment()                          // ERROR: cannot call pointer method

The conventional defence is to use *Counter in the map:

m := map[string]*Counter{
    "key": &Counter{},
}
m["key"].Increment()                              // OK

Interfaces

An interface specifies a set of method signatures:

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

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

type Closer interface {
    Close() error
}

A type implements an interface implicitly by having the required methods:

type File struct{ /* ... */ }

func (f *File) Read(p []byte) (int, error) { /* ... */ }
func (f *File) Write(p []byte) (int, error) { /* ... */ }
func (f *File) Close() error { /* ... */ }

var r Reader = &File{}                           // *File implements Reader
var w Writer = &File{}                           // *File implements Writer

There is no implements keyword; the language infers satisfaction from the method set. The mechanism is one of Go’s most distinctive features.

Interface composition

Interfaces may embed other interfaces:

type ReadCloser interface {
    Reader
    Closer
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

The composed interface requires all the embedded methods. The mechanism admits substantial reuse and modular interface design.

The empty interface

The interface interface{} (or any since Go 1.18) requires no methods — every type satisfies it:

var x interface{} = 42
var y interface{} = "hello"
var z interface{} = []int{1, 2, 3}

var a any = 3.14                                 // any is an alias for interface{}

The empty interface admits “any value” — useful for generic containers, JSON values, and APIs that handle heterogeneous data.

To extract the concrete type from an empty interface, use a type assertion or type switch:

var i interface{} = "hello"

s := i.(string)                                  // type assertion
fmt.Println(s)                                   // "hello"

s, ok := i.(string)                              // two-value form
if ok {
    fmt.Println(s)
}

switch v := i.(type) {                           // type switch
case string:
    fmt.Println("string:", v)
case int:
    fmt.Println("int:", v)
}

Treated in Switch and pattern dispatch.

Type assertions

A type assertion extracts a concrete type from an interface:

var w io.Writer = os.Stdout

f, ok := w.(*os.File)                            // assert to *os.File
if ok {
    fmt.Println("got file:", f.Name())
}

// Single-value form panics on failure:
f := w.(*os.File)                                // panics if w is not *os.File

The two-value form is the conventional defence; the single-value form is for cases where failure indicates a programmer error.

The Stringer interface

The fmt.Stringer interface admits custom string representation:

type Stringer interface {
    String() string
}

type Point struct{ X, Y int }

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

p := Point{1, 2}
fmt.Println(p)                                   // "(1, 2)"  — uses String()

Implementing Stringer admits substantial integration with the fmt package; types that implement it produce custom output for %v and similar verbs.

The error interface

The built-in error is an interface:

type error interface {
    Error() string
}

Any type with an Error() string method satisfies error:

type MyError struct {
    Code int
}

func (e *MyError) Error() string {
    return fmt.Sprintf("error code %d", e.Code)
}

var err error = &MyError{Code: 42}
fmt.Println(err)                                 // "error code 42"

Treated in Error handling.

Embedding

Embedding admits composition — including a type’s fields and methods in another struct:

type Animal struct {
    Name string
}

func (a Animal) Describe() string {
    return fmt.Sprintf("Animal named %s", a.Name)
}

type Dog struct {
    Animal                                        // embedded; promotes Name and Describe
    Breed string
}

d := Dog{
    Animal: Animal{Name: "Rex"},
    Breed:  "Labrador",
}

fmt.Println(d.Name)                              // "Rex" (promoted)
fmt.Println(d.Describe())                        // "Animal named Rex" (promoted method)
fmt.Println(d.Animal.Name)                       // also accessible

The embedded type is accessed by its type name (d.Animal); promoted fields and methods admit “as-if-inheritance” syntax.

Embedding is not inheritance — there is no virtual dispatch, no overriding (one-can shadow), and the embedded type is just a field with its name promoted. The mechanism admits substantial reuse without the complexity of class hierarchies.

Embedding interfaces

A struct can embed an interface:

type Server struct {
    io.ReadWriter                                // any type satisfying ReadWriter
    Name string
}

s := Server{
    ReadWriter: &bytes.Buffer{},
    Name:       "myserver",
}

s.Read(buf)                                       // calls Buffer's Read

The pattern admits delegation — the struct delegates method calls to the embedded value.

Common patterns

Standard library interfaces

The conventional standard-library interfaces:

io.Reader                                         // Read
io.Writer                                         // Write
io.Closer                                         // Close
io.ReadWriter                                     // Read + Write
io.ReadWriteCloser                                // Read + Write + Close
fmt.Stringer                                      // String() string
sort.Interface                                    // Len, Less, Swap
error                                             // Error() string

The interfaces compose: a type with Read, Write, and Close methods is automatically an io.ReadWriteCloser.

Implementing Stringer

type Status int

const (
    StatusActive Status = iota
    StatusInactive
    StatusBanned
)

func (s Status) String() string {
    switch s {
    case StatusActive:
        return "Active"
    case StatusInactive:
        return "Inactive"
    case StatusBanned:
        return "Banned"
    default:
        return fmt.Sprintf("Status(%d)", int(s))
    }
}

fmt.Println(StatusActive)                         // "Active"

Polymorphic dispatch

type Shape interface {
    Area() float64
}

type Circle struct{ R float64 }
type Square struct{ Side float64 }

func (c Circle) Area() float64 { return math.Pi * c.R * c.R }
func (s Square) Area() float64 { return s.Side * s.Side }

shapes := []Shape{
    Circle{R: 5},
    Square{Side: 4},
}

for _, s := range shapes {
    fmt.Println(s.Area())
}

The pattern is conventional for heterogeneous collections.

Mock implementations for testing

type Database interface {
    Get(key string) (string, error)
    Set(key, value string) error
}

type MockDB struct {
    data map[string]string
}

func (m *MockDB) Get(key string) (string, error) {
    v, ok := m.data[key]
    if !ok { return "", fmt.Errorf("not found") }
    return v, nil
}

func (m *MockDB) Set(key, value string) error {
    m.data[key] = value
    return nil
}

// In tests:
db := &MockDB{data: map[string]string{"a": "1"}}
service := NewService(db)

The pattern is conventional for testing — interfaces admit substituting a mock for the real implementation.

Embedding for default methods

type defaultLogger struct{}
func (defaultLogger) Debug(s string) { /* default no-op */ }
func (defaultLogger) Info(s string)  { fmt.Println(s) }
func (defaultLogger) Error(s string) { fmt.Fprintln(os.Stderr, s) }

type ServiceA struct {
    defaultLogger                                 // gets all the default methods
}

// Override only what's needed:
func (s *ServiceA) Info(message string) {
    fmt.Printf("[ServiceA] %s\n", message)
}

The pattern admits “default implementations” through embedding.

Type assertion for optional interface

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

type Closer interface {
    Close() error
}

func processAndClose(r Reader) {
    /* ... use r.Read ... */
    if c, ok := r.(Closer); ok {
        c.Close()
    }
}

The pattern admits checking for an optional capability.

Interface as parameter type

func writeAll(w io.Writer, lines []string) error {
    for _, line := range lines {
        if _, err := fmt.Fprintln(w, line); err != nil {
            return err
        }
    }
    return nil
}

writeAll(os.Stdout, lines)
writeAll(file, lines)
writeAll(&bytes.Buffer{}, lines)

The conventional Go discipline is to take interface parameters and return concrete types — admit any caller, return a specific implementation.

Adapter pattern

type HandlerFunc func(http.ResponseWriter, *http.Request)

func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    f(w, r)
}

// Now any function with the right signature can be an http.Handler:
http.Handle("/", HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    /* ... */
}))

The pattern is conventional in net/http and similar APIs.

A note on the absence of inheritance

Go does not have class inheritance. The conventional substitutes:

  • Embedding for code reuse and method promotion.
  • Interfaces for polymorphism and abstraction.
  • Composition — including types as fields.
// Java/C#:
// class Dog extends Animal { ... }

// Go:
type Dog struct {
    Animal                                        // composition + promotion
    Breed string
}

The mechanism admits substantial reuse without the complexities of virtual dispatch, abstract classes, and the diamond-inheritance problem.

A note on the conventional discipline

The contemporary Go interface advice:

  • Define interfaces where they are used — at the consumer side, not the producer side.
  • Keep interfaces small — the convention is “single-method interfaces” (io.Reader, io.Writer).
  • Compose interfaces via embedding rather than producing larger interfaces.
  • Accept interfaces, return concrete types — the conventional Go API design.
  • Be consistent with receivers — pointer or value, not both.
  • Use type assertions or type switches for optional capabilities.
  • Use embedding for shared behaviour — composition, not inheritance.
  • Implement Stringer on types meant for human display.
  • Implement error on types meant to be used as errors.

The combination — methods on any type, structural interfaces, embedding for composition, the empty interface for any value, type assertions and switches for runtime dispatch — is the substance of Go’s polymorphism. The discipline produces flexible, decoupled code without inheritance hierarchies.