Rez Moss

Rez Moss

Digital Reflections: Exploring Tech, Innovation & Ideas

Parsing Booleans and Integers in Go: ParseBool, ParseInt, and ParseUint 2/7

Aug 2025

String parsing is a fundamental operation in Go programming. Whether you’re reading configuration files, processing user input, or handling API responses, you’ll frequently need to convert string representations into their appropriate data types. Go’s strconv package provides three essential functions for this purpose: ParseBool, ParseInt, and ParseUint.

Understanding ParseBool

The ParseBool function converts string representations of boolean values into actual boolean types. Its signature is straightforward:

func ParseBool(str string) (bool, error)

This function accepts a limited set of string values and converts them to their boolean equivalents. The accepted values are case-insensitive and include:

  • "true", "t", "1"true
  • "false", "f", "0"false

Let’s see how this works in practice:

package main

import (
    "fmt"
    "strconv"
)

func main() {
    // Valid true values
    trueValues := []string{"true", "TRUE", "t", "T", "1"}
    for _, val := range trueValues {
        result, err := strconv.ParseBool(val)
        if err != nil {
            fmt.Printf("Error parsing %s: %v\n", val, err)
            continue
        }
        fmt.Printf("ParseBool(%s) = %t\n", val, result)
    }

    // Valid false values
    falseValues := []string{"false", "FALSE", "f", "F", "0"}
    for _, val := range falseValues {
        result, err := strconv.ParseBool(val)
        if err != nil {
            fmt.Printf("Error parsing %s: %v\n", val, err)
            continue
        }
        fmt.Printf("ParseBool(%s) = %t\n", val, result)
    }

    // Invalid values
    invalidValues := []string{"yes", "no", "2", "on", "off", ""}
    for _, val := range invalidValues {
        result, err := strconv.ParseBool(val)
        if err != nil {
            fmt.Printf("Error parsing %s: %v\n", val, err)
        } else {
            fmt.Printf("ParseBool(%s) = %t\n", val, result)
        }
    }
}

The function is strict about its input validation. Any string that doesn’t match the accepted patterns will return an error of type *strconv.NumError. This makes ParseBool reliable but requires you to handle cases where the input might not conform to these specific formats.

Mastering ParseInt: Converting Strings to Signed Integers

The ParseInt function handles the conversion of string representations to signed integers with precise control over number base and bit size. Its signature provides three parameters:

func ParseInt(s string, base int, bitSize int) (int64, error)

The base parameter determines how the string should be interpreted numerically, while bitSize controls the range of the resulting integer. Let’s explore each aspect systematically.

Base Parameter Examples

The base parameter accepts values from 2 to 36, with some special behaviors:

Binary (base 2):

result, err := strconv.ParseInt("1010", 2, 64)
// result = 10 (decimal)

result, err := strconv.ParseInt("11111111", 2, 64)
// result = 255 (decimal)

Octal (base 8):

result, err := strconv.ParseInt("755", 8, 64)
// result = 493 (decimal)

result, err := strconv.ParseInt("1777", 8, 64)
// result = 1023 (decimal)

Decimal (base 10):

result, err := strconv.ParseInt("12345", 10, 64)
// result = 12345

result, err := strconv.ParseInt("-987", 10, 64)
// result = -987

Hexadecimal (base 16):

result, err := strconv.ParseInt("ff", 16, 64)
// result = 255 (decimal)

result, err := strconv.ParseInt("DEADBEEF", 16, 64)
// result = 3735928559 (decimal)

Base 0: Automatic Detection

When you set base to 0, ParseInt automatically detects the number format based on the string prefix:

package main

import (
    "fmt"
    "strconv"
)

func main() {
    examples := []string{
        "42",        // decimal (no prefix)
        "0755",      // octal (0 prefix)
        "0xff",      // hexadecimal (0x prefix)
        "0b1010",    // binary (0b prefix)
        "-0x20",     // negative hexadecimal
    }

    for _, example := range examples {
        result, err := strconv.ParseInt(example, 0, 64)
        if err != nil {
            fmt.Printf("Error parsing %s: %v\n", example, err)
            continue
        }
        fmt.Printf("ParseInt(%s, 0, 64) = %d\n", example, result)
    }
}

This automatic detection follows Go’s integer literal conventions, making it particularly useful when parsing Go source code or configuration files that use Go-style number formats.

BitSize Parameter Usage

The bitSize parameter doesn’t change the return type (always int64) but validates that the parsed value fits within the specified bit range:

// 8-bit signed integer range: -128 to 127
result, err := strconv.ParseInt("100", 10, 8)  // Valid
result, err := strconv.ParseInt("200", 10, 8)  // Error: out of range

// 16-bit signed integer range: -32768 to 32767
result, err := strconv.ParseInt("30000", 10, 16)  // Valid
result, err := strconv.ParseInt("40000", 10, 16)  // Error: out of range

// 32-bit signed integer range: -2147483648 to 2147483647
result, err := strconv.ParseInt("2000000000", 10, 32)  // Valid
result, err := strconv.ParseInt("3000000000", 10, 32)  // Error: out of range

The most common bitSize values are 8, 16, 32, and 64. Using bitSize 0 is equivalent to using the platform’s int size (32 or 64 bits depending on your system).

Understanding ParseUint: Unsigned Integer Parsing

The ParseUint function works similarly to ParseInt but specifically handles unsigned integers, which can only represent non-negative values. This allows for a larger positive range compared to signed integers of the same bit size:

func ParseUint(s string, base int, bitSize int) (uint64, error)

Key Differences from ParseInt

The most significant difference is that ParseUint cannot accept negative values. Any string containing a negative sign will result in an error:

package main

import (
    "fmt"
    "strconv"
)

func main() {
    // Valid unsigned values
    validValues := []string{"0", "255", "65535", "4294967295"}
    
    for _, val := range validValues {
        result, err := strconv.ParseUint(val, 10, 64)
        if err != nil {
            fmt.Printf("Error parsing %s: %v\n", val, err)
            continue
        }
        fmt.Printf("ParseUint(%s, 10, 64) = %d\n", val, result)
    }

    // Invalid values (negative numbers)
    invalidValues := []string{"-1", "-100", "-0"}
    
    for _, val := range invalidValues {
        result, err := strconv.ParseUint(val, 10, 64)
        if err != nil {
            fmt.Printf("Error parsing %s: %v\n", val, err)
        }
    }
}

Base Usage with ParseUint

ParseUint supports the same base options as ParseInt, including automatic detection with base 0:

// Different bases with unsigned parsing
examples := map[string]int{
    "11111111": 2,   // Binary: 255
    "377":      8,   // Octal: 255
    "255":      10,  // Decimal: 255
    "FF":       16,  // Hexadecimal: 255
}

for str, base := range examples {
    result, err := strconv.ParseUint(str, base, 64)
    if err != nil {
        fmt.Printf("Error parsing %s (base %d): %v\n", str, base, err)
        continue
    }
    fmt.Printf("ParseUint(%s, %d, 64) = %d\n", str, base, result)
}

// Auto-detection examples
autoDetectExamples := []string{
    "255",      // Decimal
    "0377",     // Octal
    "0xFF",     // Hexadecimal
    "0b11111111", // Binary
}

for _, example := range autoDetectExamples {
    result, err := strconv.ParseUint(example, 0, 64)
    if err != nil {
        fmt.Printf("Error parsing %s: %v\n", example, err)
        continue
    }
    fmt.Printf("ParseUint(%s, 0, 64) = %d\n", example, result)
}

BitSize Ranges for Unsigned Integers

The bitSize parameter for ParseUint defines different maximum values compared to signed integers:

// 8-bit unsigned: 0 to 255
result, err := strconv.ParseUint("200", 10, 8)  // Valid
result, err := strconv.ParseUint("300", 10, 8)  // Error: out of range

// 16-bit unsigned: 0 to 65535
result, err := strconv.ParseUint("50000", 10, 16)  // Valid
result, err := strconv.ParseUint("70000", 10, 16)  // Error: out of range

// 32-bit unsigned: 0 to 4294967295
result, err := strconv.ParseUint("4000000000", 10, 32)  // Valid
result, err := strconv.ParseUint("5000000000", 10, 32)  // Error: out of range

// 64-bit unsigned: 0 to 18446744073709551615
result, err := strconv.ParseUint("18446744073709551615", 10, 64)  // Valid (max uint64)

Practical Applications

ParseUint is particularly useful when working with:

  • Port numbers (0-65535)
  • RGB color values (0-255 per channel)
  • Array indices and sizes
  • Timestamps and durations
  • Network addresses and masks
// Parsing RGB color components
rgbComponents := []string{"255", "128", "64"}
var rgb [3]uint8

for i, component := range rgbComponents {
    val, err := strconv.ParseUint(component, 10, 8)
    if err != nil {
        fmt.Printf("Invalid RGB component: %s\n", component)
        continue
    }
    rgb[i] = uint8(val)
}
fmt.Printf("RGB: (%d, %d, %d)\n", rgb[0], rgb[1], rgb[2])

The combination of base flexibility and bit size validation makes ParseUint a powerful tool for handling various unsigned integer parsing scenarios in your Go applications.

BitSize Parameter Deep Dive and Practical Limits

Understanding the bitSize parameter is crucial for writing robust parsing code. This parameter serves as both a validator and a documentation tool, ensuring that parsed values fit within the expected range for your specific use case.

BitSize Validation Mechanics

The bitSize parameter doesn’t affect the return type but validates the parsed value against the specified bit range. Here’s how it works internally:

package main

import (
    "fmt"
    "strconv"
)

func demonstrateBitSizeValidation() {
    testValue := "1000"
    
    // Different bitSize values with the same input
    bitSizes := []int{8, 16, 32, 64}
    
    for _, bitSize := range bitSizes {
        result, err := strconv.ParseInt(testValue, 10, bitSize)
        if err != nil {
            fmt.Printf("bitSize %d: Error - %v\n", bitSize, err)
        } else {
            fmt.Printf("bitSize %d: Success - %d\n", bitSize, result)
        }
    }
    
    // Testing edge cases
    edgeCases := map[string]int{
        "127":  8,  // Maximum int8
        "128":  8,  // Exceeds int8
        "-128": 8,  // Minimum int8
        "-129": 8,  // Below int8 minimum
    }
    
    for value, bitSize := range edgeCases {
        result, err := strconv.ParseInt(value, 10, bitSize)
        fmt.Printf("ParseInt(%s, 10, %d): ", value, bitSize)
        if err != nil {
            fmt.Printf("Error - %v\n", err)
        } else {
            fmt.Printf("Success - %d\n", result)
        }
    }
}

Complete BitSize Reference Table

Here’s a comprehensive reference for all standard bit sizes:

// Signed integer ranges (ParseInt)
// bitSize 8:  -128 to 127
// bitSize 16: -32768 to 32767
// bitSize 32: -2147483648 to 2147483647
// bitSize 64: -9223372036854775808 to 9223372036854775807

// Unsigned integer ranges (ParseUint)
// bitSize 8:  0 to 255
// bitSize 16: 0 to 65535
// bitSize 32: 0 to 4294967295
// bitSize 64: 0 to 18446744073709551615

func demonstrateAllRanges() {
    // Testing maximum values for each bit size
    signedMaxValues := map[int]string{
        8:  "127",
        16: "32767",
        32: "2147483647",
        64: "9223372036854775807",
    }
    
    unsignedMaxValues := map[int]string{
        8:  "255",
        16: "65535",
        32: "4294967295",
        64: "18446744073709551615",
    }
    
    fmt.Println("Signed integer maximum values:")
    for bitSize, maxVal := range signedMaxValues {
        result, err := strconv.ParseInt(maxVal, 10, bitSize)
        if err != nil {
            fmt.Printf("bitSize %d: Unexpected error - %v\n", bitSize, err)
        } else {
            fmt.Printf("bitSize %d: %d\n", bitSize, result)
        }
    }
    
    fmt.Println("\nUnsigned integer maximum values:")
    for bitSize, maxVal := range unsignedMaxValues {
        result, err := strconv.ParseUint(maxVal, 10, bitSize)
        if err != nil {
            fmt.Printf("bitSize %d: Unexpected error - %v\n", bitSize, err)
        } else {
            fmt.Printf("bitSize %d: %d\n", bitSize, result)
        }
    }
}

Special BitSize Cases

There are two special cases worth understanding:

BitSize 0: Uses the platform’s native int size (32-bit on 32-bit systems, 64-bit on 64-bit systems)

// On a 64-bit system
result, err := strconv.ParseInt("2147483648", 10, 0)  // Usually succeeds
// On a 32-bit system  
result, err := strconv.ParseInt("2147483648", 10, 0)  // Would fail

// To write portable code, be explicit:
result, err := strconv.ParseInt("2147483648", 10, 64)  // Always consistent

BitSize > 64: The functions will accept bitSize values greater than 64, but they behave the same as bitSize 64:

result, err := strconv.ParseInt("100", 10, 128)  // Treated as bitSize 64

Practical Error Handling Patterns

When working with bitSize validation, you’ll often need to handle range errors specifically:

func parseWithFallback(s string, preferredBitSize int) (int64, error) {
    // Try preferred bit size first
    result, err := strconv.ParseInt(s, 10, preferredBitSize)
    if err != nil {
        // Check if it's a range error
        if numErr, ok := err.(*strconv.NumError); ok {
            if numErr.Err == strconv.ErrRange {
                fmt.Printf("Value %s exceeds %d-bit range, trying 64-bit\n", s, preferredBitSize)
                return strconv.ParseInt(s, 10, 64)
            }
        }
        return 0, err
    }
    return result, nil
}

// Usage example
values := []string{"100", "40000", "3000000000"}
for _, val := range values {
    result, err := parseWithFallback(val, 16)
    if err != nil {
        fmt.Printf("Failed to parse %s: %v\n", val, err)
    } else {
        fmt.Printf("Parsed %s as %d\n", val, result)
    }
}

Performance Considerations

The bitSize parameter has minimal performance impact since validation occurs after parsing. However, choosing the right bitSize can help catch data corruption or invalid inputs early:

// Good: Explicit about expected range
func parsePort(s string) (uint16, error) {
    val, err := strconv.ParseUint(s, 10, 16)
    if err != nil {
        return 0, fmt.Errorf("invalid port number: %w", err)
    }
    return uint16(val), nil
}

// Less ideal: No range validation
func parsePortWeak(s string) (uint16, error) {
    val, err := strconv.ParseUint(s, 10, 64)
    if err != nil {
        return 0, err
    }
    return uint16(val), nil  // Could truncate unexpectedly
}

Understanding bitSize limits helps you write more predictable and maintainable parsing code, especially when dealing with data that needs to fit specific constraints like database column types or network protocol fields.

comments powered by Disqus