Go Functions and Methods - Building Reusable Code
Learning Objectives: By the end of this lesson, you will be able to:
- Define and call functions with various parameter patterns
- Master Go’s unique multiple return values feature
- Use variadic functions for flexible parameter handling
- Understand and implement methods on custom types
- Apply function types and first-class function concepts
- Create closures and understand their practical applications
- Design clean, maintainable function APIs
- Handle errors effectively using Go’s function patterns
1. Welcome and Introduction (5 minutes)
Icebreaker Activity
Imagine you’re a chef in a busy restaurant. Instead of writing out the full recipe every time you need to make a dish, you create standardized procedures that any cook can follow. Functions in programming work the same way - they’re reusable “recipes” that take ingredients (parameters) and produce a result. What would happen to your code if you had to copy and paste the same logic everywhere?
Lesson Overview
- Duration: 6-8 hours
- Prerequisites: Go syntax, data types, and control structures
- Materials Needed: Go development environment, understanding of basic programming concepts
2. Learning Objectives and Session Outline (5 minutes)
What We’ll Cover Today:
- Function Fundamentals - Syntax, parameters, and return values
- Multiple Return Values - Go’s unique approach to returning data
- Variadic Functions - Handling variable numbers of parameters
- Methods on Types - Attaching behavior to custom types
- Function Types - Functions as first-class citizens
- Closures and Scope - Advanced function concepts
- Error Handling Patterns - Using functions for robust error management
- Best Practices - Writing clean, maintainable functions
Success Criteria:
By the end of this session, you should be able to design and implement well-structured functions, understand Go’s method system, and apply functional programming concepts effectively.
3. Foundation Knowledge (15-20 minutes)
Understanding Functions in Go
Functions are the primary way to organize and reuse code in Go. They encapsulate logic, make code more readable, and enable modular design.
Real-World Analogy: Think of functions like specialized tools in a workshop. A hammer has a specific purpose (hitting nails), takes input (your arm motion), and produces output (drives the nail). Similarly, functions have specific purposes, take parameters as input, and return results.
Why This Matters: Functions enable:
- Code Reusability: Write once, use many times
- Modularity: Break complex problems into smaller pieces
- Testing: Isolated units that can be tested independently
- Collaboration: Team members can work on different functions
- Maintainability: Changes in one place affect the whole system
Core Concepts
Go’s Function Philosophy
Go functions follow these principles:
- Simplicity: Clear, straightforward syntax
- Multiple returns: Return multiple values naturally
- Explicit error handling: Errors are values, not exceptions
- First-class functions: Functions can be assigned to variables and passed around
Function vs Method
- Function: Standalone piece of code
- Method: Function associated with a specific type
Value vs Pointer Receivers
When defining methods, you can use:
- Value receivers: Work with copies of the data
- Pointer receivers: Work with the original data
Key Terminology
- Parameter: Variable in function definition
- Argument: Actual value passed to function
- Return value: Data sent back from function
- Signature: Function’s name, parameters, and return types
- Receiver: The type a method is attached to
- Variadic: Function that accepts variable number of arguments
- Closure: Function that captures variables from its surrounding scope
4. Detailed Explanation and Examples (25-30 minutes)
Understanding Go’s Unique Function Features
Go’s functions have several unique characteristics that distinguish them from other languages:
Multiple Return Values
Unlike many languages that return only one value, Go functions can return multiple values. This eliminates the need for output parameters or complex return objects.
Benefits:
- Error handling: Return both result and error
- Multiple outputs: Return related values together
- Cleaner APIs: No need for complex return structures
Named Return Values
Go allows you to name return values, which can make functions more self-documenting and enable early returns.
Practical Applications
Functions and methods are essential in:
- Web development: Handler functions for HTTP requests
- Data processing: Transform and validate data
- API design: Create clean, reusable interfaces
- Testing: Isolate functionality for unit tests
- Microservices: Encapsulate business logic
Common Misconceptions
-
Misconception: Go methods are like class methods in OOP languages Reality: Go methods are functions with receivers, not part of a class hierarchy
-
Misconception: Multiple return values are inefficient Reality: Go optimizes multiple returns at the compiler level
5. Live Coding Demonstration (20-25 minutes)
Example 1: Basic Functions and Multiple Returns
package main
import (
"errors"
"fmt"
"math"
"strings"
)
// Basic function with single return
func greet(name string) string {
return "Hello, " + name + "!"
}
// Function with multiple parameters
func add(a, b int) int {
return a + b
}
// Multiple return values
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Named return values
func calculate(a, b int) (sum, product int) {
sum = a + b
product = a * b
return // Naked return uses named values
}
// Function with multiple return types
func analyzeString(s string) (length int, words int, isEmpty bool) {
length = len(s)
isEmpty = strings.TrimSpace(s) == ""
if isEmpty {
words = 0
} else {
words = len(strings.Fields(s))
}
return
}
// Function that returns a function
func makeMultiplier(factor int) func(int) int {
return func(x int) int {
return x * factor
}
}
func main() {
// Basic function calls
message := greet("World")
fmt.Println(message)
sum := add(5, 3)
fmt.Printf("5 + 3 = %d\n", sum)
// Multiple return values
result, err := divide(10, 3)
if err != nil {
fmt.Printf("Error: %s\n", err)
} else {
fmt.Printf("10 / 3 = %.2f\n", result)
}
// Error case
_, err = divide(10, 0)
if err != nil {
fmt.Printf("Error: %s\n", err)
}
// Named returns
s, p := calculate(4, 5)
fmt.Printf("Sum: %d, Product: %d\n", s, p)
// Multiple returns with different types
length, words, empty := analyzeString("Hello, wonderful world!")
fmt.Printf("Text analysis: %d characters, %d words, empty: %t\n",
length, words, empty)
// Function as return value
double := makeMultiplier(2)
triple := makeMultiplier(3)
fmt.Printf("Double 5: %d\n", double(5))
fmt.Printf("Triple 5: %d\n", triple(5))
}
Explanation:
- Lines 9-11: Basic function with string parameter and return
- Lines 18-23: Function demonstrating error handling pattern
- Lines 26-30: Named return values with naked return
- Lines 33-42: Multiple return types in one function
- Lines 45-49: Higher-order function returning another function
Example 2: Variadic Functions and Function Types
package main
import (
"fmt"
"strings"
)
// Variadic function - accepts variable number of arguments
func sum(numbers ...int) int {
total := 0
for _, num := range numbers {
total += num
}
return total
}
// Variadic with mixed parameters
func formatMessage(template string, values ...interface{}) string {
return fmt.Sprintf(template, values...)
}
// Function type definition
type MathOperation func(int, int) int
// Functions that match the MathOperation type
func multiply(a, b int) int {
return a * b
}
func subtract(a, b int) int {
return a - b
}
// Function that takes another function as parameter
func applyOperation(a, b int, op MathOperation) int {
return op(a, b)
}
// Function that returns different functions based on input
func getOperation(operation string) MathOperation {
switch operation {
case "multiply":
return multiply
case "subtract":
return subtract
case "add":
return func(a, b int) int { return a + b } // Anonymous function
default:
return func(a, b int) int { return 0 }
}
}
// Closure example
func makeCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
// Advanced closure with configuration
func makeValidator(minLength int, maxLength int) func(string) bool {
return func(input string) bool {
length := len(strings.TrimSpace(input))
return length >= minLength && length <= maxLength
}
}
func main() {
// Variadic function calls
fmt.Printf("Sum of no numbers: %d\n", sum())
fmt.Printf("Sum of 1, 2, 3: %d\n", sum(1, 2, 3))
fmt.Printf("Sum of 1 through 10: %d\n", sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
// Spread operator with slices
numbers := []int{10, 20, 30}
fmt.Printf("Sum of slice: %d\n", sum(numbers...))
// Variadic with mixed types
message := formatMessage("Hello %s, you are %d years old", "Alice", 25)
fmt.Println(message)
// Function types and higher-order functions
result1 := applyOperation(10, 5, multiply)
result2 := applyOperation(10, 5, subtract)
fmt.Printf("10 * 5 = %d\n", result1)
fmt.Printf("10 - 5 = %d\n", result2)
// Dynamic function selection
addFunc := getOperation("add")
mulFunc := getOperation("multiply")
fmt.Printf("Dynamic add: %d\n", addFunc(7, 3))
fmt.Printf("Dynamic multiply: %d\n", mulFunc(7, 3))
// Closures
counter1 := makeCounter()
counter2 := makeCounter()
fmt.Printf("Counter1: %d\n", counter1()) // 1
fmt.Printf("Counter1: %d\n", counter1()) // 2
fmt.Printf("Counter2: %d\n", counter2()) // 1 (independent)
fmt.Printf("Counter1: %d\n", counter1()) // 3
// Closure with parameters
usernameValidator := makeValidator(3, 20)
passwordValidator := makeValidator(8, 50)
fmt.Printf("Username 'jo' valid: %t\n", usernameValidator("jo")) // false
fmt.Printf("Username 'john' valid: %t\n", usernameValidator("john")) // true
fmt.Printf("Password '123' valid: %t\n", passwordValidator("123")) // false
fmt.Printf("Password 'secret123' valid: %t\n", passwordValidator("secret123")) // true
}
Example 3: Methods on Custom Types
package main
import (
"fmt"
"math"
)
// Custom types
type Rectangle struct {
Width float64
Height float64
}
type Circle struct {
Radius float64
}
type Point struct {
X, Y float64
}
// Methods with value receivers
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Circumference() float64 {
return 2 * math.Pi * c.Radius
}
// Method with pointer receiver (can modify the receiver)
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
func (p *Point) Move(dx, dy float64) {
p.X += dx
p.Y += dy
}
// Method that returns multiple values
func (r Rectangle) Dimensions() (width, height float64) {
return r.Width, r.Height
}
// Method with parameters
func (r Rectangle) FitsInside(container Rectangle) bool {
return r.Width <= container.Width && r.Height <= container.Height
}
// Interface to demonstrate polymorphism
type Shape interface {
Area() float64
}
// Function that works with any Shape
func printShapeInfo(s Shape) {
fmt.Printf("Shape area: %.2f\n", s.Area())
}
// Method on built-in type (using type alias)
type Temperature float64
func (t Temperature) Celsius() float64 {
return float64(t)
}
func (t Temperature) Fahrenheit() float64 {
return float64(t)*9/5 + 32
}
func (t Temperature) Kelvin() float64 {
return float64(t) + 273.15
}
func main() {
// Rectangle methods
rect := Rectangle{Width: 10, Height: 5}
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())
// Method with multiple returns
w, h := rect.Dimensions()
fmt.Printf("Dimensions: %.1f x %.1f\n", w, h)
// Method with parameters
container := Rectangle{Width: 15, Height: 10}
fmt.Printf("Fits inside container: %t\n", rect.FitsInside(container))
// Pointer receiver method (modifies original)
fmt.Printf("Before scaling: %.1f x %.1f\n", rect.Width, rect.Height)
rect.Scale(2.0)
fmt.Printf("After scaling by 2: %.1f x %.1f\n", rect.Width, rect.Height)
// Circle methods
circle := Circle{Radius: 5}
fmt.Printf("\nCircle with radius %.1f:\n", circle.Radius)
fmt.Printf("Area: %.2f\n", circle.Area())
fmt.Printf("Circumference: %.2f\n", circle.Circumference())
// Interface usage (polymorphism)
fmt.Println("\nUsing interface:")
printShapeInfo(rect)
printShapeInfo(circle)
// Point movement
point := Point{X: 1, Y: 2}
fmt.Printf("\nPoint: (%.1f, %.1f)\n", point.X, point.Y)
point.Move(3, 4)
fmt.Printf("After moving: (%.1f, %.1f)\n", point.X, point.Y)
// Methods on custom type based on built-in type
temp := Temperature(25)
fmt.Printf("\nTemperature conversions:\n")
fmt.Printf("%.1f°C = %.1f°F = %.1fK\n",
temp.Celsius(), temp.Fahrenheit(), temp.Kelvin())
}
Example 4: Error Handling Patterns with Functions
package main
import (
"errors"
"fmt"
"strconv"
"strings"
)
// Custom error type
type ValidationError struct {
Field string
Message string
}
func (ve ValidationError) Error() string {
return fmt.Sprintf("validation error in field '%s': %s", ve.Field, ve.Message)
}
// User type for demonstration
type User struct {
Name string
Email string
Age int
}
// Function with comprehensive error handling
func parseAge(ageStr string) (int, error) {
if strings.TrimSpace(ageStr) == "" {
return 0, ValidationError{Field: "age", Message: "cannot be empty"}
}
age, err := strconv.Atoi(ageStr)
if err != nil {
return 0, ValidationError{Field: "age", Message: "must be a valid number"}
}
if age < 0 || age > 150 {
return 0, ValidationError{Field: "age", Message: "must be between 0 and 150"}
}
return age, nil
}
// Function with multiple validation steps
func validateUser(name, email, ageStr string) (*User, error) {
// Validate name
if strings.TrimSpace(name) == "" {
return nil, ValidationError{Field: "name", Message: "cannot be empty"}
}
// Validate email (simple check)
if !strings.Contains(email, "@") || !strings.Contains(email, ".") {
return nil, ValidationError{Field: "email", Message: "invalid format"}
}
// Validate age using helper function
age, err := parseAge(ageStr)
if err != nil {
return nil, err
}
user := &User{
Name: strings.TrimSpace(name),
Email: strings.ToLower(strings.TrimSpace(email)),
Age: age,
}
return user, nil
}
// Function that demonstrates error wrapping
func processUserData(data map[string]string) (*User, error) {
name, exists := data["name"]
if !exists {
return nil, errors.New("missing required field: name")
}
email, exists := data["email"]
if !exists {
return nil, errors.New("missing required field: email")
}
ageStr, exists := data["age"]
if !exists {
return nil, errors.New("missing required field: age")
}
user, err := validateUser(name, email, ageStr)
if err != nil {
return nil, fmt.Errorf("failed to create user: %w", err)
}
return user, nil
}
// Function with optional parameters using options pattern
type UserOptions struct {
ValidateEmail bool
MinAge int
MaxAge int
}
func createUserWithOptions(name, email string, age int, opts *UserOptions) (*User, error) {
// Default options
if opts == nil {
opts = &UserOptions{
ValidateEmail: true,
MinAge: 0,
MaxAge: 150,
}
}
// Validate based on options
if opts.ValidateEmail && (!strings.Contains(email, "@") || !strings.Contains(email, ".")) {
return nil, ValidationError{Field: "email", Message: "invalid format"}
}
if age < opts.MinAge || age > opts.MaxAge {
return nil, ValidationError{
Field: "age",
Message: fmt.Sprintf("must be between %d and %d", opts.MinAge, opts.MaxAge),
}
}
return &User{Name: name, Email: email, Age: age}, nil
}
func main() {
fmt.Println("=== Error Handling Examples ===")
// Valid user creation
user1, err := validateUser("John Doe", "john@example.com", "25")
if err != nil {
fmt.Printf("Error: %s\n", err)
} else {
fmt.Printf("Created user: %+v\n", user1)
}
// Invalid age
_, err = validateUser("Jane Doe", "jane@example.com", "abc")
if err != nil {
fmt.Printf("Error: %s\n", err)
// Type assertion to get specific error details
if ve, ok := err.(ValidationError); ok {
fmt.Printf("Field with error: %s\n", ve.Field)
}
}
// Using map-based data processing
fmt.Println("\n=== Map-based Processing ===")
userData := map[string]string{
"name": "Alice Smith",
"email": "alice@company.com",
"age": "30",
}
user2, err := processUserData(userData)
if err != nil {
fmt.Printf("Error: %s\n", err)
} else {
fmt.Printf("Processed user: %+v\n", user2)
}
// Missing field
invalidData := map[string]string{
"name": "Bob",
"email": "bob@test.com",
// Missing age
}
_, err = processUserData(invalidData)
if err != nil {
fmt.Printf("Error: %s\n", err)
}
// Options pattern
fmt.Println("\n=== Options Pattern ===")
opts := &UserOptions{
ValidateEmail: false,
MinAge: 18,
MaxAge: 65,
}
user3, err := createUserWithOptions("Charlie", "charlie@invalid", 25, opts)
if err != nil {
fmt.Printf("Error: %s\n", err)
} else {
fmt.Printf("Created user with options: %+v\n", user3)
}
}
Debugging Demonstration
Show common function-related errors and solutions:
Problem: Modifying value receivers
// This won't work as expected
func (r Rectangle) BadScale(factor float64) {
r.Width *= factor // Modifies copy, not original
r.Height *= factor
}
// Correct approach
func (r *Rectangle) GoodScale(factor float64) {
r.Width *= factor // Modifies original through pointer
r.Height *= factor
}
Problem: Forgetting to handle errors
// Dangerous - ignoring errors
result, _ := divide(10, 0) // Could panic or give unexpected results
// Better approach
result, err := divide(10, 0)
if err != nil {
log.Printf("Division error: %s", err)
return
}
6. Hands-On Practice (30-40 minutes)
Exercise 1: Calculator Functions (10 minutes)
Objective: Build a comprehensive calculator using functions with proper error handling
Instructions:
- Create functions for basic arithmetic operations
- Add a function that takes an operation string and two numbers
- Implement proper error handling for division by zero
- Create a variadic function for multiple number operations
Starter Code:
package main
import "fmt"
// TODO: Implement basic arithmetic functions
// TODO: Add error handling for division by zero
// TODO: Create a calculator function that takes operation as string
// TODO: Implement variadic sum and product functions
func main() {
// Test your functions here
}
Solution:
package main
import (
"errors"
"fmt"
)
// Basic arithmetic functions with error handling
func add(a, b float64) float64 {
return a + b
}
func subtract(a, b float64) float64 {
return a - b
}
func multiply(a, b float64) float64 {
return a * b
}
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Calculator function that takes operation as string
func calculate(operation string, a, b float64) (float64, error) {
switch operation {
case "+", "add":
return add(a, b), nil
case "-", "subtract":
return subtract(a, b), nil
case "*", "multiply":
return multiply(a, b), nil
case "/", "divide":
return divide(a, b)
default:
return 0, fmt.Errorf("unknown operation: %s", operation)
}
}
// Variadic functions
func sum(numbers ...float64) float64 {
total := 0.0
for _, num := range numbers {
total += num
}
return total
}
func product(numbers ...float64) float64 {
if len(numbers) == 0 {
return 0
}
result := 1.0
for _, num := range numbers {
result *= num
}
return result
}
func main() {
// Test basic operations
fmt.Println("=== Basic Operations ===")
fmt.Printf("5 + 3 = %.2f\n", add(5, 3))
fmt.Printf("5 - 3 = %.2f\n", subtract(5, 3))
fmt.Printf("5 * 3 = %.2f\n", multiply(5, 3))
if result, err := divide(5, 3); err != nil {
fmt.Printf("Error: %s\n", err)
} else {
fmt.Printf("5 / 3 = %.2f\n", result)
}
// Test calculator function
fmt.Println("\n=== Calculator Function ===")
if result, err := calculate("+", 10, 5); err != nil {
fmt.Printf("Error: %s\n", err)
} else {
fmt.Printf("10 + 5 = %.2f\n", result)
}
// Test error cases
if _, err := calculate("/", 10, 0); err != nil {
fmt.Printf("Error: %s\n", err)
}
// Test variadic functions
fmt.Println("\n=== Variadic Functions ===")
fmt.Printf("Sum of 1,2,3,4,5: %.2f\n", sum(1, 2, 3, 4, 5))
fmt.Printf("Product of 2,3,4: %.2f\n", product(2, 3, 4))
}
Exercise 2: String Processing Library (15 minutes)
Objective: Create a library of string processing functions with methods
Requirements:
- Create a custom
Texttype - Implement methods for common string operations
- Add validation and transformation functions
- Use function types for custom processing
Instructions:
- Define a
Texttype based on string - Create methods for: word count, character count, uppercase, lowercase
- Add validation methods (email, phone number)
- Implement a transformation method that takes a function parameter
Hints:
- Use pointer receivers for methods that modify the text
- Return multiple values for validation (bool, error)
- Consider using closures for configurable validators
Exercise 3: Bank Account System (15 minutes)
Objective: Design a bank account system using methods and proper error handling
Challenge: Create a realistic banking system that demonstrates all function concepts
Requirements:
Accountstruct with balance and transaction history- Methods for deposit, withdraw, transfer
- Proper error handling for insufficient funds
- Transaction logging with functions
- Interest calculation with different strategies
Extension Ideas:
- Add account types with different behaviors
- Implement overdraft protection
- Create a transaction processor that takes function parameters
- Add account validation and creation functions
7. Common Pitfalls and Troubleshooting (10 minutes)
Frequent Mistakes
-
Problem: Using value receivers when you need to modify data Cause: Misunderstanding Go’s value vs reference semantics Solution: Use pointer receivers for methods that modify the receiver
-
Problem: Not handling multiple return values properly Cause: Ignoring error returns or not checking all return values Solution: Always handle errors and use blank identifier explicitly if ignoring
-
Problem: Closure variable capture issues Cause: Not understanding how closures capture variables Solution: Be careful with loop variables in closures; use parameters when needed
Debugging Tips
- Print function signatures to understand what’s expected
- Use named return values temporarily for debugging
- Test functions in isolation before integrating
- Check receiver types (value vs pointer) if methods aren’t working
Function Design Best Practices
- Single Responsibility: Each function should do one thing well
- Error Handling: Always return errors as the last return value
- Naming: Use descriptive names that indicate what the function does
- Parameters: Limit the number of parameters; use structs for complex inputs
8. Best Practices and Professional Tips (5 minutes)
Industry Standards
- Function Length: Keep functions short and focused (typically < 50 lines)
- Error Handling: Never ignore errors; always handle or explicitly ignore
- Documentation: Add comments for exported functions
- Testing: Write unit tests for all public functions
Code Organization
// Group related functions together
type UserService struct {
db *Database
}
func (us *UserService) CreateUser(user *User) error { /* ... */ }
func (us *UserService) GetUser(id string) (*User, error) { /* ... */ }
func (us *UserService) UpdateUser(user *User) error { /* ... */ }
Performance Considerations
- Pointer vs Value Receivers: Use pointers for large structs
- Function Calls: Inline simple operations when performance critical
- Memory Allocation: Be aware of closure captures
Next Steps in Your Learning Journey
- Interfaces: Learn Go’s implicit interface system
- Packages: Organize functions into reusable packages
- Concurrency: Apply functions with goroutines and channels
- Testing: Write comprehensive tests for your functions
9. Summary and Review (5 minutes)
Key Takeaways
- Multiple Returns: Go’s multiple return values enable elegant error handling
- Methods vs Functions: Methods provide behavior for types; functions are standalone
- Function Types: Functions are first-class citizens in Go
- Error Handling: Functions should return errors as the last return value
Quick Review Questions
- What’s the difference between a value receiver and a pointer receiver?
- How do you create a function that accepts variable arguments?
- What’s the conventional way to handle errors in Go functions?
- How do closures capture variables from their surrounding scope?
Answers:
- Value receivers work on copies; pointer receivers work on originals
- Use the
...syntax:func sum(numbers ...int) int - Return error as the last return value and check it at call site
- Closures capture variables by reference, sharing the actual variable
For Instructors
Teaching Tips
- Start with simple functions before introducing multiple returns
- Demonstrate debugging by showing function call traces
- Use real-world examples to show when to use functions vs methods
- Emphasize error handling early and consistently
Assessment Ideas
- Function Design: Have students design functions for given requirements
- Error Handling: Present scenarios requiring proper error handling
- Method Implementation: Create types and have students add appropriate methods
- Refactoring: Give repetitive code and ask students to extract functions
Adaptation Notes
- Beginner Focus: Spend more time on basic function syntax and single returns
- Advanced Students: Explore function performance and assembly output
- Visual Learners: Use diagrams to show function call stacks and memory
- Time Management: Methods can be confusing initially - allow extra time
Common Student Questions
- “When should I use a method vs a function?” - Use methods when behavior belongs to a type
- “Why doesn’t Go have function overloading?” - Simplicity and explicitness
- “How do I know when to use pointer receivers?” - When you need to modify or for large structs
- “Can I have optional parameters?” - No, but you can use options patterns or variadic functions