1. go
  2. /data structures
  3. /structs

Working with Structs in Go

Structs are user-defined types that group together related data fields. This guide covers everything you need to know about working with structs effectively in Go.

Struct Basics

Struct Declaration

// Basic struct declaration
type Person struct {
    FirstName string
    LastName  string
    Age       int
}

// Struct with tags
type User struct {
    ID        int    `json:"id"`
    Username  string `json:"username" validate:"required"`
    Email     string `json:"email" validate:"required,email"`
    CreatedAt time.Time `json:"created_at"`
}

// Anonymous struct
point := struct {
    X, Y int
}{10, 20}

Creating Struct Instances

// Zero value initialization
var person Person

// Struct literal
person = Person{
    FirstName: "John",
    LastName:  "Doe",
    Age:       30,
}

// Short declaration
employee := Person{
    FirstName: "Jane",
    LastName:  "Smith",
    Age:       25,
}

// Positional initialization (avoid in production)
person = Person{"John", "Doe", 30}

// New returns a pointer to struct
personPtr := new(Person)

Accessing Fields

// Direct field access
name := person.FirstName
person.Age = 31

// Pointer to struct
ptr := &person
ptr.FirstName = "Jane"  // Automatic dereferencing
(*ptr).LastName = "Smith"  // Explicit dereferencing

// Nested field access
type Address struct {
    Street string
    City   string
}

type Contact struct {
    Name    string
    Address Address
}

contact := Contact{
    Name: "John",
    Address: Address{
        Street: "123 Main St",
        City:   "New York",
    },
}

city := contact.Address.City

Methods

Method Declaration

// Value receiver
func (p Person) FullName() string {
    return fmt.Sprintf("%s %s", p.FirstName, p.LastName)
}

// Pointer receiver
func (p *Person) SetName(first, last string) {
    p.FirstName = first
    p.LastName = last
}

// Usage
person := Person{FirstName: "John", LastName: "Doe"}
fmt.Println(person.FullName())
person.SetName("Jane", "Smith")

Value vs Pointer Receivers

type Counter struct {
    count int
}

// Value receiver (copy)
func (c Counter) Value() int {
    return c.count
}

// Pointer receiver (modify original)
func (c *Counter) Increment() {
    c.count++
}

// Usage
counter := Counter{}
counter.Increment()  // Modifies counter
value := counter.Value()  // Reads counter

Struct Embedding

Basic Embedding

type Address struct {
    Street string
    City   string
    State  string
}

type Person struct {
    Name    string
    Address // Embedded struct
}

// Usage
person := Person{
    Name: "John",
    Address: Address{
        Street: "123 Main St",
        City:   "New York",
        State:  "NY",
    },
}

// Direct access to embedded fields
city := person.City  // Same as person.Address.City

Multiple Embedding

type Timestamp struct {
    CreatedAt time.Time
    UpdatedAt time.Time
}

type Metadata struct {
    ID     int
    Active bool
}

type User struct {
    Timestamp  // Embedded struct
    Metadata   // Embedded struct
    Name string
    Email string
}

// Usage
user := User{
    Name:  "John",
    Email: "[email protected]",
}
user.CreatedAt = time.Now()  // From Timestamp
user.ID = 1                  // From Metadata

Method Embedding

type Logger struct{}

func (l Logger) Log(message string) {
    fmt.Printf("Log: %s\n", message)
}

type Server struct {
    Logger  // Embedded struct
    Address string
}

// Usage
server := Server{Address: "localhost"}
server.Log("Server starting...")  // Inherited from Logger

Struct Tags

Tag Declaration

type Product struct {
    ID          int     `json:"id" db:"id"`
    Name        string  `json:"name" validate:"required"`
    Price       float64 `json:"price" validate:"gte=0"`
    SKU         string  `json:"sku" db:"stock_keeping_unit"`
    CreatedAt   time.Time `json:"created_at" db:"created_at"`
}

Accessing Tags

import "reflect"

func getFieldTags(v interface{}) map[string]string {
    tags := make(map[string]string)
    t := reflect.TypeOf(v)
    
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        tag := field.Tag.Get("json")
        if tag != "" {
            tags[field.Name] = tag
        }
    }
    
    return tags
}

// Usage
product := Product{}
tags := getFieldTags(product)

Best Practices

1. Struct Design

// Good: Related fields grouped together
type Customer struct {
    // Personal information
    FirstName string
    LastName  string
    Email     string
    
    // Address information
    Street  string
    City    string
    Country string
    
    // Account information
    AccountID   string
    CreatedAt   time.Time
    LastLoginAt time.Time
}

// Better: Use embedded structs for logical grouping
type PersonalInfo struct {
    FirstName string
    LastName  string
    Email     string
}

type Address struct {
    Street  string
    City    string
    Country string
}

type AccountInfo struct {
    AccountID   string
    CreatedAt   time.Time
    LastLoginAt time.Time
}

type Customer struct {
    PersonalInfo
    Address
    AccountInfo
}

2. Constructor Functions

// Good: Constructor function with validation
func NewUser(name, email string) (*User, error) {
    if name == "" {
        return nil, errors.New("name is required")
    }
    if !isValidEmail(email) {
        return nil, errors.New("invalid email")
    }
    
    return &User{
        Name:      name,
        Email:     email,
        CreatedAt: time.Now(),
    }, nil
}

// Usage
user, err := NewUser("John", "[email protected]")
if err != nil {
    log.Fatal(err)
}

3. Method Organization

// Good: Methods grouped by functionality
type Order struct {
    ID     int
    Items  []Item
    Status string
}

// Validation methods
func (o *Order) Validate() error {
    if len(o.Items) == 0 {
        return errors.New("order must have items")
    }
    return nil
}

// Business logic methods
func (o *Order) CalculateTotal() float64 {
    var total float64
    for _, item := range o.Items {
        total += item.Price
    }
    return total
}

// State management methods
func (o *Order) MarkAsShipped() error {
    if o.Status != "paid" {
        return errors.New("order must be paid before shipping")
    }
    o.Status = "shipped"
    return nil
}

Common Patterns

1. Builder Pattern

type ServerBuilder struct {
    server *Server
}

func NewServerBuilder() *ServerBuilder {
    return &ServerBuilder{server: &Server{}}
}

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) Build() (*Server, error) {
    if b.server.Host == "" {
        return nil, errors.New("host is required")
    }
    return b.server, nil
}

// Usage
server, err := NewServerBuilder().
    WithHost("localhost").
    WithPort(8080).
    Build()

2. Option Pattern

type ServerOption func(*Server)

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

func WithTLS(cert, key string) ServerOption {
    return func(s *Server) {
        s.TLSCert = cert
        s.TLSKey = key
    }
}

func NewServer(options ...ServerOption) *Server {
    server := &Server{
        Port: 8080,  // Default value
    }
    
    for _, option := range options {
        option(server)
    }
    
    return server
}

// Usage
server := NewServer(
    WithPort(3000),
    WithTLS("cert.pem", "key.pem"),
)

3. Factory Pattern

type PaymentMethod interface {
    Process(amount float64) error
}

type CreditCard struct {
    Number string
    CVV    string
}

type PayPal struct {
    Email string
}

func NewPaymentMethod(method string, config map[string]string) PaymentMethod {
    switch method {
    case "credit_card":
        return &CreditCard{
            Number: config["number"],
            CVV:    config["cvv"],
        }
    case "paypal":
        return &PayPal{
            Email: config["email"],
        }
    default:
        return nil
    }
}

Performance Considerations

1. Memory Layout

// Good: Struct field alignment
type Efficient struct {
    ID    int64    // 8 bytes
    Count int64    // 8 bytes
    Flag  bool     // 1 byte
    pad   [7]byte  // 7 bytes padding
}

// Bad: Inefficient memory layout
type Inefficient struct {
    Flag  bool     // 1 byte + 7 bytes padding
    ID    int64    // 8 bytes
    Count int64    // 8 bytes
}

2. Value vs Pointer

// Use value receiver for small structs
type Point struct{ X, Y float64 }
func (p Point) Distance() float64 { /* ... */ }

// Use pointer receiver for large structs
type Image struct{ data [1024*1024]byte }
func (i *Image) Process() { /* ... */ }

3. Struct Copying

// Efficient: Copy small structs by value
type Small struct {
    X, Y int
}
func process(s Small) { /* ... */ }

// Efficient: Pass large structs by pointer
type Large struct {
    Data [1024]int
}
func process(l *Large) { /* ... */ }

Next Steps

  1. Learn about interfaces
  2. Explore methods
  3. Study reflection
  4. Practice with design patterns

Additional Resources