Lab 04: Structs & Methods

Objective

Define Go structs, attach methods, use embedding for composition, and understand value vs pointer receivers.

Time

30 minutes

Prerequisites

  • Lab 01–03

Tools

  • Docker image: zchencow/innozverse-go:latest


Lab Instructions

Step 1: Struct Basics

docker run --rm zchencow/innozverse-go:latest go run - << 'EOF'
package main

import "fmt"

type Address struct {
    Street string
    City   string
    State  string
    Zip    string
}

type Person struct {
    FirstName string
    LastName  string
    Age       int
    Email     string
    Address   Address // embedded struct (by value)
}

func (p Person) FullName() string {
    return p.FirstName + " " + p.LastName
}

func (p Person) String() string {
    return fmt.Sprintf("%s (%d) <%s>", p.FullName(), p.Age, p.Email)
}

func NewPerson(first, last string, age int, email string) Person {
    return Person{FirstName: first, LastName: last, Age: age, Email: email}
}

func main() {
    // Literal initialization
    p := Person{
        FirstName: "Chen",
        LastName:  "Dr.",
        Age:       42,
        Email:     "[email protected]",
        Address: Address{
            Street: "950 Ridge Rd",
            City:   "Claymont",
            State:  "DE",
            Zip:    "19703",
        },
    }

    fmt.Println(p)
    fmt.Println("City:", p.Address.City)

    // Struct comparison — structs with comparable fields are comparable
    p2 := NewPerson("Chen", "Dr.", 42, "[email protected]")
    fmt.Println("Equal (no address):", p.FirstName == p2.FirstName && p.Age == p2.Age)

    // Anonymous struct
    point := struct{ X, Y int }{X: 10, Y: 20}
    fmt.Println("Point:", point)

    // Struct tags (used by JSON, ORM, etc.)
    type Product struct {
        ID    int     `json:"id" db:"product_id"`
        Name  string  `json:"name"`
        Price float64 `json:"price,omitempty"`
    }
    prod := Product{ID: 1, Name: "Surface Pro", Price: 864}
    fmt.Printf("Product: %+v\n", prod)
}
EOF

💡 Struct tags (json:"name") are metadata strings read by reflection packages. They control how encoding/json, GORM, and validator work. Tags don't affect runtime behavior unless a package explicitly reads them via reflect.StructTag. The +v format verb prints field names alongside values.

📸 Verified Output:


Step 2: Value vs Pointer Receivers

💡 Use pointer receivers when: (1) the method modifies the struct, OR (2) the struct is large (copying is expensive), OR (3) you need consistency — if any method uses a pointer receiver, all methods should. Value receivers are safe for small, immutable data like Point. Go auto-dereferences: p.Method() works even if p is a pointer.

📸 Verified Output:


Step 3: Embedding & Composition

💡 Embedding is Go's answer to inheritance. When you embed Named in Product, all of Named's fields and methods are promoted to Product — you can call p.Name instead of p.Named.Name. But it's composition, not inheritance — Product is not a Named. This avoids the fragile base class problem.

📸 Verified Output:


Steps 4–8: Constructors, Stringer, Builder, Repository, Capstone Inventory System

📸 Verified Output:


Summary

Pattern
Syntax
When to use

Value receiver

func (p Point) Method()

Read-only, small structs

Pointer receiver

func (p *Product) Method()

Modifies struct, large structs

Embedding

type A struct { B }

Composition, promote methods

Constructor

func NewX(...) (*X, error)

Validated initialization

Stringer

func (x X) String() string

Human-readable output

Further Reading

Last updated