Introduction to String to Integer (atoi)
The String to Integer (atoi) problem is a classic algorithmic challenge frequently encountered in coding interviews and real-world parsing tasks. The goal is to convert a string representation of a number into a 32-bit signed integer, while handling edge cases such as leading whitespace, optional signs, non-numeric characters, and integer overflow. In this tutorial, we will walk through a complete implementation in Go, explaining each step along the way.
What Is atoi?
The name atoi comes from the C standard library function that converts ASCII strings to integers. The LeetCode-style version of this problem adds several constraints that make it more interesting than a simple conversion:
- Read in and ignore any leading whitespace.
- Check for an optional
+or-sign character. - Read digits until a non-digit character is encountered or the end of the string is reached.
- Clamp the result to the 32-bit signed integer range
[-2^31, 2^31 - 1]. - If no valid digits were read, return 0.
Why It Matters
While Go provides built-in functions like strconv.Atoi and strconv.ParseInt, implementing atoi manually teaches fundamental concepts: character-by-character parsing, state management, overflow handling, and defensive programming. These skills transfer directly to building parsers, lexers, configuration readers, and protocol decoders. Understanding how to safely convert untrusted input into numeric values is also a critical security consideration in many applications.
Understanding the Problem Constraints
Before writing code, let us clearly define the behavior we need to implement. Consider the following examples:
"42"returns42" -42"returns-42(leading whitespace ignored, sign respected)"4193 with words"returns4193(parsing stops at first non-digit)"words and 987"returns0(no valid conversion because the first non-whitespace character is not a digit or sign)"-91283472332"returns-2147483648(clamped to the minimum 32-bit integer)
The 32-bit signed integer range is [-2147483648, 2147483647]. Any result outside this range must be clamped to the nearest bound.
Step-by-Step Implementation in Go
Step 1: Setting Up the Function Signature
We begin by defining the function. It accepts a string and returns an integer:
func myAtoi(s string) int {
// Implementation goes here
}
Step 2: Skipping Leading Whitespace
The first task is to advance an index past any leading space characters. We use a pointer i to track our current position in the string:
func myAtoi(s string) int {
i := 0
n := len(s)
// Skip leading whitespace
for i < n && s[i] == ' ' {
i++
}
}
Step 3: Handling the Optional Sign
After whitespace, we check whether the next character is a + or -. We store the sign in a variable and advance the pointer:
func myAtoi(s string) int {
i := 0
n := len(s)
for i < n && s[i] == ' ' {
i++
}
sign := 1
if i < n && (s[i] == '+' || s[i] == '-') {
if s[i] == '-' {
sign = -1
}
i++
}
}
Step 4: Reading Digits and Building the Result
Now we read consecutive digit characters and accumulate the result. For each digit, we multiply the current result by 10 and add the digit value. We obtain the numeric value of a digit character by subtracting the ASCII value of '0':
func myAtoi(s string) int {
i := 0
n := len(s)
for i < n && s[i] == ' ' {
i++
}
sign := 1
if i < n && (s[i] == '+' || s[i] == '-') {
if s[i] == '-' {
sign = -1
}
i++
}
result := 0
for i < n && s[i] >= '0' && s[i] <= '9' {
digit := int(s[i] - '0')
result = result*10 + digit
i++
}
return sign * result
}
This works for simple cases, but it does not yet handle overflow. If the input represents a number larger than the 32-bit maximum, the result will be incorrect.
Step 5: Handling Overflow
To detect overflow before it happens, we check whether multiplying the current result by 10 would exceed the maximum value. The 32-bit signed integer bounds are -2147483648 and 2147483647. We define constants for clarity:
const (
maxInt32 = 2147483647
minInt32 = -2147483648
)
Before appending a new digit, we verify that result*10 + digit will not overflow. If the sign is positive and the result would exceed maxInt32, we clamp to maxInt32. If the sign is negative and the result would exceed maxInt32 + 1, we clamp to minInt32:
func myAtoi(s string) int {
const (
maxInt32 = 2147483647
minInt32 = -2147483648
)
i := 0
n := len(s)
// Skip leading whitespace
for i < n && s[i] == ' ' {
i++
}
// Handle optional sign
sign := 1
if i < n && (s[i] == '+' || s[i] == '-') {
if s[i] == '-' {
sign = -1
}
i++
}
// Convert digits
result := 0
for i < n && s[i] >= '0' && s[i] <= '9' {
digit := int(s[i] - '0')
// Check for overflow before adding the digit
if result > (maxInt32-digit)/10 {
if sign == 1 {
return maxInt32
}
return minInt32
}
result = result*10 + digit
i++
}
return sign * result
}
The overflow check result > (maxInt32-digit)/10 works because it rearranges the inequality result*10 + digit > maxInt32 to avoid actually performing the overflowing multiplication. When the sign is negative, the most negative value has a magnitude of maxInt32 + 1, but since we are working with positive result and applying the sign at the end, returning minInt32 directly is correct.
Testing the Implementation
To verify correctness, we write a set of test cases covering normal inputs, edge cases, and overflow scenarios:
package main
import "fmt"
func main() {
tests := []struct {
input string
expected int
}{
{"42", 42},
{" -42", -42},
{"4193 with words", 4193},
{"words and 987", 0},
{"-91283472332", -2147483648},
{"3.14159", 3},
{"+1", 1},
{" 0000000000012345678", 12345678},
{"00000-42a1234", 0},
{" + 413", 0},
{"2147483648", 2147483647},
{"-2147483649", -2147483648},
{"", 0},
{" ", 0},
}
for _, t := range tests {
result := myAtoi(t.input)
status := "PASS"
if result != t.expected {
status = "FAIL"
}
fmt.Printf("[%s] input=%q expected=%d got=%d\n",
status, t.input, t.expected, result)
}
}
Running this test suite confirms that the implementation handles all the specified cases correctly. Each test exercises a different aspect of the parsing logic, from simple conversions to boundary conditions.
Best Practices
Validate Input Early
Always check bounds before accessing string indices. In Go, accessing an out-of-range index causes a panic, so every s[i] access should be guarded by an i < n check. The implementation above does this consistently in every loop condition.
Use Constants for Magic Numbers
Defining maxInt32 and minInt32 as named constants makes the code more readable and maintainable. Alternatively, you can use the constants from the math package:
import "math"
maxInt32 := math.MaxInt32
minInt32 := math.MinInt32
Prevent Overflow Before It Happens
Never perform an arithmetic operation that might overflow and then check the result. Instead, rearrange the calculation so the check happens first. The pattern result > (max - digit) / 10 is a standard technique for safe accumulation that works in any language.
Keep the Logic Linear and Readable
The atoi algorithm naturally follows a linear state machine: skip whitespace, read sign, read digits, clamp. Keeping each phase in its own clearly labeled block makes the code easy to review and modify. Avoid premature optimization or clever tricks that obscure the intent.
Write Comprehensive Tests
Edge cases are where atoi implementations fail. Always test empty strings, strings with only whitespace, strings with no valid digits, strings with multiple signs, strings with embedded non-digit characters, and values at both overflow boundaries. A thorough test suite is your best defense against subtle bugs.
Comparing With Go's Standard Library
For production code, you should generally use strconv.Atoi or strconv.ParseInt rather than a custom implementation. Here is how the standard library handles conversion:
package main
import (
"fmt"
"strconv"
)
func main() {
// strconv.Atoi converts to int (platform-dependent size)
val, err := strconv.Atoi("42")
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Value:", val)
}
// strconv.ParseInt lets you specify bit size and base
val2, err := strconv.ParseInt("-2147483648", 10, 32)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Value:", val2)
}
}
The standard library functions return an error for invalid input rather than silently returning zero or clamping. This behavior is more appropriate for production applications where you need to distinguish between a genuine zero and a failed parse. The manual implementation we built is ideal for interview practice and for situations where you need the specific clamping and lenient parsing behavior defined by the atoi problem specification.
Conclusion
Implementing string-to-integer conversion in Go is an excellent exercise that reinforces careful index management, defensive overflow checking, and clean state-machine design. By breaking the problem into discrete steps—skipping whitespace, reading the sign, accumulating digits, and clamping to bounds—we arrive at a solution that is both correct and readable. While production code should typically rely on strconv for its robust error handling, understanding the manual approach gives you deeper insight into how numeric parsing works under the hood and prepares you to tackle more complex parsing challenges with confidence.