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.