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.