Variables and Types

Why Type Systems Matter

When building a financial application that processes millions of transactions daily, a single type error could cost your company millions. Type-related bugs have caused real-world financial losses, system crashes, and security vulnerabilities.

The Type Safety Advantage:
Think of Go's type system like having a meticulous assistant who checks every calculation before execution. Unlike dynamically typed languages where 100 + "50" might silently become "10050", Go forces you to be explicit about your intentions, preventing entire classes of runtime errors.

Real-World Impact:

  • Aerospace: NASA's Mars Climate Orbiter failed due to mixing metric and imperial units—a type conversion error that cost $327 million
  • Finance: Knight Capital lost $465 million in 45 minutes due to configuration flag type errors that weren't caught
  • Healthcare: Type conversion errors in medical devices have caused incorrect dosage calculations leading to patient harm
  • Infrastructure: Cloudflare's 2020 outage was partly due to regex parsing type mismatches

Go's static typing prevents these disasters by making type mismatches compile-time errors, not runtime surprises.

Learning Objectives

By the end of this article, you will:

  • ✅ Understand Go's type system and why static typing prevents bugs
  • ✅ Master variable declaration patterns (var, :=, package-level)
  • ✅ Recognize and avoid common pitfalls like variable shadowing
  • ✅ Perform safe type conversions with proper error handling
  • ✅ Choose appropriate types for optimal memory usage and performance
  • ✅ Apply type safety principles to build robust, maintainable code
  • ✅ Understand zero values and leverage them effectively

Understanding Go's Type System

The Philosophy: Explicit Over Implicit

Go follows a simple principle: be explicit about your intentions. Unlike languages that guess what you mean, Go requires you to state clearly what types you're working with.

1// Go: Type is explicit, compiler verifies correctness
2var age int = 25
3var price float64 = 19.99
4var isActive bool = true
5
6// Python: Types are implicit, errors surface at runtime
7age = 25
8price = 19.99
9is_active = True

Why this matters: Explicitness makes code self-documenting and catches errors early. When you read var timeout int = 30, you immediately know you're working with an integer representing seconds or milliseconds, not a floating-point value.

The Type Hierarchy

Go organizes types into clear categories:

  1. Basic Types: Numbers, strings, booleans
  2. Composite Types: Arrays, slices, maps, structs, pointers
  3. Interface Types: Abstract behavior definitions
  4. Function Types: Signatures for first-class functions
  5. Channel Types: Communication primitives for concurrency

Let's start with the fundamentals and build up to advanced patterns.

Basic Types Deep Dive

Numeric Types: Choosing the Right Tool

Go provides multiple numeric types, each with specific use cases:

Integer Types:

  • int8, int16, int32, int64: Signed integers of specific bit sizes
  • uint8 (byte), uint16, uint32, uint64: Unsigned integers
  • int, uint: Platform-dependent (32-bit or 64-bit)
  • uintptr: For storing pointer values (advanced use)

Floating-Point Types:

  • float32: 32-bit IEEE-754 floating-point
  • float64: 64-bit IEEE-754 floating-point (default)

Complex Types:

  • complex64: Complex numbers with float32 real/imaginary parts
  • complex128: Complex numbers with float64 real/imaginary parts
 1// run
 2package main
 3
 4import "fmt"
 5
 6func demonstrateNumericTypes() {
 7    // Integer types with specific ranges
 8    var age uint8 = 25              // 0-255, perfect for age
 9    var population int32 = 8_000_000_000  // ~8 billion people
10    var atomicNumber uint8 = 92     // Uranium, 0-118 elements
11
12    // Floating-point precision
13    var pi32 float32 = 3.14159265358979323846
14    var pi64 float64 = 3.14159265358979323846
15
16    // Complex numbers for scientific computing
17    var signal complex128 = complex(3.0, 4.0)  // 3+4i
18
19    fmt.Printf("Age: %d (uint8)\n", age)
20    fmt.Printf("Population: %d (int32)\n", population)
21    fmt.Printf("Atomic number: %d (uint8)\n", atomicNumber)
22    fmt.Printf("Pi as float32: %.20f\n", pi32)
23    fmt.Printf("Pi as float64: %.20f\n", pi64)
24    fmt.Printf("Complex signal: %v, magnitude: %.2f\n", signal,
25        real(signal)*real(signal)+imag(signal)*imag(signal))
26}
27
28func main() {
29    fmt.Println("=== Numeric Types Demonstration ===")
30    demonstrateNumericTypes()
31}

Key insight: Notice how float32 loses precision compared to float64. Choose float32 only when memory is constrained and precision loss is acceptable.

String Types: More Than Text

In Go, strings are immutable byte slices encoded in UTF-8:

 1// run
 2package main
 3
 4import (
 5    "fmt"
 6    "unicode/utf8"
 7)
 8
 9func demonstrateStrings() {
10    // String literals
11    regularString := "Hello, 世界"  // UTF-8 encoded
12    rawString := `Line 1
13Line 2
14Line 3`  // Raw string, no escape sequences
15
16    // String properties
17    fmt.Printf("String: %s\n", regularString)
18    fmt.Printf("Byte length: %d\n", len(regularString))
19    fmt.Printf("Rune count: %d\n", utf8.RuneCountInString(regularString))
20
21    // String iteration: bytes vs runes
22    fmt.Println("\nIterating bytes:")
23    for i := 0; i < len(regularString); i++ {
24        fmt.Printf("  [%d] = %x\n", i, regularString[i])
25    }
26
27    fmt.Println("\nIterating runes (characters):")
28    for i, r := range regularString {
29        fmt.Printf("  [%d] = %c (U+%04X)\n", i, r, r)
30    }
31
32    // Raw strings preserve formatting
33    fmt.Printf("\nRaw string:\n%s\n", rawString)
34
35    // String concatenation
36    greeting := "Hello"
37    name := "Gopher"
38    message := greeting + ", " + name + "!"  // Creates new string
39    fmt.Printf("\nConcatenated: %s\n", message)
40
41    // Strings are immutable
42    // regularString[0] = 'h'  // ❌ Compile error: cannot assign
43}
44
45func main() {
46    fmt.Println("=== String Types Demonstration ===")
47    demonstrateStrings()
48}

Important: Strings are immutable. Every concatenation creates a new string. For building strings efficiently, use strings.Builder.

Boolean Types: Clarity and Safety

Go's boolean type is simple but powerful:

 1// run
 2package main
 3
 4import "fmt"
 5
 6func demonstrateBooleans() {
 7    // Boolean literals
 8    var isActive bool = true
 9    var hasPermission bool = false
10    var isReady bool  // Zero value: false
11
12    // Boolean operations
13    canProceed := isActive && hasPermission
14    shouldWait := !isReady
15    needsAction := isActive || isReady
16
17    fmt.Printf("isActive: %v\n", isActive)
18    fmt.Printf("hasPermission: %v\n", hasPermission)
19    fmt.Printf("isReady: %v (zero value)\n", isReady)
20    fmt.Printf("canProceed: %v\n", canProceed)
21    fmt.Printf("shouldWait: %v\n", shouldWait)
22    fmt.Printf("needsAction: %v\n", needsAction)
23
24    // No truthy/falsy values like in JavaScript or Python
25    // var count int = 0
26    // if count { }  // ❌ Compile error: count is not bool
27
28    // Must be explicit
29    var count int = 0
30    if count > 0 {  // ✅ Explicit comparison
31        fmt.Println("Count is positive")
32    } else {
33        fmt.Println("Count is zero or negative")
34    }
35}
36
37func main() {
38    fmt.Println("=== Boolean Types Demonstration ===")
39    demonstrateBooleans()
40}

Key point: Go has no truthy/falsy values. You must use explicit boolean expressions, which prevents common bugs.

Mastering Variable Declarations

Example 1: Choosing the Right Declaration Method

 1// run
 2package main
 3
 4import "fmt"
 5
 6// Package-level variables (must use var)
 7var (
 8    AppName    = "MyApp"        // Type inferred as string
 9    Version    = "1.0.0"       // Global configuration
10    DebugMode  = false          // Feature flag
11    ServerPort = 8080          // Configuration
12)
13
14func main() {
15    // Local variables: prefer := for type inference
16    userCount := 1000              // Type inferred as int
17    serverURL := "localhost:8080"  // Type inferred as string
18
19    // When type clarity is important, use explicit var
20    var timeout int = 30           // Explicit int makes intention clear
21    var maxConnections int = 100   // Configuration parameter
22
23    // When you need a specific type (not default)
24    var requestID uint32 = 12345   // Force uint32 for specific range
25    var precision float32 = 0.95   // Force float32 for memory efficiency
26
27    // Declare multiple variables
28    var (
29        retryCount   int     = 3
30        retryDelay   int     = 1000
31        enableRetry  bool    = true
32    )
33
34    fmt.Printf("App: %s v%s (Debug: %v)\n", AppName, Version, DebugMode)
35    fmt.Printf("Server: %s, Port: %d, Users: %d\n", serverURL, ServerPort, userCount)
36    fmt.Printf("Config: timeout=%ds, maxConn=%d\n", timeout, maxConnections)
37    fmt.Printf("Request: ID=%d, Precision=%.2f\n", requestID, precision)
38    fmt.Printf("Retry: enabled=%v, count=%d, delay=%dms\n",
39        enableRetry, retryCount, retryDelay)
40}

What this demonstrates:

  • Package-level: Must use var keyword for global state
  • Local with obvious type: Use := for concise, idiomatic code
  • Type specification: Use var when you need specific type control
  • Multiple declarations: Group related variables for clarity
  • Initialization: All variables get appropriate initial values

Example 2: Zero Values - Go's Safety Net

 1// run
 2package main
 3
 4import "fmt"
 5
 6// Demonstration of all zero values
 7func demonstrateZeroValues() {
 8    // Basic types
 9    var i int                // 0
10    var f float64            // 0.0
11    var b bool               // false
12    var s string             // ""
13
14    // Composite types
15    var ptr *int             // nil
16    var slice []int          // nil
17    var m map[string]int     // nil
18    var ch chan int          // nil
19    var fn func()            // nil
20
21    fmt.Printf("Basic types:\n")
22    fmt.Printf("  int:     %d\n", i)
23    fmt.Printf("  float64: %.1f\n", f)
24    fmt.Printf("  bool:    %v\n", b)
25    fmt.Printf("  string:  %q\n", s)
26
27    fmt.Printf("\nComposite types:\n")
28    fmt.Printf("  *int:         %v\n", ptr)
29    fmt.Printf("  []int:        %v\n", slice)
30    fmt.Printf("  map:          %v\n", m)
31    fmt.Printf("  chan:         %v\n", ch)
32    fmt.Printf("  func:         %v\n", fn)
33
34    // Practical zero value usage
35    var counter int           // Starts at 0 - perfect for counting
36    var result string         // Starts empty - perfect for building
37    var isValid bool          // Starts false - safe default
38
39    counter++                 // Now 1
40    result += "Hello"         // Now "Hello"
41    isValid = true            // Now true
42
43    fmt.Printf("\nPractical usage:\n")
44    fmt.Printf("  counter: %d\n", counter)
45    fmt.Printf("  result:  %q\n", result)
46    fmt.Printf("  isValid: %v\n", isValid)
47
48    // Zero values enable clean patterns
49    var sum int  // No need to initialize to 0
50    numbers := []int{1, 2, 3, 4, 5}
51    for _, n := range numbers {
52        sum += n  // sum starts at 0 automatically
53    }
54    fmt.Printf("\nSum of %v = %d\n", numbers, sum)
55}
56
57func main() {
58    fmt.Println("=== Zero Values Demonstration ===")
59    demonstrateZeroValues()
60}

Key insights:

  • Safety: No uninitialized variables, no garbage values from memory
  • Predictability: Zero values are consistent and documented
  • Utility: Many zero values are immediately useful (0, false, "")
  • Nil safety: Reference types start as nil, preventing accidental dereference

Example 3: Type Conversion - The Safe Way

 1// run
 2package main
 3
 4import (
 5    "fmt"
 6    "math"
 7    "strconv"
 8)
 9
10func demonstrateTypeConversions() {
11    // Numeric conversions with bounds checking
12    var largeInt int64 = 1000000
13
14    fmt.Println("=== Unsafe vs Safe Conversion ===")
15
16    // Unsafe conversion (can overflow)
17    unsafeSmall := int8(largeInt)  // Overflow! 1000000 doesn't fit in int8
18    fmt.Printf("Unsafe: %d -> %d (WRONG!)\n", largeInt, unsafeSmall)
19
20    // Safe conversion with validation
21    if largeInt > math.MaxInt8 || largeInt < math.MinInt8 {
22        fmt.Printf("Safe: Cannot convert %d to int8 (range: %d to %d)\n",
23            largeInt, math.MinInt8, math.MaxInt8)
24    } else {
25        safeSmall := int8(largeInt)
26        fmt.Printf("Safe: %d -> %d\n", largeInt, safeSmall)
27    }
28
29    // String to number conversion with error handling
30    fmt.Println("\n=== String Conversions ===")
31    validInput := "123.45"
32    value, err := strconv.ParseFloat(validInput, 64)
33    if err != nil {
34        fmt.Printf("Error parsing '%s': %v\n", validInput, err)
35    } else {
36        fmt.Printf("Successfully parsed '%s' as %.2f\n", validInput, value)
37    }
38
39    // Invalid input handling
40    invalidInput := "not-a-number"
41    value2, err := strconv.ParseFloat(invalidInput, 64)
42    if err != nil {
43        fmt.Printf("Error parsing '%s': %v\n", invalidInput, err)
44    } else {
45        fmt.Printf("Parsed: %.2f\n", value2)
46    }
47
48    // Number to string conversion
49    fmt.Println("\n=== Number to String ===")
50    number := 42
51    asString := strconv.Itoa(number)  // Int to ASCII
52    asFloat := strconv.FormatFloat(3.14159, 'f', 2, 64)  // Float with precision
53    fmt.Printf("Integer %d -> '%s'\n", number, asString)
54    fmt.Printf("Float 3.14159 -> '%s' (2 decimals)\n", asFloat)
55
56    // Practical example: Currency conversion
57    fmt.Println("\n=== Currency Conversion ===")
58    dollars := 100.50
59    cents := int(math.Round(dollars * 100))  // Convert to cents safely
60    backToDollars := float64(cents) / 100.0
61    fmt.Printf("$%.2f = %d cents = $%.2f\n", dollars, cents, backToDollars)
62
63    // Precision matters
64    fmt.Println("\n=== Precision Matters ===")
65    var f32 float32 = 0.1 + 0.2
66    var f64 float64 = 0.1 + 0.2
67    fmt.Printf("float32: 0.1 + 0.2 = %.20f\n", f32)
68    fmt.Printf("float64: 0.1 + 0.2 = %.20f\n", f64)
69}
70
71func main() {
72    fmt.Println("=== Safe Type Conversions ===\n")
73    demonstrateTypeConversions()
74}

Best practices demonstrated:

  • Bounds checking: Validate before conversion to prevent overflow
  • Error handling: Always check conversion functions that can fail
  • Precision control: Use proper rounding for financial calculations
  • Explicitness: Make conversions obvious in code, never implicit

Example 4: Real-World Type Selection

 1// run
 2package main
 3
 4import (
 5    "fmt"
 6    "unsafe"
 7)
 8
 9// Efficient user structure - choose optimal types
10type EfficientUser struct {
11    ID       uint32     // 0 to 4.2 billion users is sufficient
12    Age      uint8      // Ages 0-255, saves memory vs int
13    IsActive bool       // true/false, perfect for bool
14    Score    float32    // Precision is sufficient, saves memory vs float64
15}
16
17// Inefficient structure - poor type choices
18type InefficientUser struct {
19    ID       uint64     // Overkill for user count
20    Age      float64    // Unnecessary decimal precision for age
21    IsActive string     // "true"/"false" vs bool wastes memory
22    Score    float64    // More precision than needed
23}
24
25func demonstrateTypeSelection() {
26    efficient := EfficientUser{
27        ID:       12345678,
28        Age:      25,
29        IsActive: true,
30        Score:    89.5,
31    }
32
33    inefficient := InefficientUser{
34        ID:       12345678,
35        Age:      25.0,
36        IsActive: "true",
37        Score:    89.5,
38    }
39
40    fmt.Printf("Efficient user: ID=%d, Age=%d, Active=%v, Score=%.1f\n",
41        efficient.ID, efficient.Age, efficient.IsActive, efficient.Score)
42
43    fmt.Printf("Inefficient user: ID=%d, Age=%.0f, Active=%s, Score=%.1f\n",
44        inefficient.ID, inefficient.Age, inefficient.IsActive, inefficient.Score)
45
46    // Memory comparison
47    efficientSize := unsafe.Sizeof(efficient)
48    inefficientSize := unsafe.Sizeof(inefficient)
49    savings := inefficientSize - efficientSize
50    savingsPercent := float64(savings) / float64(inefficientSize) * 100
51
52    fmt.Printf("\n=== Memory Analysis ===\n")
53    fmt.Printf("EfficientUser:   %d bytes\n", efficientSize)
54    fmt.Printf("InefficientUser: %d bytes\n", inefficientSize)
55    fmt.Printf("Savings per user: %d bytes (%.1f%%)\n", savings, savingsPercent)
56
57    // Real-world impact: 1 million users
58    million := 1_000_000
59    efficientMemory := uintptr(efficientSize) * uintptr(million)
60    inefficientMemory := uintptr(inefficientSize) * uintptr(million)
61    totalSavings := inefficientMemory - efficientMemory
62
63    fmt.Printf("\n=== Impact at Scale (1 million users) ===\n")
64    fmt.Printf("Efficient:   %.2f MB\n", float64(efficientMemory)/1024/1024)
65    fmt.Printf("Inefficient: %.2f MB\n", float64(inefficientMemory)/1024/1024)
66    fmt.Printf("Total saved: %.2f MB\n", float64(totalSavings)/1024/1024)
67    fmt.Printf("That's %.1f%% less memory!\n", savingsPercent)
68}
69
70func main() {
71    fmt.Println("=== Real-World Type Selection ===\n")
72    demonstrateTypeSelection()
73}

Real-world impact: Choosing optimal types can save significant memory and improve cache performance in large-scale applications. At scale, these savings translate to real infrastructure costs.

Constants and Iota

Understanding Constants

Constants are immutable values known at compile time:

 1// run
 2package main
 3
 4import (
 5    "fmt"
 6    "time"
 7)
 8
 9// Constants can be declared at package level
10const (
11    MaxConnections = 1000
12    DefaultTimeout = 30  // In seconds
13    APIVersion     = "v1.2.0"
14)
15
16// Typed vs untyped constants
17const (
18    TypedInt    int     = 42
19    UntypedInt          = 42  // Untyped, adapts to context
20    TypedFloat  float64 = 3.14
21    UntypedFloat        = 3.14
22)
23
24func demonstrateConstants() {
25    fmt.Printf("Configuration:\n")
26    fmt.Printf("  Max connections: %d\n", MaxConnections)
27    fmt.Printf("  Default timeout: %d seconds\n", DefaultTimeout)
28    fmt.Printf("  API version: %s\n", APIVersion)
29
30    // Untyped constants are flexible
31    var i32 int32 = UntypedInt    // Works
32    var i64 int64 = UntypedInt    // Works
33    var f32 float32 = UntypedFloat // Works
34    var f64 float64 = UntypedFloat // Works
35
36    fmt.Printf("\nUntyped constant flexibility:\n")
37    fmt.Printf("  int32:   %d\n", i32)
38    fmt.Printf("  int64:   %d\n", i64)
39    fmt.Printf("  float32: %.2f\n", f32)
40    fmt.Printf("  float64: %.2f\n", f64)
41
42    // Constants can be used in compile-time expressions
43    const HoursPerDay = 24
44    const MinutesPerHour = 60
45    const SecondsPerMinute = 60
46    const SecondsPerDay = HoursPerDay * MinutesPerHour * SecondsPerMinute
47
48    fmt.Printf("\nCompile-time calculations:\n")
49    fmt.Printf("  Seconds per day: %d\n", SecondsPerDay)
50
51    // Real-world usage: timeouts
52    timeout := DefaultTimeout * time.Second
53    fmt.Printf("\nTimeout duration: %v\n", timeout)
54}
55
56func main() {
57    fmt.Println("=== Constants Demonstration ===\n")
58    demonstrateConstants()
59}

Iota: Enumeration Generator

iota is a constant generator that starts at 0 and increments:

 1// run
 2package main
 3
 4import "fmt"
 5
 6// Simple enum
 7const (
 8    Sunday = iota  // 0
 9    Monday         // 1
10    Tuesday        // 2
11    Wednesday      // 3
12    Thursday       // 4
13    Friday         // 5
14    Saturday       // 6
15)
16
17// Skip values
18const (
19    _           = iota  // Skip 0
20    KB = 1 << (10 * iota)  // 1 << 10 = 1024
21    MB                      // 1 << 20 = 1048576
22    GB                      // 1 << 30 = 1073741824
23    TB                      // 1 << 40 = 1099511627776
24)
25
26// Flags using bit shifts
27const (
28    FlagRead = 1 << iota  // 1 << 0 = 1
29    FlagWrite             // 1 << 1 = 2
30    FlagExecute           // 1 << 2 = 4
31)
32
33// Status codes
34const (
35    StatusPending = iota
36    StatusActive
37    StatusInactive
38    StatusSuspended
39)
40
41func demonstrateIota() {
42    fmt.Println("=== Days of Week ===")
43    fmt.Printf("Sunday:    %d\n", Sunday)
44    fmt.Printf("Monday:    %d\n", Monday)
45    fmt.Printf("Wednesday: %d\n", Wednesday)
46    fmt.Printf("Saturday:  %d\n", Saturday)
47
48    fmt.Println("\n=== File Sizes ===")
49    fmt.Printf("1 KB = %d bytes\n", KB)
50    fmt.Printf("1 MB = %d bytes\n", MB)
51    fmt.Printf("1 GB = %d bytes\n", GB)
52    fmt.Printf("1 TB = %d bytes\n", TB)
53
54    fmt.Println("\n=== File Permissions ===")
55    fmt.Printf("Read:    %d (binary: %08b)\n", FlagRead, FlagRead)
56    fmt.Printf("Write:   %d (binary: %08b)\n", FlagWrite, FlagWrite)
57    fmt.Printf("Execute: %d (binary: %08b)\n", FlagExecute, FlagExecute)
58
59    // Combining flags
60    permissions := FlagRead | FlagWrite
61    fmt.Printf("Read+Write: %d (binary: %08b)\n", permissions, permissions)
62
63    // Checking flags
64    canRead := (permissions & FlagRead) != 0
65    canExecute := (permissions & FlagExecute) != 0
66    fmt.Printf("Can read: %v, Can execute: %v\n", canRead, canExecute)
67
68    fmt.Println("\n=== Status Codes ===")
69    statuses := []string{"Pending", "Active", "Inactive", "Suspended"}
70    for i, status := range statuses {
71        fmt.Printf("%s: %d\n", status, i)
72    }
73}
74
75func main() {
76    fmt.Println("=== Iota Demonstration ===\n")
77    demonstrateIota()
78}

Key patterns:

  • Simple enums: Sequential values starting from 0
  • Skipping values: Use _ to skip
  • Bit flags: Use bit shifts for combinations
  • Size constants: Powers of two for memory sizes

Common Patterns and Pitfalls

Pattern 1: The "Declare, Check, Use" Pattern

 1// Good: Initialize and check in one place
 2func processUserData(userID string) error {
 3    var user *User
 4    var err error
 5
 6    // Declare and initialize
 7    user, err = findUser(userID)
 8    if err != nil {
 9        return fmt.Errorf("finding user: %w", err)
10    }
11
12    // Use validated user
13    return processUser(user)
14}

Pattern 2: Constants for Configuration

 1// Good: Use constants for configuration values
 2const (
 3    MaxConnections = 1000
 4    DefaultTimeout = 30 * time.Second
 5    APIVersion     = "v1"
 6
 7    // Use iota for related constants
 8    StatusPending = iota
 9    StatusActive
10    StatusInactive
11    StatusSuspended
12)
13
14func getConnectionLimit() int {
15    return MaxConnections  // Single source of truth
16}

Pattern 3: Type Aliases for Clarity

1// Good: Type aliases make intent clear
2type UserID int64
3type Email string
4type Age uint8
5
6func createUser(id UserID, email Email, age Age) {
7    // Function signature is self-documenting
8}

Pitfall 1: Variable Shadowing

❌ WRONG: Creating new variable instead of using existing one

 1func processOrder() error {
 2    var err error  // Outer error variable
 3
 4    if true {
 5        err := doSomething()  // ❌ Creates NEW local variable!
 6        if err != nil {
 7            log.Println(err)  // Logs local variable
 8        }
 9        // Outer err remains unchanged!
10    }
11
12    return err  // Returns nil, not the actual error!
13}

✅ CORRECT: Use assignment to existing variable

 1func processOrder() error {
 2    var err error  // Outer error variable
 3
 4    if true {
 5        err := doSomething()  // Local variable, clear scope
 6        if err != nil {
 7            log.Println(err)
 8            return err  // Return error immediately
 9        }
10    }
11
12    // Or use the outer variable:
13    if true {
14        err = doSomething()  // ✅ Assign to existing variable (no colon)
15        if err != nil {
16            return err
17        }
18    }
19
20    return nil  // Success
21}

Pitfall 2: Type Conversion Without Validation

❌ WRONG: Assume conversion always succeeds

1func convertID(id string) int64 {
2    result, _ := strconv.ParseInt(id, 10, 64)  // ❌ Ignoring error!
3    return result  // Returns 0 if conversion fails!
4}

✅ CORRECT: Always handle conversion errors

1func convertID(id string) (int64, error) {
2    result, err := strconv.ParseInt(id, 10, 64)
3    if err != nil {
4        return 0, fmt.Errorf("invalid ID '%s': %w", id, err)
5    }
6    return result, nil
7}

Pitfall 3: Mixing Types in Operations

❌ WRONG: Assume automatic type promotion

1func calculateArea(radius int) float64 {
2    // ❌ Error: cannot multiply int by float64
3    return 3.14159 * radius * radius
4}

✅ CORRECT: Explicit type conversion

1func calculateArea(radius int) float64 {
2    r := float64(radius)  // ✅ Explicit conversion
3    return 3.14159 * r * r
4}

Pitfall 4: Using Wrong Integer Type

❌ WRONG: Using signed int for always-positive values

1func calculateAge(birthYear int) int {
2    currentYear := 2024
3    age := currentYear - birthYear  // Could be negative!
4    return age
5}

✅ CORRECT: Use unsigned or validate

 1func calculateAge(birthYear int) (uint8, error) {
 2    currentYear := 2024
 3    age := currentYear - birthYear
 4
 5    if age < 0 || age > 255 {
 6        return 0, fmt.Errorf("invalid age calculation: %d", age)
 7    }
 8
 9    return uint8(age), nil
10}

Building Type-Safe Configuration

Let's build a practical configuration system that demonstrates type safety, zero values, and proper error handling:

  1// run
  2package main
  3
  4import (
  5    "encoding/json"
  6    "fmt"
  7    "os"
  8    "time"
  9)
 10
 11// Configuration structure with optimal type selection
 12type Config struct {
 13    Server struct {
 14        Host         string        `json:"host"`
 15        Port         uint16        `json:"port"`        // 0-65535, sufficient for ports
 16        ReadTimeout  time.Duration `json:"read_timeout"` // Use time.Duration for time values
 17        WriteTimeout time.Duration `json:"write_timeout"`
 18    } `json:"server"`
 19
 20    Database struct {
 21        Type         string        `json:"type"`
 22        Host         string        `json:"host"`
 23        Port         uint16        `json:"port"`
 24        Name         string        `json:"name"`
 25        MaxOpenConns uint16        `json:"max_open_conns"` // 0-65535 connections
 26        ConnLifetime time.Duration `json:"conn_lifetime"`
 27    } `json:"database"`
 28
 29    Features struct {
 30        EnableCache   bool `json:"enable_cache"`
 31        EnableMetrics bool `json:"enable_metrics"`
 32        Debug         bool `json:"debug"`
 33    } `json:"features"`
 34
 35    // Performance tuning with optimal types
 36    Performance struct {
 37        WorkerCount uint8  `json:"worker_count"` // 0-255 workers is sufficient
 38        QueueSize   uint16 `json:"queue_size"`   // 0-65535 queue items
 39        BatchSize   uint16 `json:"batch_size"`   // 0-65535 items per batch
 40    } `json:"performance"`
 41}
 42
 43// Default configuration with proper zero values
 44func getDefaultConfig() *Config {
 45    config := &Config{}
 46
 47    // Server defaults
 48    config.Server.Host = "localhost"
 49    config.Server.Port = 8080
 50    config.Server.ReadTimeout = 30 * time.Second
 51    config.Server.WriteTimeout = 30 * time.Second
 52
 53    // Database defaults
 54    config.Database.Type = "postgres"
 55    config.Database.Host = "localhost"
 56    config.Database.Port = 5432
 57    config.Database.Name = "myapp"
 58    config.Database.MaxOpenConns = 100
 59    config.Database.ConnLifetime = time.Hour
 60
 61    // Feature defaults
 62    config.Features.EnableCache = true
 63    config.Features.EnableMetrics = true
 64    config.Features.Debug = false  // Production-safe default
 65
 66    // Performance defaults
 67    config.Performance.WorkerCount = 10
 68    config.Performance.QueueSize = 1000
 69    config.Performance.BatchSize = 100
 70
 71    return config
 72}
 73
 74// Load configuration with comprehensive error handling
 75func loadConfig(filename string) (*Config, error) {
 76    // Check if file exists
 77    if _, err := os.Stat(filename); os.IsNotExist(err) {
 78        fmt.Printf("Config file '%s' not found, using defaults\n", filename)
 79        return getDefaultConfig(), nil
 80    }
 81
 82    data, err := os.ReadFile(filename)
 83    if err != nil {
 84        return nil, fmt.Errorf("reading config file: %w", err)
 85    }
 86
 87    config := getDefaultConfig()
 88    if err := json.Unmarshal(data, config); err != nil {
 89        return nil, fmt.Errorf("parsing config file: %w", err)
 90    }
 91
 92    // Validate configuration
 93    if err := validateConfig(config); err != nil {
 94        return nil, fmt.Errorf("invalid configuration: %w", err)
 95    }
 96
 97    return config, nil
 98}
 99
100// Validate configuration with type-specific checks
101func validateConfig(config *Config) error {
102    // Server validation
103    if config.Server.Host == "" {
104        return fmt.Errorf("server host cannot be empty")
105    }
106    if config.Server.Port == 0 {
107        return fmt.Errorf("server port cannot be 0")
108    }
109    if config.Server.ReadTimeout < time.Second {
110        return fmt.Errorf("read timeout must be at least 1 second")
111    }
112
113    // Database validation
114    validDBTypes := map[string]bool{
115        "postgres": true,
116        "mysql":    true,
117        "sqlite":   true,
118    }
119    if !validDBTypes[config.Database.Type] {
120        return fmt.Errorf("unsupported database type: %s", config.Database.Type)
121    }
122
123    if config.Database.Host == "" {
124        return fmt.Errorf("database host cannot be empty")
125    }
126    if config.Database.Port == 0 {
127        return fmt.Errorf("database port cannot be 0")
128    }
129    if config.Database.MaxOpenConns == 0 {
130        return fmt.Errorf("max open connections cannot be 0")
131    }
132
133    // Performance validation
134    if config.Performance.WorkerCount == 0 {
135        return fmt.Errorf("worker count cannot be 0")
136    }
137    if config.Performance.QueueSize == 0 {
138        return fmt.Errorf("queue size cannot be 0")
139    }
140    if config.Performance.BatchSize == 0 {
141        return fmt.Errorf("batch size cannot be 0")
142    }
143
144    return nil
145}
146
147func main() {
148    configFile := "config.json"
149
150    // Load configuration
151    config, err := loadConfig(configFile)
152    if err != nil {
153        fmt.Printf("Error loading configuration: %v\n", err)
154        return
155    }
156
157    fmt.Printf("=== Loaded Configuration ===\n")
158    fmt.Printf("Server: %s:%d (timeouts: read=%v, write=%v)\n",
159        config.Server.Host, config.Server.Port,
160        config.Server.ReadTimeout, config.Server.WriteTimeout)
161    fmt.Printf("Database: %s@%s:%d/%s (max_conns=%d, lifetime=%v)\n",
162        config.Database.Type, config.Database.Host, config.Database.Port,
163        config.Database.Name, config.Database.MaxOpenConns, config.Database.ConnLifetime)
164    fmt.Printf("Features: cache=%v, metrics=%v, debug=%v\n",
165        config.Features.EnableCache, config.Features.EnableMetrics, config.Features.Debug)
166    fmt.Printf("Performance: %d workers, queue=%d, batch=%d\n",
167        config.Performance.WorkerCount, config.Performance.QueueSize,
168        config.Performance.BatchSize)
169
170    // Demonstrate configuration usage
171    fmt.Printf("\n=== Configuration Usage ===\n")
172    serverAddr := fmt.Sprintf("%s:%d", config.Server.Host, config.Server.Port)
173    fmt.Printf("Server will listen on: %s\n", serverAddr)
174
175    if config.Features.Debug {
176        fmt.Println("Debug mode enabled - verbose logging active")
177    }
178    if config.Features.EnableCache {
179        fmt.Println("Cache enabled - performance optimizations active")
180    }
181    if config.Features.EnableMetrics {
182        fmt.Println("Metrics enabled - performance monitoring active")
183    }
184}

What this demonstrates:

  • Optimal type selection: Using uint16 for ports, uint8 for worker counts
  • Zero value utilization: Default configuration uses appropriate zero values
  • Comprehensive validation: Type-specific bounds checking
  • Error handling: Clear error messages with context
  • Practical application: Real-world configuration system

Practice Exercises

Exercise 1: Numeric Type Range Calculator

Learning Objectives: Understand numeric type ranges and overflow behavior

Difficulty: Beginner

Real-World Context: Choosing the correct numeric type prevents overflow bugs in production

Task: Create a program that displays the range of all integer types and demonstrates overflow behavior.

Solution:

Show Solution
 1// run
 2package main
 3
 4import (
 5    "fmt"
 6    "math"
 7)
 8
 9func displayTypeRanges() {
10    fmt.Println("=== Signed Integer Types ===")
11    fmt.Printf("int8:   %d to %d\n", math.MinInt8, math.MaxInt8)
12    fmt.Printf("int16:  %d to %d\n", math.MinInt16, math.MaxInt16)
13    fmt.Printf("int32:  %d to %d\n", math.MinInt32, math.MaxInt32)
14    fmt.Printf("int64:  %d to %d\n", math.MinInt64, math.MaxInt64)
15
16    fmt.Println("\n=== Unsigned Integer Types ===")
17    fmt.Printf("uint8:  0 to %d\n", math.MaxUint8)
18    fmt.Printf("uint16: 0 to %d\n", math.MaxUint16)
19    fmt.Printf("uint32: 0 to %d\n", math.MaxUint32)
20    fmt.Printf("uint64: 0 to %d\n", uint64(math.MaxUint64))
21
22    fmt.Println("\n=== Floating-Point Types ===")
23    fmt.Printf("float32: %.2e to %.2e\n", -math.MaxFloat32, math.MaxFloat32)
24    fmt.Printf("float64: %.2e to %.2e\n", -math.MaxFloat64, math.MaxFloat64)
25}
26
27func demonstrateOverflow() {
28    fmt.Println("\n=== Overflow Demonstration ===")
29
30    // int8 overflow
31    var i8 int8 = 127  // Max value
32    fmt.Printf("int8: %d + 1 = %d (overflow!)\n", i8, i8+1)
33
34    // uint8 underflow
35    var u8 uint8 = 0  // Min value
36    fmt.Printf("uint8: %d - 1 = %d (underflow!)\n", u8, u8-1)
37
38    // Safe increment with check
39    var safeInt8 int8 = 127
40    if safeInt8 < math.MaxInt8 {
41        safeInt8++
42        fmt.Printf("Safe increment: %d\n", safeInt8)
43    } else {
44        fmt.Printf("Cannot increment %d: would overflow\n", safeInt8)
45    }
46}
47
48func main() {
49    displayTypeRanges()
50    demonstrateOverflow()
51}

Exercise 2: String Builder Performance

Learning Objectives: Understand string immutability and efficient string building

Difficulty: Beginner

Real-World Context: Inefficient string concatenation can cause performance issues in loops

Task: Compare string concatenation performance using + operator vs strings.Builder.

Solution:

Show Solution
 1// run
 2package main
 3
 4import (
 5    "fmt"
 6    "strings"
 7    "time"
 8)
 9
10func concatenateWithPlus(n int) string {
11    var result string
12    for i := 0; i < n; i++ {
13        result += "a"  // Creates new string each iteration
14    }
15    return result
16}
17
18func concatenateWithBuilder(n int) string {
19    var builder strings.Builder
20    for i := 0; i < n; i++ {
21        builder.WriteString("a")  // Efficient append
22    }
23    return builder.String()
24}
25
26func benchmarkConcatenation(n int) {
27    fmt.Printf("Building string with %d characters\n\n", n)
28
29    // Test with + operator
30    start := time.Now()
31    result1 := concatenateWithPlus(n)
32    elapsed1 := time.Since(start)
33    fmt.Printf("Using + operator: %v (length: %d)\n", elapsed1, len(result1))
34
35    // Test with strings.Builder
36    start = time.Now()
37    result2 := concatenateWithBuilder(n)
38    elapsed2 := time.Since(start)
39    fmt.Printf("Using strings.Builder: %v (length: %d)\n", elapsed2, len(result2))
40
41    // Calculate speedup
42    speedup := float64(elapsed1) / float64(elapsed2)
43    fmt.Printf("\nstrings.Builder is %.2fx faster\n", speedup)
44}
45
46func main() {
47    fmt.Println("=== String Building Performance ===\n")
48
49    // Test with different sizes
50    sizes := []int{100, 1000, 10000}
51    for _, size := range sizes {
52        benchmarkConcatenation(size)
53        fmt.Println()
54    }
55
56    // Explain why
57    fmt.Println("=== Why strings.Builder is Faster ===")
58    fmt.Println("Strings are immutable in Go.")
59    fmt.Println("Using + creates a new string each time (O(n²) complexity).")
60    fmt.Println("strings.Builder uses a growable buffer (O(n) complexity).")
61}

Exercise 3: Type Selection Challenge

Learning Objectives: Choose optimal types for a user management system

Difficulty: Intermediate

Real-World Context: Proper type selection reduces memory usage at scale

Task: Create a struct for managing user profiles that optimizes for memory usage.

Requirements:

  1. Choose the most appropriate types for each field
  2. Calculate memory savings vs using generic types
  3. Explain your type selection choices

Solution:

Show Solution
 1// run
 2package main
 3
 4import (
 5    "fmt"
 6    "unsafe"
 7)
 8
 9type OptimizedUser struct {
10    ID           uint32  // 0-4.2B users, 4 bytes
11    Age          uint8   // 0-255 years, 1 byte
12    BalanceCents int32   // Supports +/- $21M, 4 bytes, precise to cents
13    IsActive     bool    // true/false, 1 byte
14    MemberLevel  uint8   // 1-255 levels, 1 byte
15}
16
17type GenericUser struct {
18    ID           int64   // 8 bytes
19    Age          int64   // 8 bytes
20    Balance      float64 // 8 bytes
21    IsActive     bool    // 1 byte
22    MemberLevel  int64   // 8 bytes
23}
24
25func main() {
26    optimized := OptimizedUser{
27        ID:           12345678,
28        Age:          25,
29        BalanceCents: 50000,  // $500.00
30        IsActive:     true,
31        MemberLevel:  5,
32    }
33
34    generic := GenericUser{
35        ID:          12345678,
36        Age:         25,
37        Balance:     500.00,
38        IsActive:    true,
39        MemberLevel: 5,
40    }
41
42    optSize := unsafe.Sizeof(optimized)
43    genSize := unsafe.Sizeof(generic)
44    savings := genSize - optSize
45
46    fmt.Printf("=== Memory Comparison ===\n")
47    fmt.Printf("Optimized user size: %d bytes\n", optSize)
48    fmt.Printf("Generic user size: %d bytes\n", genSize)
49    fmt.Printf("Memory saved per user: %d bytes (%.1f%%)\n",
50        savings, float64(savings)/float64(genSize)*100)
51
52    // Real-world impact
53    million := 1_000_000
54    fmt.Printf("\n=== Impact at Scale (1M users) ===\n")
55    fmt.Printf("Optimized: %.2f MB\n", float64(optSize*uintptr(million))/1024/1024)
56    fmt.Printf("Generic: %.2f MB\n", float64(genSize*uintptr(million))/1024/1024)
57    fmt.Printf("Total saved: %.2f MB\n", float64(savings*uintptr(million))/1024/1024)
58
59    fmt.Printf("\n=== Type Selection Rationale ===\n")
60    fmt.Println("ID: uint32 - 4.2B users is sufficient for most applications")
61    fmt.Println("Age: uint8 - Ages 0-255 cover all human ages")
62    fmt.Println("Balance: int32 (cents) - Avoids float imprecision, supports $±21M")
63    fmt.Println("IsActive: bool - Perfect for binary state")
64    fmt.Println("MemberLevel: uint8 - 255 membership tiers is plenty")
65}

Exercise 4: Safe Type Conversion Library

Learning Objectives: Create safe conversion functions with proper error handling

Difficulty: Intermediate

Real-World Context: User input and external data often need type conversion with validation

Task: Implement functions for safe type conversion.

Implement:

  1. SafeStringToInt(string) (int, error)
  2. SafeStringToFloat(string) (float64, error)
  3. SafeIntToFloat(int) float64
  4. SafeFloatToInt(float64) (int, error)

Requirements:

  • Handle empty strings
  • Validate input format
  • Check for numeric overflow
  • Provide clear error messages

Solution:

Show Solution
  1// run
  2package main
  3
  4import (
  5    "fmt"
  6    "math"
  7    "strconv"
  8    "strings"
  9)
 10
 11func SafeStringToInt(s string) (int, error) {
 12    s = strings.TrimSpace(s)
 13    if s == "" {
 14        return 0, fmt.Errorf("empty string cannot be converted to int")
 15    }
 16
 17    result, err := strconv.ParseInt(s, 10, 64)
 18    if err != nil {
 19        return 0, fmt.Errorf("invalid integer format '%s': %w", s, err)
 20    }
 21
 22    // Check for 32-bit int overflow
 23    if result < math.MinInt32 || result > math.MaxInt32 {
 24        return 0, fmt.Errorf("value %s overflows 32-bit integer range", s)
 25    }
 26
 27    return int(result), nil
 28}
 29
 30func SafeStringToFloat(s string) (float64, error) {
 31    s = strings.TrimSpace(s)
 32    if s == "" {
 33        return 0, fmt.Errorf("empty string cannot be converted to float")
 34    }
 35
 36    result, err := strconv.ParseFloat(s, 64)
 37    if err != nil {
 38        return 0, fmt.Errorf("invalid float format '%s': %w", s, err)
 39    }
 40
 41    // Check for special values
 42    if math.IsNaN(result) {
 43        return 0, fmt.Errorf("NaN values are not allowed")
 44    }
 45    if math.IsInf(result, 0) {
 46        return 0, fmt.Errorf("infinite values are not allowed")
 47    }
 48
 49    return result, nil
 50}
 51
 52func SafeIntToFloat(i int) float64 {
 53    return float64(i)  // Always safe
 54}
 55
 56func SafeFloatToInt(f float64) (int, error) {
 57    if math.IsNaN(f) {
 58        return 0, fmt.Errorf("cannot convert NaN to int")
 59    }
 60    if math.IsInf(f, 0) {
 61        return 0, fmt.Errorf("cannot convert infinite value to int")
 62    }
 63
 64    // Check for overflow
 65    if f < math.MinInt32 || f > math.MaxInt32 {
 66        return 0, fmt.Errorf("value %f overflows 32-bit integer range", f)
 67    }
 68
 69    // Round properly instead of truncating
 70    return int(math.Round(f)), nil
 71}
 72
 73func main() {
 74    testCases := []string{
 75        "123",
 76        "-456",
 77        "123.45",
 78        "abc",
 79        "",
 80        "2147483647",     // Max int32
 81        "2147483648",     // Overflows int32
 82    }
 83
 84    fmt.Println("=== String to Int Tests ===")
 85    for _, test := range testCases {
 86        result, err := SafeStringToInt(test)
 87        if err != nil {
 88            fmt.Printf("'%s' -> Error: %v\n", test, err)
 89        } else {
 90            fmt.Printf("'%s' -> %d\n", test, result)
 91        }
 92    }
 93
 94    fmt.Println("\n=== String to Float Tests ===")
 95    for _, test := range testCases {
 96        result, err := SafeStringToFloat(test)
 97        if err != nil {
 98            fmt.Printf("'%s' -> Error: %v\n", test, err)
 99        } else {
100            fmt.Printf("'%s' -> %.2f\n", test, result)
101        }
102    }
103
104    fmt.Println("\n=== Float to Int Tests ===")
105    floatTests := []float64{123.456, 789.0, -456.123, 2147483647.9, math.NaN(), math.Inf(1)}
106    for _, f := range floatTests {
107        result, err := SafeFloatToInt(f)
108        if err != nil {
109            fmt.Printf("%.1f -> Error: %v\n", f, err)
110        } else {
111            fmt.Printf("%.1f -> %d\n", f, result)
112        }
113    }
114}

Exercise 5: Configuration Validation System

Learning Objectives: Build a configuration system with comprehensive type validation

Difficulty: Advanced

Real-World Context: Applications need robust configuration systems that validate all inputs

Task: Create a configuration system that loads, validates, and manages application settings.

Requirements:

  1. Load JSON configuration from file
  2. Validate all configuration values
  3. Provide meaningful error messages
  4. Use appropriate Go types for each field
  5. Handle missing configuration files gracefully
  6. Support default values

Solution:

Show Solution
  1// run
  2package main
  3
  4import (
  5    "encoding/json"
  6    "fmt"
  7    "os"
  8    "time"
  9)
 10
 11type DatabaseConfig struct {
 12    Type     string `json:"type"`
 13    Host     string `json:"host"`
 14    Port     int    `json:"port"`
 15    Database string `json:"database"`
 16    Username string `json:"username"`
 17    Password string `json:"password"`
 18    SSLMode  bool   `json:"ssl_mode"`
 19}
 20
 21type ServerConfig struct {
 22    Host          string        `json:"host"`
 23    Port          int           `json:"port"`
 24    ReadTimeout   time.Duration `json:"read_timeout"`
 25    WriteTimeout  time.Duration `json:"write_timeout"`
 26    KeepAlive     bool          `json:"keep_alive"`
 27    MaxHeaderSize int64         `json:"max_header_size"`
 28}
 29
 30type AppConfig struct {
 31    Database DatabaseConfig `json:"database"`
 32    Server   ServerConfig   `json:"server"`
 33    Debug    bool           `json:"debug"`
 34    LogLevel string         `json:"log_level"`
 35}
 36
 37func validateConfig(config *AppConfig) error {
 38    // Validate database configuration
 39    validDBTypes := map[string]bool{
 40        "postgres": true, "mysql": true, "sqlite": true,
 41    }
 42    if config.Database.Type == "" {
 43        return fmt.Errorf("database type is required")
 44    }
 45    if !validDBTypes[config.Database.Type] {
 46        return fmt.Errorf("unsupported database type: %s", config.Database.Type)
 47    }
 48    if config.Database.Host == "" {
 49        return fmt.Errorf("database host is required")
 50    }
 51    if config.Database.Port <= 0 || config.Database.Port > 65535 {
 52        return fmt.Errorf("database port must be between 1 and 65535, got: %d", config.Database.Port)
 53    }
 54    if config.Database.Database == "" {
 55        return fmt.Errorf("database name is required")
 56    }
 57
 58    // Validate server configuration
 59    if config.Server.Host == "" {
 60        return fmt.Errorf("server host is required")
 61    }
 62    if config.Server.Port <= 0 || config.Server.Port > 65535 {
 63        return fmt.Errorf("server port must be between 1 and 65535, got: %d", config.Server.Port)
 64    }
 65    if config.Server.ReadTimeout < time.Second {
 66        return fmt.Errorf("read timeout must be at least 1 second")
 67    }
 68    if config.Server.MaxHeaderSize <= 0 {
 69        return fmt.Errorf("max header size must be positive")
 70    }
 71
 72    // Validate general configuration
 73    validLogLevels := map[string]bool{
 74        "debug": true, "info": true, "warn": true, "error": true,
 75    }
 76    if config.LogLevel != "" && !validLogLevels[config.LogLevel] {
 77        return fmt.Errorf("invalid log level: %s", config.LogLevel)
 78    }
 79
 80    return nil
 81}
 82
 83func getDefaultConfig() *AppConfig {
 84    return &AppConfig{
 85        Database: DatabaseConfig{
 86            Type:     "sqlite",
 87            Host:     "localhost",
 88            Port:     5432,
 89            Database: "app.db",
 90            SSLMode:  false,
 91        },
 92        Server: ServerConfig{
 93            Host:          "localhost",
 94            Port:          8080,
 95            ReadTimeout:   30 * time.Second,
 96            WriteTimeout:  30 * time.Second,
 97            KeepAlive:     true,
 98            MaxHeaderSize: 8192,
 99        },
100        Debug:    false,
101        LogLevel: "info",
102    }
103}
104
105func loadConfig(filename string) (*AppConfig, error) {
106    // Try to read config file
107    data, err := os.ReadFile(filename)
108    if err != nil {
109        if os.IsNotExist(err) {
110            return getDefaultConfig(), nil
111        }
112        return nil, fmt.Errorf("reading config file: %w", err)
113    }
114
115    config := getDefaultConfig()
116    if err := json.Unmarshal(data, config); err != nil {
117        return nil, fmt.Errorf("parsing config file: %w", err)
118    }
119
120    // Validate configuration
121    if err := validateConfig(config); err != nil {
122        return nil, fmt.Errorf("invalid configuration: %w", err)
123    }
124
125    return config, nil
126}
127
128func main() {
129    config, err := loadConfig("app-config.json")
130    if err != nil {
131        fmt.Printf("Error loading configuration: %v\n", err)
132        fmt.Println("Using default configuration instead")
133        config = getDefaultConfig()
134    }
135
136    fmt.Printf("Configuration loaded successfully\n")
137    fmt.Printf("Database: %s://%s:%d/%s (SSL: %v)\n",
138        config.Database.Type, config.Database.Host, config.Database.Port,
139        config.Database.Database, config.Database.SSLMode)
140    fmt.Printf("Server: %s:%d (timeouts: read=%v, write=%v)\n",
141        config.Server.Host, config.Server.Port,
142        config.Server.ReadTimeout, config.Server.WriteTimeout)
143    fmt.Printf("Debug: %v, Log Level: %s\n", config.Debug, config.LogLevel)
144}

Summary

What You've Mastered

Type System Understanding:

  • Static typing benefits: Compile-time error prevention saves debugging time
  • Type selection: Choosing optimal types for memory and performance
  • Zero values: Understanding and utilizing Go's initialization safety
  • Type conversions: Safe conversion with proper error handling
  • Numeric types: Choosing between signed/unsigned, int sizes, float precision

Variable Declaration Patterns:

  • var vs :=: When to use each declaration method
  • Package vs local: Scope and lifetime considerations
  • Constants: Using const and iota for compile-time values
  • Type inference: Letting Go determine types while maintaining safety
  • Multiple declarations: Grouping related variables for clarity

Best Practices:

  • Explicit over implicit: Clear type expressions prevent ambiguity
  • Error handling: Always checking conversion and parsing results
  • Memory optimization: Selecting appropriate types for large datasets
  • Validation: Comprehensive input validation with meaningful errors
  • Type safety: Leveraging the compiler to catch bugs early

Key Takeaways

  1. Type safety is Go's superpower - use it to prevent runtime errors before they happen
  2. Choose types deliberately - consider range, memory usage, and performance implications
  3. Zero values are your friend - leverage them for safe, predictable initialization
  4. Never ignore conversion errors - they indicate real problems that need handling
  5. Validate inputs rigorously - assume all external data can be malformed
  6. Use constants for configuration - single source of truth prevents inconsistencies
  7. Understand type limitations - know the range and precision of numeric types

What's Next

Continue your Go journey with:

Further Practice

Type Safety Challenges:

  • Financial calculations: Practice precise decimal arithmetic with cents
  • Configuration systems: Build type-safe configuration loaders
  • Data validation: Create comprehensive input validation systems
  • Performance optimization: Profile and optimize memory usage with proper types

Real-World Applications:

  • API development: Build type-safe request/response handling
  • Database operations: Create type-safe database interactions
  • CLI tools: Develop command-line applications with proper input handling
  • Data processing: Build ETL pipelines with type validation

You now have a solid foundation in Go's type system and variable handling! These fundamentals will serve you well as you build more complex applications. The type safety, explicit conversions, and zero values you've learned are the building blocks of reliable Go code. Happy coding!