Structs
Structs are Go's primary way to create composite types. Master struct embedding (Go's version of inheritance), struct tags, memory layout and padding, and constructor patterns used in production codebases.
π― Learning Objectives
- Define structs and use all initialisation forms
- Understand struct memory layout and field padding
- Use struct embedding to achieve code reuse
- Apply struct tags for JSON, YAML, and database ORM
- Implement the constructor pattern for complex structs
- Know when to pass structs by value vs by pointer
- Use anonymous structs for one-off data shapes
π Struct Definition & Initialisation
package main
import "fmt"
// Define a struct type
type Person struct {
Name string
Age int
Email string
IsAdmin bool
}
// Unexported fields (lowercase) are not accessible outside the package
type Account struct {
ID int
Username string
balance float64 // unexported β only accessible within package
}
func main() {
// Form 1: zero value (all fields zeroed)
var p1 Person
fmt.Println(p1) // { 0 false}
// Form 2: positional literal (fragile β breaks if fields are reordered!)
p2 := Person{"Alice", 30, "alice@example.com", true}
// Form 3: named fields (preferred β readable, order-independent)
p3 := Person{
Name: "Bob",
Age: 25,
Email: "bob@example.com",
IsAdmin: false,
}
// Unset fields get zero values
p4 := Person{Name: "Charlie"} // Age=0, Email="", IsAdmin=false
// Accessing fields
fmt.Println(p3.Name) // Bob
p3.Age = 26 // modify a field
fmt.Println(p3.Age) // 26
// Pointer to struct β Go auto-dereferences
p5 := &Person{Name: "Dave", Age: 40}
fmt.Println(p5.Name) // Dave (same as (*p5).Name)
p5.Age++ // Go auto-dereferences: (*p5).Age++
// Struct comparison β structs are comparable if all fields are comparable
p6 := Person{Name: "Alice", Age: 30, Email: "alice@example.com", IsAdmin: true}
fmt.Println(p2 == p6) // true (all fields equal)
_ = p1; _ = p4
}π§ Memory Layout & Padding
Fields in a struct are laid out in memory in the order they are declared. The compiler may insert padding bytes between fields to satisfy alignment requirements. Field order can significantly affect struct size.
struct {
a bool // 1 byte
// 7 bytes PADDING
b int64 // 8 bytes
c bool // 1 byte
// 7 bytes PADDING
}
Total: 1+7+8+1+7 = 24 bytes!
struct {
b int64 // 8 bytes
a bool // 1 byte
c bool // 1 byte
// 0 bytes padding (aligned!)
}
Total: 8+1+1 = 10 bytes (16 w/align)
package main
import (
"fmt"
"unsafe"
)
// Bad ordering β wasted padding
type BadLayout struct {
A bool // 1 byte
B int64 // 8 bytes β needs 8-byte alignment
C bool // 1 byte
D int32 // 4 bytes
}
// Good ordering β fields sorted largest-to-smallest
type GoodLayout struct {
B int64 // 8 bytes
D int32 // 4 bytes
A bool // 1 byte
C bool // 1 byte
// 2 bytes padding to make total a multiple of 8
}
func main() {
fmt.Printf("BadLayout: %d bytes\n", unsafe.Sizeof(BadLayout{})) // 32 bytes!
fmt.Printf("GoodLayout: %d bytes\n", unsafe.Sizeof(GoodLayout{})) // 16 bytes
// Check field offsets
bad := BadLayout{}
fmt.Printf("Bad: A@%d, B@%d, C@%d, D@%d\n",
unsafe.Offsetof(bad.A), unsafe.Offsetof(bad.B),
unsafe.Offsetof(bad.C), unsafe.Offsetof(bad.D))
// A@0, B@8 (7 bytes padding!), C@16, D@20
}In most production code, struct padding is a micro-optimisation not worth worrying about. However, for structs that are: (1) used in very large arrays (millions of instances); (2) passed between CPU cores; (3) serialised as binary, field ordering matters. Use fieldalignment from golang.org/x/tools to detect poorly ordered structs.
π Struct Embedding β Composition over Inheritance
Go doesn't have inheritance. Instead, it uses embedding to achieve code reuse. An embedded struct's fields and methods are promoted to the outer struct.
package main
import "fmt"
// Base type
type Animal struct {
Name string
Age int
}
func (a Animal) Speak() string {
return fmt.Sprintf("I am %s, age %d", a.Name, a.Age)
}
func (a Animal) IsAlive() bool { return true }
// Dog embeds Animal β gets all its fields and methods
type Dog struct {
Animal // embedded (no field name β type name is used)
Breed string
}
func (d Dog) Speak() string { // overrides Animal.Speak
return fmt.Sprintf("Woof! I'm %s the %s", d.Name, d.Breed)
}
// Cat also embeds Animal
type Cat struct {
Animal
IndoorOnly bool
}
// Multiple embedding β embed several types
type Logger struct {
Prefix string
}
func (l Logger) Log(msg string) { fmt.Printf("[%s] %s\n", l.Prefix, msg) }
type Server struct {
Logger // embedded logger
Host string
Port int
}
func main() {
d := Dog{
Animal: Animal{Name: "Rex", Age: 3},
Breed: "Labrador",
}
// Access promoted fields directly
fmt.Println(d.Name) // Rex (promoted from Animal)
fmt.Println(d.Age) // 3 (promoted from Animal)
fmt.Println(d.Breed) // Labrador
// Access promoted methods
fmt.Println(d.IsAlive()) // true (Animal.IsAlive)
fmt.Println(d.Speak()) // Woof! I'm Rex the Labrador (Dog.Speak overrides)
// Access Animal.Speak directly (before override)
fmt.Println(d.Animal.Speak()) // I am Rex, age 3
// Embedding enables interface satisfaction
// If Animal implements fmt.Stringer, Dog gets it too
s := Server{
Logger: Logger{Prefix: "APP"},
Host: "localhost",
Port: 8080,
}
s.Log("Server starting") // [APP] Server starting (promoted from Logger)
s.Logger.Log("direct") // access explicitly
c := Cat{Animal: Animal{Name: "Whiskers", Age: 5}, IndoorOnly: true}
fmt.Println(c.Speak()) // I am Whiskers, age 5 (Animal.Speak, not overridden)
}π Constructor Patterns
Go doesn't have constructors, but uses factory functions by convention. The naming convention is New[TypeName](...).
package main
import (
"fmt"
"time"
)
// Simple constructor
type Config struct {
Host string
Port int
Timeout time.Duration
MaxConn int
}
// NewConfig validates and initialises a Config
func NewConfig(host string, port int) (*Config, error) {
if host == "" {
return nil, fmt.Errorf("host cannot be empty")
}
if port < 1 || port > 65535 {
return nil, fmt.Errorf("invalid port: %d", port)
}
return &Config{
Host: host,
Port: port,
Timeout: 30 * time.Second, // sensible defaults
MaxConn: 100,
}, nil
}
// Options pattern β for structs with many optional fields
type ServerOption func(*Server2)
type Server2 struct {
host string
port int
timeout time.Duration
maxConn int
tls bool
}
func WithTimeout(d time.Duration) ServerOption {
return func(s *Server2) { s.timeout = d }
}
func WithMaxConn(n int) ServerOption {
return func(s *Server2) { s.maxConn = n }
}
func WithTLS() ServerOption {
return func(s *Server2) { s.tls = true }
}
func NewServer(host string, port int, opts ...ServerOption) *Server2 {
s := &Server2{
host: host,
port: port,
timeout: 30 * time.Second, // defaults
maxConn: 50,
}
for _, opt := range opts {
opt(s) // apply each option
}
return s
}
func main() {
// Simple constructor
cfg, err := NewConfig("localhost", 8080)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Printf("Config: %+v\n", cfg)
}
// Test validation
_, err = NewConfig("", 8080)
fmt.Println("Empty host:", err)
// Options pattern β readable, extensible
srv := NewServer("localhost", 9090,
WithTimeout(60*time.Second),
WithMaxConn(200),
WithTLS(),
)
fmt.Printf("Server: %+v\n", *srv)
// Anonymous struct β for one-off shapes
point := struct {
X, Y float64
}{X: 3.14, Y: 2.71}
fmt.Printf("Point: %+v\n", point)
// Slice of anonymous structs β common for table-driven tests
tests := []struct {
input int
expected int
}{
{1, 1},
{5, 120},
{10, 3628800},
}
for _, tc := range tests {
fmt.Printf("factorial(%d) == %d\n", tc.input, tc.expected)
}
}π€ Interview Questions
Go doesn't have inheritance. Instead it uses composition via embedding: you embed one struct type inside another without giving it a field name. The embedded struct's fields and methods are "promoted" β you can access them directly on the outer struct without the intermediate name.
Key difference from inheritance: embedding is delegation, not subtyping. A Dog embedding Animal doesn't make Dog an Animal at the type system level. You can't pass a *Dog to a function expecting *Animal. But you can: define methods on Dog that promote Animal's methods; override specific methods; have Dog satisfy interfaces that Animal satisfies.
Multiple embedding is allowed. If two embedded types have a method with the same name, accessing that method directly is a compile error β you must use the explicit path (outer.Inner1.Method()).
Pass by value when: (1) the struct is small (1β3 machine words β 8β24 bytes on 64-bit); (2) you want a copy and won't modify the original; (3) the struct has value semantics (like a mathematical point or a color).
Pass by pointer when: (1) the struct is large (copying would be expensive); (2) you need to modify the struct inside the function; (3) nil is a meaningful value (optional struct); (4) you need to implement mutating methods; (5) the struct contains a sync.Mutex (must not be copied!).
Rule of thumb: if in doubt, use a pointer. The Go team uses pointers for almost all methods on structs in the standard library. For very small immutable structs (like time.Time), value semantics are used to avoid heap allocation.
β οΈ Never copy a sync.Mutex β always pass structs containing mutexes by pointer.
π§ͺ Quiz
Animal into struct Dog?json:"-" do?json:"name,omitempty" do when Name == ""?π Cheat Sheet
type T struct { Field Type }T{Field1: v1, Field2: v2}var t T // all fields zeroed&T{Field: val}type Dog struct { Animal }Field string `json:"field_name"`Field string `json:"name,omitempty"`Password string `json:"-"`func NewT(args) (*T, error) { }type Option func(*T); NewT(opts...)struct{ X, Y int }{X:1, Y:2}by pointer *T to avoid copying