Web Analytics

Structs & Methods

Beginner ~30 min read

Structs are Go's way of creating custom types that group related data together. Methods let you add behavior to these types. Together, they form the foundation of object-oriented programming in Goβ€”without classes or inheritance!

What is a Struct?

A struct is a composite data type that groups together zero or more fields:

Output
Click Run to execute your code
Struct Characteristics:
  • Value type - Copying creates a new struct
  • Fields can be any type (including other structs)
  • Zero value - All fields set to their zero values
  • Exported fields start with uppercase letter

Creating Struct Instances

// Using struct literal
p1 := Person{Name: "Alice", Age: 25}

// Positional (not recommended)
p2 := Person{"Bob", 30}

// Zero value
var p3 Person  // Name: "", Age: 0

// Pointer to struct
p4 := &Person{Name: "Carol", Age: 28}

// Partial initialization
p5 := Person{Name: "Dave"}  // Age: 0
Best Practice: Always use named fields in struct literals. It's more readable and won't break if you reorder fields!

Methods

Methods are functions with a special receiver argument that associates them with a type:

Output
Click Run to execute your code
Method Syntax:
func (receiver Type) MethodName(params) returnType {
    // method body
}
The receiver appears between func and the method name.

Value vs Pointer Receivers

Methods can have value receivers or pointer receivers:

Output
Click Run to execute your code
Receiver Type When to Use Behavior
Value
(r Type)
β€’ Method doesn't modify receiver
β€’ Small structs
β€’ Read-only operations
Works on a copy
Pointer
(r *Type)
β€’ Method modifies receiver
β€’ Large structs
β€’ Consistency
Works on original
Best Practice: Use pointer receivers by default unless you have a good reason not to. This avoids copying and allows modification.
Structs and Methods - Value vs Pointer Receivers

Figure: Structs and Methods - Understanding value receivers (copy) vs pointer receivers (reference)

Struct Embedding (Composition)

Go doesn't have inheritance, but it has composition through embedding:

Output
Click Run to execute your code
Embedding Benefits:
  • Promoted fields - Access embedded fields directly
  • Promoted methods - Call embedded type's methods
  • Composition over inheritance - More flexible
  • No diamond problem - Simpler than inheritance

Anonymous Structs

Structs without a name, useful for one-time use:

// Anonymous struct
person := struct {
    Name string
    Age  int
}{
    Name: "Alice",
    Age:  25,
}

// Common use: test data
testCases := []struct {
    input    int
    expected int
}{
    {1, 2},
    {2, 4},
    {3, 6},
}

for _, tc := range testCases {
    result := double(tc.input)
    if result != tc.expected {
        t.Errorf("Expected %d, got %d", tc.expected, result)
    }
}

Struct Tags

Struct tags provide metadata about fields, commonly used for JSON encoding:

type User struct {
    ID        int    `json:"id"`
    Name      string `json:"name"`
    Email     string `json:"email,omitempty"`
    Password  string `json:"-"`  // Never serialize
    CreatedAt time.Time `json:"created_at"`
}

user := User{
    ID:    1,
    Name:  "Alice",
    Email: "[email protected]",
}

// Marshal to JSON
data, _ := json.Marshal(user)
fmt.Println(string(data))
// {"id":1,"name":"Alice","email":"[email protected]","created_at":"..."}

Common Mistakes

1. Forgetting pointer receiver for modification

// ❌ Wrong - value receiver doesn't modify original
type Counter struct {
    count int
}

func (c Counter) Increment() {
    c.count++  // Modifies copy!
}

c := Counter{}
c.Increment()
fmt.Println(c.count)  // 0 (unchanged)

// βœ… Correct - pointer receiver
func (c *Counter) Increment() {
    c.count++  // Modifies original
}

c := Counter{}
c.Increment()
fmt.Println(c.count)  // 1

2. Comparing structs with uncomparable fields

// ❌ Wrong - can't compare structs with slices
type Person struct {
    Name    string
    Friends []string  // Slice is not comparable
}

p1 := Person{Name: "Alice"}
p2 := Person{Name: "Alice"}
if p1 == p2 {  // Error: invalid operation
    // ...
}

// βœ… Correct - compare manually or use reflect.DeepEqual
func (p Person) Equals(other Person) bool {
    if p.Name != other.Name {
        return false
    }
    // Compare slices manually
    // ...
}

3. Mixing value and pointer receivers

// ❌ Inconsistent - mixing receiver types
type Person struct {
    Name string
}

func (p Person) GetName() string {
    return p.Name
}

func (p *Person) SetName(name string) {
    p.Name = name
}

// βœ… Better - use pointer receivers consistently
func (p *Person) GetName() string {
    return p.Name
}

func (p *Person) SetName(name string) {
    p.Name = name
}

Exercise: Rectangle Calculator

Task: Create a Rectangle struct with methods.

Requirements:

  • Create a Rectangle struct with Width and Height
  • Add an Area() method
  • Add a Perimeter() method
  • Add a Scale(factor float64) method to resize
  • Test with a 5x10 rectangle
Show Solution
package main

import "fmt"

type Rectangle struct {
    Width  float64
    Height float64
}

// Area calculates the area
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

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

// Scale resizes the rectangle
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

// String provides a string representation
func (r Rectangle) String() string {
    return fmt.Sprintf("Rectangle{Width: %.2f, Height: %.2f}", r.Width, r.Height)
}

func main() {
    rect := Rectangle{Width: 5, Height: 10}
    
    fmt.Println(rect)
    fmt.Printf("Area: %.2f\n", rect.Area())
    fmt.Printf("Perimeter: %.2f\n", rect.Perimeter())
    
    // Scale up by 2x
    rect.Scale(2)
    fmt.Println("\nAfter scaling by 2:")
    fmt.Println(rect)
    fmt.Printf("Area: %.2f\n", rect.Area())
    fmt.Printf("Perimeter: %.2f\n", rect.Perimeter())
    
    // Bonus: Check if it's a square
    if rect.Width == rect.Height {
        fmt.Println("This is a square!")
    } else {
        fmt.Println("This is a rectangle!")
    }
}

Summary

  • Structs group related data into custom types
  • Methods add behavior to types via receivers
  • Value receivers work on copies (read-only)
  • Pointer receivers work on originals (can modify)
  • Embedding provides composition (not inheritance)
  • Anonymous structs useful for one-time use
  • Struct tags provide metadata for encoding/decoding
  • Use pointer receivers by default for consistency

What's Next?

Congratulations on completing the Data Structures module! You now understand arrays, slices, maps, and structs. Next, you'll learn about Pointersβ€”how to work with memory addresses and understand Go's memory model.