OOP#

Go has no classes, no inheritance, no constructors. Object orientation in Go is structs + methods + interfaces + embedding. Type satisfies interface structurally (no implements keyword); composition replaces inheritance; constructors are plain functions, conventionally named New / NewXxx.

For the underlying function mechanics, see Functions. For struct fields and other composite types, see Types.

Methods#

A method is a function with a receiver. The receiver appears in parentheses before the function name.

type Counter struct{ n int }

func (c Counter) Get() int { return c.n }       // value receiver
func (c *Counter) Inc()    { c.n++ }            // pointer receiver

var c Counter
fmt.Println(c.Get())                // 0
c.Inc()
fmt.Println(c.Get())                // 1

Value vs pointer receiver.

  • Pointer receiver lets the method mutate the value and is cheaper for large structs.

  • Value receiver is safe to call on any value; the operator uses it for tiny structs that mirror primitives.

Consistency matters: pick one and uses it for every method on a given type.

Constructors#

Go has no special constructor syntax. The convention is a package-level function New / NewXxx that returns the new value (often a pointer).

type Server struct {
    addr   string
    client *http.Client
}

func NewServer(addr string) *Server {
    return &Server{
        addr:   addr,
        client: &http.Client{Timeout: 5 * time.Second},
    }
}

s := NewServer("0.0.0.0:8080")

For variants and configuration, use functional options.

type Option func(*Server)

func WithTimeout(d time.Duration) Option {
    return func(s *Server) { s.client.Timeout = d }
}

func NewServer(addr string, opts ...Option) *Server {
    s := &Server{addr: addr, client: &http.Client{}}
    for _, opt := range opts { opt(s) }
    return s
}

s := NewServer("0.0.0.0:8080", WithTimeout(2*time.Second))

Interfaces#

An interface is a set of method signatures. A type satisfies the interface implicitly; the compiler checks structurally at the assignment site.

type Stringer interface {
    String() string
}

type User struct{ Name string }

func (u User) String() string { return "user:" + u.Name }

var s Stringer = User{Name: "rk"}    // satisfied automatically
fmt.Println(s)                       // user:rk

The “accept interfaces, return structs” convention. Function parameters typed as the smallest interface the function actually needs; return values typed as the concrete struct.

func summarize(r io.Reader) (int, error) {
    data, err := io.ReadAll(r)
    if err != nil { return 0, err }
    return len(data), nil
}

// r could be a *os.File, a *bytes.Buffer, an http.Response.Body, …

The empty interface any (alias for interface{}) accepts any value.

Type assertions#

x.(T) extracts the concrete type from an interface value. Returns (T, ok) in the two-value form.

var x any = "hello"

s := x.(string)         // panics if x is not a string
s, ok := x.(string)     // ok=true; safe form

switch v := x.(type) {
case string: fmt.Println("string", v)
case int:    fmt.Println("int", v)
default:     fmt.Println("other")
}

Embedding#

Go’s substitute for inheritance. An embedded field is declared with just a type (no name); the embedding struct promotes the embedded type’s fields and methods.

type Logger struct{ prefix string }
func (l Logger) Log(msg string) { fmt.Println(l.prefix, msg) }

type Server struct {
    Logger              // embedded
    addr string
}

s := Server{Logger: Logger{prefix: "[svc]"}, addr: ":8080"}
s.Log("starting")       // promoted method; calls Logger.Log via s.Logger

Embedding composes; it does not inherit. The compiler does not look up “is-a” relationships; it copies the method set up from the embedded type to the outer type.

Embedding an interface inside another interface means “include those method signatures”.

type ReadWriter interface {
    io.Reader
    io.Writer
}

Interface composition#

Composition by embedding is how the stdlib builds its layered interfaces (io.Reader, io.ReadWriter, io.ReadWriteCloser).

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

type ReadCloser interface {
    Reader
    Closer
}

Interface satisfaction at compile time#

The operator asserts a type satisfies an interface at compile time with a blank-identifier declaration. Useful when the satisfaction is part of the package contract and would otherwise only surface at distant call sites.

var _ io.Writer = (*Server)(nil)        // *Server must satisfy io.Writer

If *Server later loses a method, this fails at compile time.

Generic methods#

Methods cannot have their own type parameters (an open spec issue); the type parameters live on the receiver type.

type Stack[T any] struct{ items []T }

func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }
func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    n := len(s.items) - 1
    v := s.items[n]
    s.items = s.items[:n]
    return v, true
}

References#