golang · level 5

Structs & Methods

Composition over inheritance — the Go way.

125 XP

Structs & Methods

Go has no classes. The combination of struct + method is the closest equivalent — and it composes more cleanly than inheritance ever did.

Defining a struct

type User struct {
    Name  string
    Email string
    Age   int
}

Field names follow the same export rule as identifiers: Name is visible from other packages, name would not be.

Creating instances

// Positional — fragile, never use except for tiny tuples.
u1 := User{"Alice", "alice@x.com", 30}

// Named fields — preferred. Order doesn't matter; missing fields
// take their zero value.
u2 := User{Name: "Alice", Email: "alice@x.com"}
u2.Age                          // 0 — zero value

// Just zero-value everything.
var u3 User                     // {Name: "", Email: "", Age: 0}

// Pointer to a new value.
u4 := &User{Name: "Bob"}

new(User) and &User{} both produce a *User pointing at a zero-valued struct. The &User{...} form is more common because you can set initial fields.

Methods

A method is a function with a receiver declared between func and the name:

func (u User) DisplayName() string {
    return strings.Title(u.Name)
}

alice := User{Name: "alice"}
alice.DisplayName()             // "Alice"

This is a value receiveru is a copy. Mutations don't affect the caller's struct.

For mutating methods (and large structs), use a pointer receiver:

func (u *User) SetEmail(email string) {
    u.Email = email
}

alice := &User{Name: "Alice"}
alice.SetEmail("a@x.com")       // mutates the original

Go automatically takes the address when you call a pointer-receiver method on an addressable value:

alice := User{Name: "Alice"}    // value, not pointer
alice.SetEmail("a@x.com")       // Go calls (&alice).SetEmail(...)

Pointer or value?

Three rules:

  1. The method needs to mutate the receiver? Pointer receiver.
  2. The receiver is large (more than a few words)? Pointer receiver to skip the copy.
  3. All other methods on the same type use a pointer receiver? Match them for consistency.

Otherwise value receivers are fine. They're cheaper for small types, and they make the type usable as a map key.

Embedding (Go's "inheritance")

Embed a type to "promote" its fields and methods:

type Timestamped struct {
    CreatedAt time.Time
    UpdatedAt time.Time
}

type User struct {
    Timestamped       // embedded — no field name
    Name string
    Email string
}

u := User{
    Timestamped: Timestamped{CreatedAt: time.Now()},
    Name:        "Alice",
}

u.CreatedAt           // works — promoted from Timestamped
u.Timestamped.CreatedAt  // also works — explicit access

Embedding gives you composition with sugar, not classical inheritance. There's no super(); if User defines its own String() method it simply hides the embedded one. To call the embedded method explicitly: u.Timestamped.String().

Constructors

Go has no __init__ / constructor. The convention is a New<Type> function:

func NewUser(name, email string) (*User, error) {
    if !strings.Contains(email, "@") {
        return nil, errors.New("invalid email")
    }
    return &User{
        Timestamped: Timestamped{CreatedAt: time.Now()},
        Name:        name,
        Email:       email,
    }, nil
}

Returning (value, error) lets the constructor reject bad inputs without panicking.

Tags

Struct fields can carry "tags" — string metadata that libraries (json, db, validate) read at runtime:

type User struct {
    Name  string `json:"name"           db:"user_name"`
    Email string `json:"email,omitempty" db:"email"`
    Age   int    `json:"age,omitempty"  db:"age"`
}

encoding/json reads the json: tag to know which JSON key to map to. ORM libraries do the same with their own tag prefix. The string format is unstructured — packages parse it however they like.

Comparing structs

Two structs are == if their fields are equal pairwise — but only if all fields are comparable types (no slices, maps, or functions inside):

type Point struct {
    X, Y int
}

Point{1, 2} == Point{1, 2}      // true

type Bag struct {
    Items []string
}

Bag{[]string{"x"}} == Bag{[]string{"x"}}   // compile error: slices not comparable

For deep comparisons, use reflect.DeepEqual (or, in tests, cmp.Diff).