Structs and Methods

Go uses structs instead of classes. Combined with methods and interfaces, they provide a flexible approach to organizing code without traditional OOP inheritance.

Defining Structs

type Person struct {
    Name    string
    Age     int
    Email   string
}

type Point struct {
    X, Y float64  // Same type can share a line
}

// Empty struct (zero bytes, useful for signals and sets)
type signal struct{}

Creating Struct Instances

// Zero value (all fields are zero values)
var p Person
// p.Name = "", p.Age = 0, p.Email = ""

// Literal with field names (preferred)
p := Person{
    Name:  "Alice",
    Age:   30,
    Email: "alice@example.com",
}

// Literal without field names (fragile, avoid)
p := Person{"Alice", 30, "alice@example.com"}

// Partial initialization
p := Person{Name: "Bob"}  // Age=0, Email=""

// Pointer to struct
p := &Person{Name: "Charlie", Age: 25}

// Using new (returns pointer with zero values)
p := new(Person)  // *Person

Accessing Fields

p := Person{Name: "Alice", Age: 30}

// Direct access
fmt.Println(p.Name)  // Alice
p.Age = 31           // Modify

// Through pointer (Go auto-dereferences)
ptr := &p
fmt.Println(ptr.Name)  // Alice (not (*ptr).Name)
ptr.Age = 32           // Works directly

Anonymous Structs

Useful for one-off data structures:

// Inline definition and initialization
config := struct {
    Host string
    Port int
}{
    Host: "localhost",
    Port: 8080,
}

// Common in tests
tests := []struct {
    input    string
    expected int
}{
    {"hello", 5},
    {"world", 5},
    {"", 0},
}

Embedded Structs (Composition)

Go favors composition over inheritance:

type Address struct {
    Street  string
    City    string
    Country string
}

type Person struct {
    Name    string
    Age     int
    Address  // Embedded (anonymous field)
}

// Usage
p := Person{
    Name: "Alice",
    Age:  30,
    Address: Address{
        Street:  "123 Main St",
        City:    "Boston",
        Country: "USA",
    },
}

// Fields are "promoted" - can access directly
fmt.Println(p.City)      // Boston
fmt.Println(p.Address.City)  // Also works

Embedding and Name Conflicts

type A struct {
    Name string
}

type B struct {
    Name string
}

type C struct {
    A
    B
}

c := C{}
// c.Name  // Ambiguous! Compile error
c.A.Name   // OK
c.B.Name   // OK

Methods

Methods are functions with a receiver argument:

type Rectangle struct {
    Width, Height float64
}

// Method with value receiver
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// Method with value receiver
func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// Usage
rect := Rectangle{Width: 10, Height: 5}
fmt.Println(rect.Area())      // 50
fmt.Println(rect.Perimeter()) // 30

Value vs Pointer Receivers

type Counter struct {
    value int
}

// Value receiver (works on copy)
func (c Counter) Value() int {
    return c.value
}

// Pointer receiver (modifies original)
func (c *Counter) Increment() {
    c.value++
}

func (c *Counter) Add(n int) {
    c.value += n
}

// Usage
counter := Counter{}
counter.Increment()        // Go auto-converts to (&counter).Increment()
fmt.Println(counter.Value()) // 1

When to Use Pointer Receivers

Use pointer receivers when:

  1. The method modifies the receiver
  2. The struct is large (avoid copying)
  3. Consistency: if one method needs a pointer, use it for all
type Config struct {
    // Many fields...
    DatabaseURL string
    CacheSize   int
    // ...more fields
}

// Even if not modifying, use pointer for large structs
func (c *Config) Validate() error {
    if c.DatabaseURL == "" {
        return errors.New("database URL required")
    }
    return nil
}

Methods on Non-Struct Types

You can define methods on any type you own:

type MyInt int

func (m MyInt) Double() MyInt {
    return m * 2
}

func (m MyInt) IsPositive() bool {
    return m > 0
}

x := MyInt(5)
fmt.Println(x.Double())      // 10
fmt.Println(x.IsPositive())  // true

Constructors

Go doesn't have constructors. Use factory functions:

type Server struct {
    host string
    port int
}

// Constructor function (convention: New<Type> or New)
func NewServer(host string, port int) *Server {
    return &Server{
        host: host,
        port: port,
    }
}

// With validation
func NewServer(host string, port int) (*Server, error) {
    if port < 0 || port > 65535 {
        return nil, errors.New("invalid port")
    }
    return &Server{host: host, port: port}, nil
}

// Usage
server := NewServer("localhost", 8080)

Functional Options Pattern

For complex constructors with many optional parameters:

type Server struct {
    host    string
    port    int
    timeout time.Duration
    maxConn int
}

type ServerOption func(*Server)

func WithPort(port int) ServerOption {
    return func(s *Server) {
        s.port = port
    }
}

func WithTimeout(timeout time.Duration) ServerOption {
    return func(s *Server) {
        s.timeout = timeout
    }
}

func WithMaxConnections(max int) ServerOption {
    return func(s *Server) {
        s.maxConn = max
    }
}

func NewServer(host string, opts ...ServerOption) *Server {
    s := &Server{
        host:    host,
        port:    8080,          // default
        timeout: 30 * time.Second,  // default
        maxConn: 100,           // default
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

// Usage
server := NewServer("localhost",
    WithPort(9000),
    WithTimeout(60*time.Second),
)

Struct Tags

Metadata for struct fields, used by encoding packages:

type User struct {
    ID        int    `json:"id" db:"user_id"`
    Name      string `json:"name" db:"user_name"`
    Email     string `json:"email,omitempty"`  // Omit if empty
    Password  string `json:"-"`                 // Never encode
    CreatedAt time.Time `json:"created_at"`
}

// Usage with encoding/json
user := User{ID: 1, Name: "Alice", Email: "alice@example.com"}
data, _ := json.Marshal(user)
// {"id":1,"name":"Alice","email":"alice@example.com","created_at":"..."}

Reading Tags with Reflection

import "reflect"

type User struct {
    Name string `validate:"required,min=2"`
}

t := reflect.TypeOf(User{})
field, _ := t.FieldByName("Name")
tag := field.Tag.Get("validate")
fmt.Println(tag)  // "required,min=2"

Comparing Structs

Structs are comparable if all their fields are comparable:

type Point struct {
    X, Y int
}

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

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

// Structs with slices/maps are NOT comparable
type Data struct {
    Values []int  // Slice
}
// d1 == d2  // Compile error!

Struct Copying

Structs are copied by value:

p1 := Person{Name: "Alice", Age: 30}
p2 := p1      // Copy
p2.Name = "Bob"
fmt.Println(p1.Name)  // Still "Alice"

// Careful with pointers inside structs!
type Container struct {
    Data *[]int
}

c1 := Container{Data: &[]int{1, 2, 3}}
c2 := c1              // Shallow copy!
(*c2.Data)[0] = 100
fmt.Println((*c1.Data)[0])  // 100 - modified!

// Deep copy manually
c2 := Container{Data: &[]int{}}
*c2.Data = append(*c2.Data, *c1.Data...)

Common Patterns

Builder Pattern

type QueryBuilder struct {
    table   string
    columns []string
    where   string
    limit   int
}

func (qb *QueryBuilder) Select(cols ...string) *QueryBuilder {
    qb.columns = cols
    return qb
}

func (qb *QueryBuilder) From(table string) *QueryBuilder {
    qb.table = table
    return qb
}

func (qb *QueryBuilder) Where(condition string) *QueryBuilder {
    qb.where = condition
    return qb
}

func (qb *QueryBuilder) Limit(n int) *QueryBuilder {
    qb.limit = n
    return qb
}

func (qb *QueryBuilder) Build() string {
    // Build SQL string...
}

// Usage
query := new(QueryBuilder).
    Select("id", "name").
    From("users").
    Where("age > 18").
    Limit(10).
    Build()

Self-Referential Structs

type Node struct {
    Value int
    Next  *Node  // Pointer to same type
}

// Linked list
head := &Node{Value: 1}
head.Next = &Node{Value: 2}
head.Next.Next = &Node{Value: 3}

// Tree
type TreeNode struct {
    Value int
    Left  *TreeNode
    Right *TreeNode
}

Next Steps

Continue to 07-interfaces.md to learn about interfaces and polymorphism.