Structs and Methods in Go

Structs and Methods in Go

Structs are collections of fields that together represent a single concept or entity. Methods are functions associated with a particular type, allowing you to work with that type’s data.

What are Structs?

A struct is a composite data type that groups together variables under a single name. These variables, called fields, can be of different types.

Basic Struct Definition
type Person struct {
    Name string
    Age  int
    Email string
}

Creating and Using Structs

Basic Struct Usage

package main

import "fmt"

// Define a struct
type Person struct {
    Name  string
    Age   int
    Email string
}

func main() {
    // Create a struct using field names
    person1 := Person{
        Name:  "Alice Johnson",
        Age:   30,
        Email: "[email protected]",
    }
    
    // Create a struct without field names (order matters)
    person2 := Person{"Bob Smith", 25, "[email protected]"}
    
    // Create a struct and set fields later
    var person3 Person
    person3.Name = "Carol Williams"
    person3.Age = 28
    person3.Email = "[email protected]"
    
    fmt.Println("Person 1:", person1)
    fmt.Println("Person 2:", person2)
    fmt.Println("Person 3:", person3)
    
    // Access individual fields
    fmt.Printf("%s is %d years old\n", person1.Name, person1.Age)
}

Struct Pointers

package main

import "fmt"

type Car struct {
    Make  string
    Model string
    Year  int
}

func main() {
    // Create a struct
    car := Car{Make: "Toyota", Model: "Camry", Year: 2020}
    
    // Create a pointer to a struct
    carPtr := &car
    
    // Access fields through pointer (automatic dereferencing)
    fmt.Println("Make:", carPtr.Make)
    
    // Modify through pointer
    carPtr.Year = 2021
    fmt.Println("Updated year:", car.Year)
    
    // Create pointer directly
    carPtr2 := &Car{
        Make:  "Honda",
        Model: "Civic",
        Year:  2022,
    }
    fmt.Println("Car 2:", *carPtr2)
}

Methods

Methods are functions attached to a specific type. They can access and modify the data of that type.

Value Receiver Methods

package main

import (
    "fmt"
    "math"
)

// Rectangle struct
type Rectangle struct {
    Width  float64
    Height float64
}

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

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

func (r Rectangle) Diagonal() float64 {
    return math.Sqrt(r.Width*r.Width + r.Height*r.Height)
}

func main() {
    rect := Rectangle{Width: 5, Height: 3}
    
    fmt.Printf("Rectangle: %.1f x %.1f\n", rect.Width, rect.Height)
    fmt.Printf("Area: %.2f\n", rect.Area())
    fmt.Printf("Perimeter: %.2f\n", rect.Perimeter())
    fmt.Printf("Diagonal: %.2f\n", rect.Diagonal())
}

Pointer Receiver Methods

package main

import "fmt"

type BankAccount struct {
    Owner   string
    Balance float64
}

// Pointer receiver method - can modify the struct
func (ba *BankAccount) Deposit(amount float64) {
    if amount > 0 {
        ba.Balance += amount
        fmt.Printf("Deposited: $%.2f, New Balance: $%.2f\n", amount, ba.Balance)
    }
}

func (ba *BankAccount) Withdraw(amount float64) bool {
    if amount > 0 && ba.Balance >= amount {
        ba.Balance -= amount
        fmt.Printf("Withdrew: $%.2f, New Balance: $%.2f\n", amount, ba.Balance)
        return true
    }
    fmt.Printf("Withdrawal failed: Insufficient funds\n")
    return false
}

// Value receiver method - cannot modify the struct
func (ba BankAccount) GetBalance() float64 {
    return ba.Balance
}

func (ba BankAccount) String() string {
    return fmt.Sprintf("Account Owner: %s, Balance: $%.2f", ba.Owner, ba.Balance)
}

func main() {
    account := BankAccount{Owner: "John Doe", Balance: 1000.0}
    
    fmt.Println(account) // Uses String() method automatically
    
    account.Deposit(500.0)
    account.Withdraw(200.0)
    account.Withdraw(2000.0) // Will fail
    
    fmt.Printf("Current balance: $%.2f\n", account.GetBalance())
}

Advanced Struct Features

Embedded Structs (Composition)

package main

import "fmt"

// Address struct
type Address struct {
    Street  string
    City    string
    State   string
    ZipCode string
}

// Person struct with embedded Address
type Person struct {
    Name    string
    Age     int
    Address // Embedded struct
}

func main() {
    person := Person{
        Name: "Alice Johnson",
        Age:  30,
        Address: Address{
            Street:  "123 Main St",
            City:    "New York",
            State:   "NY",
            ZipCode: "10001",
        },
    }
    
    // Access embedded fields directly
    fmt.Println("Name:", person.Name)
    fmt.Println("Street:", person.Street) // Direct access to embedded field
    
    // Access embedded struct
    fmt.Println("Full Address:", person.Address)
    
    // Alternative access to embedded fields
    fmt.Println("City:", person.Address.City)
}

Struct Tags

Struct tags provide metadata about struct fields, commonly used for JSON encoding, database mapping, and validation:

package main

import (
    "encoding/json"
    "fmt"
    "reflect"
)

type User struct {
    ID        int    `json:"id" db:"user_id"`
    Username  string `json:"username" db:"username" validate:"required,min=3,max=20"`
    Email     string `json:"email" db:"email" validate:"required,email"`
    Age       int    `json:"age,omitempty" db:"age"`
    Password  string `json:"-" db:"password"` // "-" means omit in JSON
    CreatedAt string `json:"created_at" db:"created_at"`
}

func getStructTags() {
    t := reflect.TypeOf(User{})
    
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fmt.Printf("Field: %s, Tags: %s\n", field.Name, field.Tag)
    }
}

func main() {
    user := User{
        ID:        1,
        Username:  "johndoe",
        Email:     "[email protected]",
        Age:       30,
        Password:  "secret123",
        CreatedAt: "2023-01-01",
    }
    
    // Convert to JSON
    jsonData, err := json.MarshalIndent(user, "", "  ")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    
    fmt.Println("JSON representation:")
    fmt.Println(string(jsonData))
    
    fmt.Println("\nStruct tags:")
    getStructTags()
}

Anonymous Structs

package main

import "fmt"

func main() {
    // Anonymous struct literal
    person := struct {
        Name string
        Age  int
        City string
    }{
        Name: "Alice",
        Age:  30,
        City: "New York",
    }
    
    fmt.Printf("%+v\n", person)
    
    // Slice of anonymous structs
    employees := []struct {
        ID     int
        Name   string
        Salary float64
    }{
        {ID: 1, Name: "John", Salary: 50000},
        {ID: 2, Name: "Jane", Salary: 60000},
        {ID: 3, Name: "Bob", Salary: 55000},
    }
    
    for _, emp := range employees {
        fmt.Printf("ID: %d, Name: %s, Salary: $%.2f\n", emp.ID, emp.Name, emp.Salary)
    }
    
    // Function returning anonymous struct
    getUser := func(id int) struct {
        ID   int
        Name string
        Role string
    } {
        if id == 1 {
            return struct {
                ID   int
                Name string
                Role string
            }{ID: 1, Name: "Admin User", Role: "Administrator"}
        }
        return struct {
            ID   int
            Name string
            Role string
        }{ID: id, Name: "Regular User", Role: "User"}
    }
    
    user := getUser(1)
    fmt.Printf("User: %+v\n", user)
}

Method Sets and Interfaces

package main

import "fmt"

// Writer interface
type Writer interface {
    Write(data string) error
}

// Reader interface
type Reader interface {
    Read() string
}

// Document struct
type Document struct {
    content string
}

// Pointer receiver method - implements Writer
func (d *Document) Write(data string) error {
    d.content = data
    return nil
}

// Value receiver method - implements Reader
func (d Document) Read() string {
    return d.content
}

func main() {
    // Create a Document
    doc := &Document{}
    
    // Use Write method (requires pointer)
    err := doc.Write("Hello, Go methods!")
    if err != nil {
        fmt.Println("Error writing:", err)
        return
    }
    
    // Use Read method (works with value or pointer)
    content := doc.Read()
    fmt.Println("Content:", content)
    
    // Interface usage
    var writer Writer = doc // OK because Write has pointer receiver
    writer.Write("New content")
    
    var reader Reader = *doc // OK because Read has value receiver
    content2 := reader.Read()
    fmt.Println("Content from interface:", content2)
    
    // This won't compile because Read() has value receiver
    // var reader2 Reader = doc
}

Practical Example: Shopping Cart

package main

import (
    "fmt"
    "time"
)

type Item struct {
    ID          string
    Name        string
    Price       float64
    Description string
}

type CartItem struct {
    Item    Item
    Quantity int
}

func (ci CartItem) Subtotal() float64 {
    return ci.Item.Price * float64(ci.Quantity)
}

type ShoppingCart struct {
    Items      []CartItem
    CustomerID string
    CreatedAt  time.Time
}

func NewShoppingCart(customerID string) *ShoppingCart {
    return &ShoppingCart{
        Items:      make([]CartItem, 0),
        CustomerID: customerID,
        CreatedAt:  time.Now(),
    }
}

func (sc *ShoppingCart) AddItem(item Item, quantity int) {
    // Check if item already exists in cart
    for i, cartItem := range sc.Items {
        if cartItem.Item.ID == item.ID {
            sc.Items[i].Quantity += quantity
            fmt.Printf("Updated quantity for %s to %d\n", item.Name, sc.Items[i].Quantity)
            return
        }
    }
    
    // Add new item
    sc.Items = append(sc.Items, CartItem{
        Item:    item,
        Quantity: quantity,
    })
    fmt.Printf("Added %d x %s to cart\n", quantity, item.Name)
}

func (sc *ShoppingCart) RemoveItem(itemID string) bool {
    for i, cartItem := range sc.Items {
        if cartItem.Item.ID == itemID {
            sc.Items = append(sc.Items[:i], sc.Items[i+1:]...)
            fmt.Printf("Removed %s from cart\n", cartItem.Item.Name)
            return true
        }
    }
    fmt.Printf("Item with ID %s not found in cart\n", itemID)
    return false
}

func (sc *ShoppingCart) UpdateQuantity(itemID string, quantity int) bool {
    if quantity <= 0 {
        return sc.RemoveItem(itemID)
    }
    
    for i, cartItem := range sc.Items {
        if cartItem.Item.ID == itemID {
            sc.Items[i].Quantity = quantity
            fmt.Printf("Updated quantity for %s to %d\n", cartItem.Item.Name, quantity)
            return true
        }
    }
    fmt.Printf("Item with ID %s not found in cart\n", itemID)
    return false
}

func (sc ShoppingCart) Total() float64 {
    var total float64
    for _, cartItem := range sc.Items {
        total += cartItem.Subtotal()
    }
    return total
}

func (sc ShoppingCart) ItemCount() int {
    count := 0
    for _, cartItem := range sc.Items {
        count += cartItem.Quantity
    }
    return count
}

func (sc ShoppingCart) Display() {
    fmt.Printf("\nShopping Cart for Customer: %s\n", sc.CustomerID)
    fmt.Printf("Created: %s\n", sc.CreatedAt.Format("2006-01-02 15:04:05"))
    fmt.Println("Items:")
    
    if len(sc.Items) == 0 {
        fmt.Println("  (Cart is empty)")
        return
    }
    
    for _, cartItem := range sc.Items {
        fmt.Printf("  - %s (ID: %s)\n", cartItem.Item.Name, cartItem.Item.ID)
        fmt.Printf("    Price: $%.2f x %d = $%.2f\n", 
            cartItem.Item.Price, cartItem.Quantity, cartItem.Subtotal())
        fmt.Printf("    %s\n", cartItem.Item.Description)
    }
    
    fmt.Printf("\nTotal Items: %d\n", sc.ItemCount())
    fmt.Printf("Total Price: $%.2f\n", sc.Total())
}

func main() {
    // Create some items
    laptop := Item{
        ID:          "LT001",
        Name:        "Laptop",
        Price:       999.99,
        Description: "High-performance laptop with 16GB RAM",
    }
    
    mouse := Item{
        ID:          "MS001",
        Name:        "Wireless Mouse",
        Price:       29.99,
        Description: "Ergonomic wireless mouse",
    }
    
    keyboard := Item{
        ID:          "KB001",
        Name:        "Mechanical Keyboard",
        Price:       149.99,
        Description: "RGB mechanical keyboard",
    }
    
    // Create shopping cart
    cart := NewShoppingCart("CUST12345")
    cart.Display()
    
    // Add items
    cart.AddItem(laptop, 1)
    cart.AddItem(mouse, 2)
    cart.AddItem(keyboard, 1)
    cart.Display()
    
    // Update quantity
    cart.UpdateQuantity("MS001", 3)
    cart.Display()
    
    // Remove item
    cart.RemoveItem("KB001")
    cart.Display()
    
    fmt.Printf("\nFinal Total: $%.2f\n", cart.Total())
}

Best Practices

  1. Use clear, descriptive names for your structs and fields
  2. Group related fields together in logical structs
  3. Use pointer receivers when methods need to modify the struct
  4. Use value receivers when methods don’t need to modify the struct
  5. Keep structs small and focused on a single responsibility
  6. Use struct tags for serialization and validation metadata
  7. Consider embedding for composition instead of inheritance

External Resources:

Related Tutorials:

Last updated on