Structs & Methods
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:
Click Run to execute your code
- 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
Methods
Methods are functions with a special receiver argument that associates them with a type:
Click Run to execute your code
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:
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 |
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:
Click Run to execute your code
- 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.
Enjoying these tutorials?