Foundation ⏱ 7 min read

02 · Core Language

Structs, interfaces, pointers, slices, maps, defer

Node.js background? Structs are like plain objects with a fixed shape. Interfaces are like TypeScript interfaces but satisfied implicitly — no implements keyword. Pointers are new but simple.

Structs

A struct is a named set of fields. Think of it as a typed object shape.

type Coupon struct {
  ID     string
  Name   string
  Status string
}

c := Coupon{ID: "abc", Name: "10% Off", Status: "active"}
fmt.Println(c.Name) // "10% Off"

Methods attach behavior to a struct using a receiver:

func (c Coupon) IsActive() bool {
  return c.Status == "active"
}

// Value receiver (c is a copy) — use for read-only methods
// Pointer receiver (*Coupon) — use when the method modifies the struct

Interfaces

An interface defines a set of methods. Any type that has those methods satisfies the interface — no declaration needed.

type Stringer interface {
  String() string
}

type Coupon struct { Name string }

func (c Coupon) String() string { return "Coupon: " + c.Name }

// Coupon satisfies Stringer automatically — no "implements" keyword
var s Stringer = Coupon{Name: "10% Off"}
fmt.Println(s.String())
Node.js analogy: Like TypeScript structural typing — if an object has the required methods, it matches the interface. Duck typing, but checked at compile time.

Pointers

Go passes values by copy. To modify the original, pass a pointer.

x := 10
p := &x  // p is a *int — pointer to x
*p = 20   // dereference and assign
fmt.Println(x) // 20

Use pointer receivers when a method needs to modify the struct or when the struct is large (avoids copying):

func (c *Coupon) Deactivate() {
  c.Status = "inactive"  // modifies the original
}
Rule of thumb: If in doubt, use a pointer receiver. Mixing value and pointer receivers on the same type causes problems.

Slices

Slices are dynamic arrays. The zero value is nil, which behaves like an empty slice for append and range.

var names []string            // nil slice — safe to use
names = append(names, "Alice")
names = append(names, "Bob")

for i, name := range names {
  fmt.Println(i, name)
}

Maps

Maps are key-value stores with string, int, or any comparable type as the key. The zero value is nil — safe to read (returns zero value), but writing to a nil map panics.

scores := map[string]int{
  "Alice": 95,
  "Bob":   87,
}

// Read — always check if key exists
score, ok := scores["Alice"]
if ok {
  fmt.Println(score)
}

// Delete
delete(scores, "Bob")
Never read from a nil map (it returns zero value), but writing to a nil map panics. Always initialize: scores := make(map[string]int).

defer

defer schedules a function call to run when the surrounding function returns. Use it for cleanup.

func readFile(path string) error {
  f, err := os.Open(path)
  if err != nil {
    return err
  }
  defer f.Close() // runs when readFile returns, regardless of how

  // ... read file
  return nil
}
Node.js analogy: Like a finally block, but attached to the function instead of a try/catch, and it stacks (multiple defers run in LIFO order).

Key Takeaways