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:

  1. Function Fundamentals - Syntax, parameters, and return values
  2. Multiple Return Values - Go’s unique approach to returning data
  3. Variadic Functions - Handling variable numbers of parameters
  4. Methods on Types - Attaching behavior to custom types
  5. Function Types - Functions as first-class citizens
  6. Closures and Scope - Advanced function concepts
  7. Error Handling Patterns - Using functions for robust error management
  8. 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:

  1. Simplicity: Clear, straightforward syntax
  2. Multiple returns: Return multiple values naturally
  3. Explicit error handling: Errors are values, not exceptions
  4. 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

  1. Misconception: Go methods are like class methods in OOP languages Reality: Go methods are functions with receivers, not part of a class hierarchy

  2. 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:

  1. Create functions for basic arithmetic operations
  2. Add a function that takes an operation string and two numbers
  3. Implement proper error handling for division by zero
  4. 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 Text type
  • Implement methods for common string operations
  • Add validation and transformation functions
  • Use function types for custom processing

Instructions:

  1. Define a Text type based on string
  2. Create methods for: word count, character count, uppercase, lowercase
  3. Add validation methods (email, phone number)
  4. 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:

  • Account struct 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

  1. 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

  2. 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

  3. 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

  1. Multiple Returns: Go’s multiple return values enable elegant error handling
  2. Methods vs Functions: Methods provide behavior for types; functions are standalone
  3. Function Types: Functions are first-class citizens in Go
  4. Error Handling: Functions should return errors as the last return value

Quick Review Questions

  1. What’s the difference between a value receiver and a pointer receiver?
  2. How do you create a function that accepts variable arguments?
  3. What’s the conventional way to handle errors in Go functions?
  4. How do closures capture variables from their surrounding scope?

Answers:

  1. Value receivers work on copies; pointer receivers work on originals
  2. Use the ... syntax: func sum(numbers ...int) int
  3. Return error as the last return value and check it at call site
  4. 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

  1. “When should I use a method vs a function?” - Use methods when behavior belongs to a type
  2. “Why doesn’t Go have function overloading?” - Simplicity and explicitness
  3. “How do I know when to use pointer receivers?” - When you need to modify or for large structs
  4. “Can I have optional parameters?” - No, but you can use options patterns or variadic functions