The hard line between text and data
You are building a command line tool that reads a port number from a flag. The flag library hands you "8080". You try to add one to it. Go refuses to compile. In Python or JavaScript, the runtime quietly coerces types behind your back. Go treats implicit coercion as a bug waiting to happen. The language draws a hard line between text and data. A string is a sequence of bytes. An integer is a fixed width binary value. They live in different memory layouts and serve different purposes.
The strconv package exists to bridge that gap explicitly. It converts strings to numbers, booleans, and runes, and converts them back. It does not guess your intent. It asks for rules and reports failures. Every function in the package follows a predictable naming pattern. ParseX turns a string into a typed value. FormatX turns a typed value into a string. Atoi and Itoa are legacy shortcuts for ASCII to integer and integer to ASCII. The package lives in the standard library because type conversion is a foundational operation, but Go deliberately keeps it out of the language syntax. You must call the function. You must handle the error. You must choose the base and precision.
Explicit conversion prevents silent data corruption. It forces you to think about what happens when the input is malformed, too large, or in the wrong format. The trade off is a few extra lines of code. The payoff is predictable behavior in production.
How the conversion actually works
When you call strconv.Atoi("42"), you are actually calling strconv.ParseInt("42", 10, 0). The function scans the string byte by byte. It skips leading whitespace. It checks for an optional sign. It validates that every remaining character is a digit in the requested base. It builds the result by multiplying the accumulator by the base and adding the new digit. If it encounters a letter, a second sign, or a digit outside the base range, it stops immediately and returns an error.
The error type is *strconv.NumError. It carries three fields: the function name, the original input string, and the underlying cause. This structure makes debugging straightforward. You never get a silent truncation or a wrapped around negative number. You get a clear failure that tells you exactly what went wrong.
The bitSize parameter controls how the function validates the result. A value of 64 means the function will reject any number that exceeds the maximum value of a signed 64 bit integer. A value of 32 enforces 32 bit limits. A value of 0 tells the function to accept any size that fits in the platform native int, which is 64 bits on modern systems. The function returns the full precision value regardless of the bit size you pass. You are responsible for casting it to a smaller type if needed, and you are responsible for checking that the value actually fits.
Minimal example
Here is the simplest round trip: read a decimal string, turn it into an integer, and format it back.
package main
import (
"fmt"
"strconv"
)
func main() {
// Atoi stands for ASCII to integer. It assumes base 10.
port, err := strconv.Atoi("8080")
if err != nil {
// Atoi returns a typed error. Print it to see the cause.
fmt.Println("bad port:", err)
return
}
// Itoa converts the integer back to a decimal string.
// It always uses base 10 and never includes a sign for positive numbers.
label := strconv.Itoa(port)
fmt.Println("listening on", label)
}
The compiler will reject the program with port declared and not used if you remove the label assignment. Go enforces variable usage at compile time to catch dead code early. The if err != nil block is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. Do not swallow errors with _ unless you have a specific reason to ignore them.
Realistic configuration parsing
Real programs rarely deal with clean decimal strings. Configuration files, environment variables, and HTTP headers often contain mixed formats. This function shows how to parse a port and a boolean flag while wrapping errors for the caller.
package main
import (
"fmt"
"strconv"
)
func parseConfig(rawPort, rawDebug string) (int, bool, error) {
// ParseInt handles explicit bases and bit sizes.
// Base 10 means decimal. BitSize 64 forces a 64 bit integer.
port, err := strconv.ParseInt(rawPort, 10, 64)
if err != nil {
// Wrap the error so the caller knows which field failed.
return 0, false, fmt.Errorf("invalid port: %w", err)
}
// ParseBool accepts many truthy and falsy representations.
// It returns true for "1", "t", "T", "TRUE", "true", "True".
debug, err := strconv.ParseBool(rawDebug)
if err != nil {
return 0, false, fmt.Errorf("invalid debug flag: %w", err)
}
// Cast to int only after validation. The value is guaranteed to fit.
return int(port), debug, nil
}
The ParseBool function is intentionally permissive. It accepts "1", "t", "T", "TRUE", "true", and "True" as truthy. It accepts "0", "f", "F", "FALSE", "false", and "False" as falsy. Anything else returns an error. This behavior is useful for environment variables where users type 1 instead of true, but it trips people up when they expect strict validation. Read the documentation before you rely on it for security sensitive flags.
Formatting numbers back to strings
Converting numbers to strings requires more choices than parsing. You need to decide the base, the precision, and whether to use scientific notation. strconv.FormatInt handles integers. strconv.FormatFloat handles floating point values. Both functions accept a format verb that controls the output style.
package main
import (
"fmt"
"strconv"
)
func main() {
// FormatInt uses base 10 by default. The 'd' verb means decimal.
// Base 16 would use 'x' or 'X' for hex digits.
ipOctet := strconv.FormatInt(192, 10)
fmt.Println("octet:", ipOctet)
// FormatFloat takes a verb, precision, and bitSize.
// Verb 'f' means decimal notation. Precision 2 means two decimal places.
price := strconv.FormatFloat(19.995, 'f', 2, 64)
fmt.Println("price:", price)
// Verb 'g' switches to the shorter of 'e' or 'f'.
// It drops trailing zeros and uses scientific notation for large values.
scientific := strconv.FormatFloat(1.23e4, 'g', -1, 64)
fmt.Println("scientific:", scientific)
}
The precision parameter behaves differently depending on the verb. For 'f', it is the number of digits after the decimal point. For 'e' and 'g', it is the total number of significant digits. A precision of -1 tells the function to use the minimum number of digits necessary to represent the value exactly. This is useful for logging raw values without adding artificial rounding.
Public names in Go start with a capital letter. Private names start lowercase. The strconv package follows this rule strictly. ParseInt is exported. parseFloat inside the package is not. You will only ever call the exported functions. Trust the naming convention. It tells you exactly what is available to your code.
Pitfalls and compiler behavior
Base zero auto detection is convenient but dangerous. strconv.ParseInt("0xFF", 0, 64) correctly parses hexadecimal. strconv.ParseInt("0755", 0, 64) parses octal. If your input comes from a user or a legacy system, an accidental leading zero can change the numeric base and corrupt your data. Always use base 10 unless you explicitly expect prefixed numbers.
Bit size truncation happens at the cast site, not in the parser. ParseInt returns a int64. If you cast it to int32 without checking the range, Go silently drops the upper bits. The compiler will not warn you about overflow. You must add a range check or use math.MaxInt32 to validate before casting.
The compiler catches type mismatches early. If you pass a string where an integer is expected, you get cannot use "42" (untyped string constant) as int value in assignment. If you forget to import strconv, you get undefined: strconv. If you assign the error to a variable and never read it, you get err declared and not used. These errors are strict by design. They force you to handle every conversion path before the program runs.
Goroutine leaks happen when a background task waits on a channel that never closes. The same principle applies to error handling. If you ignore a parse error and proceed with a zero value, your program will panic later in a completely unrelated function. Handle the error at the boundary. Fail fast. Log the raw input.
When to reach for strconv
Use strconv.Atoi when you expect a standard decimal string and want the platform native integer size.
Use strconv.ParseInt when you need to specify the numeric base or enforce a fixed bit width like 32 or 64.
Use strconv.ParseUint when you are parsing unsigned values like file sizes or memory addresses and want to reject negative signs.
Use strconv.ParseFloat when your input contains decimal points or scientific notation like 1.23e4.
Use strconv.FormatFloat when you need precise control over decimal places or scientific notation output.
Use strconv.ParseBool when you are reading user input that might contain "1", "t", "TRUE", or "false" and want a single function to normalize it.
Use fmt.Sscanf when you are parsing a structured line with mixed types like 192.168.1.1:8080.
Use encoding/json when you are decoding entire payloads and want automatic type mapping.
Use plain string concatenation when you are building log messages and performance is not the bottleneck.
The standard library favors explicit over implicit. Pick the function that matches your exact requirements. Do not reach for regex when a parser exists. Do not reach for fmt.Sprintf when strconv.FormatInt is faster.