Structs and Methods in Go

📚 Comprehensive Guide ⏱️ 25 min read 🎯 Intermediate Level

Understanding Structs in Go

Memory Layout and Alignment

Structs in Go are composite types that group related data together. Understanding their memory layout is crucial for writing efficient code. Fields are laid out in memory in the order they're declared, with padding added for alignment based on the platform's word size.

Struct Memory Principles

  • Alignment: Fields align to their natural boundaries (int64 to 8 bytes)
  • Padding: Compiler adds padding between fields for alignment
  • Size: Total size includes all fields plus padding
  • Ordering: Reordering fields can reduce memory usage
// Memory layout demonstration
type Inefficient struct {
    a bool    // 1 byte + 7 bytes padding
    b int64   // 8 bytes
    c bool    // 1 byte + 7 bytes padding
    d int64   // 8 bytes
}  // Total: 32 bytes

type Efficient struct {
    b int64   // 8 bytes
    d int64   // 8 bytes
    a bool    // 1 byte
    c bool    // 1 byte + 6 bytes padding
}  // Total: 24 bytes

// Check struct size
fmt.Println(unsafe.Sizeof(Inefficient{}))  // 32
fmt.Println(unsafe.Sizeof(Efficient{}))    // 24

Struct Fundamentals

Declaration and Initialization

Go provides multiple ways to create and initialize structs. Understanding the differences between zero values, literals, and the new function helps you choose the right approach.

// Struct definition
type Person struct {
    Name    string
    Age     int
    Email   string
    Active  bool
}

// Various initialization methods
var p1 Person                            // Zero value
p2 := Person{}                            // Empty literal
p3 := Person{Name: "Alice", Age: 30}    // Named fields
p4 := Person{"Bob", 25, "bob@ex.com", true} // Positional (avoid)

// Using new (returns pointer)
p5 := new(Person)                        // *Person, zero-valued
p6 := &Person{Name: "Carol"}             // *Person, literal

// Anonymous structs
point := struct {
    X, Y float64
}{10.5, 20.3}

// Struct tags
type User struct {
    ID       int    `json:"id" db:"user_id"`
    Username string `json:"username" validate:"required,min=3"`
    Password string `json:"-" db:"password_hash"`
}

Struct Comparison and Assignment

Structs are comparable if all their fields are comparable. Assignment copies all fields, creating an independent copy (value semantics).

// Comparison
type Point struct { X, Y int }

p1 := Point{1, 2}
p2 := Point{1, 2}
p3 := Point{2, 1}

fmt.Println(p1 == p2)  // true
fmt.Println(p1 == p3)  // false

// Assignment creates a copy
p4 := p1
p4.X = 100
fmt.Println(p1.X)      // Still 1

// Non-comparable structs (contains slice)
type Data struct {
    Values []int
}
// d1 == d2  // Compile error!

Methods in Go

Defining Methods

Methods are functions with a special receiver argument. The receiver appears between the func keyword and the method name, binding the function to the type.

// Method with value receiver
type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// Method with pointer receiver
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

// Method on non-struct types
type Counter int

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

func (c Counter) String() string {
    return fmt.Sprintf("Counter: %d", c)
}

Value vs Pointer Receivers

Choosing between value and pointer receivers is a critical design decision. Each has specific use cases and performance implications.

⚠️ Receiver Type Guidelines

  • Use pointer receivers for methods that modify the receiver
  • Use pointer receivers for large structs to avoid copying
  • Be consistent - if one method has a pointer receiver, all should
  • Value receivers are safe for concurrent use without synchronization
Aspect Value Receiver Pointer Receiver
Modification Cannot modify original Can modify original
Memory Copies entire struct Copies pointer (8 bytes)
Nil Safety Cannot be nil Must check for nil
Interface T and *T can use Only *T can use
Concurrency Safe without locks Needs synchronization

Struct Embedding and Composition

Embedding for Composition

Go doesn't have inheritance, but struct embedding provides a powerful composition mechanism. Embedded fields promote their methods to the outer struct, enabling code reuse.

// Base types
type Address struct {
    Street  string
    City    string
    Country string
}

func (a Address) FullAddress() string {
    return fmt.Sprintf("%s, %s, %s", a.Street, a.City, a.Country)
}

// Embedding
type Employee struct {
    Name    string
    ID      int
    Address // Embedded field
}

// Usage
emp := Employee{
    Name: "Alice",
    ID:   123,
    Address: Address{
        Street:  "123 Main St",
        City:    "Boston",
        Country: "USA",
    },
}

// Promoted fields and methods
fmt.Println(emp.City)          // Direct access to embedded field
fmt.Println(emp.FullAddress()) // Promoted method

// Method overriding
func (e Employee) String() string {
    return fmt.Sprintf("%s (#%d) at %s", e.Name, e.ID, e.FullAddress())
}

Multiple Embedding

Structs can embed multiple types, but field/method name conflicts must be resolved explicitly.

// Multiple embedding
type Reader struct {
    Name string
}

func (r Reader) Read() string {
    return "Reading..."
}

type Writer struct {
    Name string
}

func (w Writer) Write() string {
    return "Writing..."
}

type ReadWriter struct {
    Reader
    Writer
}

// Resolving conflicts
rw := ReadWriter{
    Reader: Reader{Name: "ReaderName"},
    Writer: Writer{Name: "WriterName"},
}

// Ambiguous: rw.Name (compile error)
fmt.Println(rw.Reader.Name)  // Explicit access
fmt.Println(rw.Writer.Name)
fmt.Println(rw.Read())      // Promoted method
fmt.Println(rw.Write())     // Promoted method

Advanced Patterns

Builder Pattern

The builder pattern is useful for constructing complex structs with many optional fields. It provides a fluent interface for step-by-step construction.

// Builder pattern implementation
type Server struct {
    Host     string
    Port     int
    Timeout  time.Duration
    MaxConns int
    TLS      bool
}

type ServerBuilder struct {
    server Server
}

func NewServerBuilder() *ServerBuilder {
    return &ServerBuilder{
        server: Server{
            Port:     8080,      // Default values
            Timeout:  30 * time.Second,
            MaxConns: 100,
        },
    }
}

func (b *ServerBuilder) WithHost(host string) *ServerBuilder {
    b.server.Host = host
    return b
}

func (b *ServerBuilder) WithPort(port int) *ServerBuilder {
    b.server.Port = port
    return b
}

func (b *ServerBuilder) WithTimeout(timeout time.Duration) *ServerBuilder {
    b.server.Timeout = timeout
    return b
}

func (b *ServerBuilder) WithTLS() *ServerBuilder {
    b.server.TLS = true
    return b
}

func (b *ServerBuilder) Build() (*Server, error) {
    if b.server.Host == "" {
        return nil, fmt.Errorf("host is required")
    }
    return &b.server, nil
}

// Usage
server, err := NewServerBuilder().
    WithHost("localhost").
    WithPort(9000).
    WithTimeout(60 * time.Second).
    WithTLS().
    Build()

Functional Options Pattern

Functional options provide a clean way to handle optional parameters and configuration. This pattern is widely used in Go libraries for API design.

// Functional options pattern
type Client struct {
    baseURL    string
    timeout    time.Duration
    maxRetries int
    apiKey     string
}

type ClientOption func(*Client)

func WithTimeout(d time.Duration) ClientOption {
    return func(c *Client) {
        c.timeout = d
    }
}

func WithMaxRetries(n int) ClientOption {
    return func(c *Client) {
        c.maxRetries = n
    }
}

func WithAPIKey(key string) ClientOption {
    return func(c *Client) {
        c.apiKey = key
    }
}

func NewClient(baseURL string, opts ...ClientOption) *Client {
    c := &Client{
        baseURL:    baseURL,
        timeout:    30 * time.Second,  // Defaults
        maxRetries: 3,
    }
    
    for _, opt := range opts {
        opt(c)
    }
    
    return c
}

// Usage
client := NewClient("https://api.example.com",
    WithTimeout(60*time.Second),
    WithMaxRetries(5),
    WithAPIKey("secret-key"),
)

Method Sets and Interfaces

Understanding Method Sets

The method set determines which methods are accessible and which interfaces a type implements. The rules differ for value and pointer types.

Method Set Rules

  • Method set of T contains all methods with receiver T
  • Method set of *T contains all methods with receiver T or *T
  • Interface satisfaction depends on the method set
  • Addressable values can use pointer receiver methods
// Method sets demonstration
type Shape interface {
    Area() float64
    Scale(float64)
}

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c *Circle) Scale(factor float64) {
    c.Radius *= factor
}

// Interface satisfaction
var s Shape
c := Circle{Radius: 5}
// s = c     // Compile error! Circle doesn't implement Shape
s = &c       // OK: *Circle implements Shape

// But addressable values work
circles := []Circle{{1}, {2}, {3}}
circles[0].Scale(2)  // OK: circles[0] is addressable

Performance Considerations

Stack vs Heap Allocation

Understanding when structs are allocated on the stack versus the heap is crucial for performance. Escape analysis determines the allocation location.

// Stack allocation (no escape)
func stackAlloc() {
    p := Person{Name: "Alice", Age: 30}
    fmt.Println(p.Name)
    // p doesn't escape, allocated on stack
}

// Heap allocation (escapes)
func heapAlloc() *Person {
    p := &Person{Name: "Bob", Age: 25}
    return p  // p escapes, allocated on heap
}

// Check escape analysis
// go build -gcflags="-m" main.go

// Benchmark comparison
func BenchmarkStackStruct(b *testing.B) {
    for i := 0; i < b.N; i++ {
        p := Person{Name: "Test", Age: 20}
        _ = p
    }
}

func BenchmarkHeapStruct(b *testing.B) {
    for i := 0; i < b.N; i++ {
        p := new(Person)
        p.Name = "Test"
        p.Age = 20
        _ = p
    }
}
Aspect Stack Allocation Heap Allocation
Speed Very fast Slower (GC overhead)
Memory Auto-freed on return GC managed
Size Limits Limited (typically 2-8MB) Limited by available RAM
Sharing Cannot share beyond scope Can share via pointers
Use Case Local, temporary data Shared, long-lived data

Best Practices

Struct and Method Guidelines

  • Keep structs focused: Single responsibility principle
  • Use embedding wisely: Prefer composition over inheritance
  • Be consistent with receivers: All pointer or all value
  • Document exported types: Clear godoc comments
  • Consider zero values: Make them useful when possible
  • Validate in constructors: Return errors for invalid states
  • Use tags appropriately: JSON, validation, database mapping

Common Pitfalls

⚠️ Struct Gotchas

  • Nil pointer receivers: Methods must handle nil gracefully
  • Copying mutexes: Never copy structs containing sync.Mutex
  • Large value receivers: Cause unnecessary copying
  • Unexported fields: Break JSON marshaling across packages
  • Embedded conflicts: Ambiguous field/method access

🎯 Practice Exercises

Exercise 1: Design a Cache

Create a generic cache struct with methods for Get, Set, Delete, and TTL support.

Exercise 2: Implement a Vector Type

Build a 3D vector struct with methods for math operations (add, subtract, dot product, cross product).

Exercise 3: Builder with Validation

Create a configuration builder that validates settings and returns detailed errors.

Exercise 4: Method Chaining

Design a query builder with fluent interface for building SQL-like queries.