When strings become numbers and back
You are building a CLI tool that reads a configuration file. The file stores the server port as the string "8080". Your network code needs an int to bind the listener. Later, you want to log the active port, so you need to turn that integer back into a string for the log line. Go does not do implicit conversions. You have to ask for the conversion explicitly. The strconv package handles this work. It provides functions to parse strings into numbers and format numbers into strings. The functions are explicit about base, bit size, and precision. This explicitness prevents subtle bugs where a number is misinterpreted or truncated.
How conversion works
Computers store integers as sequences of bits. They store strings as sequences of bytes representing characters. These are different representations of the same idea. Converting between them requires parsing the characters to build the number, or formatting the number to produce characters. Go separates these concerns. The strconv package provides the functions. You specify the base (usually 10 for decimal), the bit size (64 for int64), and the precision for floats.
The base determines the numeral system. Base 10 is standard decimal. Base 16 produces hexadecimal digits. Base 2 produces binary. Base 0 allows the function to detect the base from a prefix like 0x or 0b. The bit size limits the range of the result. It ensures the parsed number fits in the target type. The precision controls how many digits appear in the output for floating-point numbers.
Conversion is explicit. Trust the types.
Minimal round-trip
Here is the basic round-trip: integer to string, string back to integer.
package main
import (
"fmt"
"strconv"
)
func main() {
// Start with an int64 value.
// Go requires explicit types for strconv functions.
num := int64(42)
// FormatInt converts the integer to a string in base 10.
// The second argument is the base. 10 means decimal.
str := strconv.FormatInt(num, 10)
fmt.Println(str) // prints: 42
// ParseInt reads the string and returns an int64.
// Arguments: string, base, bitSize.
// Bit size 64 means the result fits in an int64.
parsed, err := strconv.ParseInt(str, 10, 64)
if err != nil {
fmt.Println("parse failed:", err)
return
}
fmt.Println(parsed) // prints: 42
}
FormatInt takes the integer and produces a new string. It allocates memory for the characters. The base argument tells the function which numeral system to use. ParseInt does the reverse. It scans the string character by character. It rejects leading whitespace unless you trim it first. It checks that every character is valid for the base. It calculates the numeric value. If the value exceeds the range allowed by the bit size, it returns an error. The bit size argument is a safety net. You ask for 64, and the function guarantees the result fits in an int64. If the string represents a number too large for 64 bits, you get an overflow error. The function returns two values: the parsed number and an error. The error is nil on success. This pattern forces the caller to handle failure.
Parsing configuration values
Here is how this looks in a configuration loader that handles multiple fields.
// Config holds parsed settings.
type Config struct {
Port int
}
// ParsePort extracts the port number from a raw string value.
// It returns an error if the value is not a valid integer.
func ParsePort(raw string) (int, error) {
// ParseInt handles the conversion.
// Base 10 for decimal input.
// Bit size 64 ensures the result fits in int64.
p, err := strconv.ParseInt(raw, 10, 64)
if err != nil {
// Wrap the error to provide context.
// The %w verb preserves the error chain.
return 0, fmt.Errorf("invalid port %q: %w", raw, err)
}
// Narrow int64 to int.
// This is safe because we validated the range above.
// In production code, check p > 0 and p < 65536.
return int(p), nil
}
ParseInt returns an int64. If you try to assign the result directly to an int variable, the compiler rejects the code with cannot use strconv.ParseInt(...) (value of type int64) as int value in assignment. You must cast the result or store it in an int64. ParseInt also fails on whitespace. The string " 42" causes an error. You need to trim spaces first. The error message looks like strconv.ParseInt: parsing " 42": invalid syntax. Large numbers trigger overflow. Parsing "99999999999999999999" with bit size 64 returns strconv.ParseInt: parsing "99999999999999999999": value out of range.
Trim input. Check ranges. Validate before you trust.
Unsigned integers and safety
Use strconv.ParseUint when you expect a non-negative integer. Port numbers, file sizes, and counts are naturally unsigned. ParseUint returns a uint64. It rejects strings that start with a minus sign. This catches accidental negative inputs. If you use ParseInt for a port number, a string like "-1" parses successfully. The result is -1. Your code might crash later when it tries to bind to a negative port. ParseUint fails immediately with strconv.ParseUint: parsing "-1": invalid syntax. This fails fast. It prevents invalid data from propagating.
Float precision and formats
FormatFloat has precision traps. If you format a float with strconv.FormatFloat(val, 'f', -1, 64), the -1 means "use the smallest number of digits necessary". This can produce scientific notation for very small or large numbers. If you need fixed decimal places, pass a positive precision like 2. The format verb 'f' forces decimal notation. The verb 'e' forces scientific notation. The verb 'g' switches between them based on the exponent.
ParseFloat takes a precision argument that refers to the bit size of the floating-point type, not the number of decimal places. Pass 64 for float64. Pass 32 for float32. The function returns a float64. If you need a float32, cast the result. Be aware that casting a float64 to float32 can lose precision if the value has more significant digits than float32 can hold.
Performance with byte slices
When you are building a string in a loop, repeated concatenation allocates memory. strconv.AppendInt writes directly to a byte slice. This avoids intermediate string allocations.
// BuildString demonstrates efficient string construction.
// It uses a byte slice buffer to avoid allocations.
func BuildString(values []int64) string {
// Pre-allocate a buffer.
// Estimate size to reduce resizing.
buf := make([]byte, 0, len(values)*10)
for _, v := range values {
// AppendInt writes the number to the buffer.
// It returns the extended slice.
// Base 10 for decimal output.
buf = strconv.AppendInt(buf, v, 10)
// Append a separator.
buf = append(buf, ',')
}
// Convert the byte slice to a string.
// This copies the data once.
return string(buf)
}
AppendInt is faster than FormatInt plus concatenation. It avoids allocation. In a loop processing millions of numbers, AppendInt can reduce memory pressure significantly. The garbage collector has less work. This matters in high-throughput services.
Convenience functions
strconv.Atoi is a shortcut for ParseInt(s, 10, 0). The bit size 0 tells the function to return a value that fits in the native int type. The size of int depends on the architecture. It is 64 bits on most modern machines. It is 32 bits on some embedded systems. Atoi is convenient for quick scripts. It hides the bit size. In library code, prefer ParseInt with an explicit bit size. Explicit bit sizes make the code portable. They document the expected range. strconv.Itoa is the reverse shortcut. It calls FormatInt with base 10. It takes an int and returns a string. Use Itoa when you have an int and do not care about the base.
Error inspection
strconv errors implement the *strconv.NumError type. You can inspect the error to distinguish between syntax errors and range errors. A syntax error means the string is malformed. A range error means the string is valid but too big. This distinction helps in logging or user feedback. If the user typed abc, it is a syntax error. If the user typed 99999999999999999999, it is a range error. You can suggest different fixes. Check the error with err.(*strconv.NumError).Err == strconv.ErrRange.
Convention and style
The community prefers strconv for pure conversions. fmt.Sprintf is heavier because it parses the format string at runtime. Use strconv for performance-critical loops. The error handling pattern if err != nil { return err } is standard. The boilerplate makes the failure path obvious. Do not hide the error. gofmt ensures the indentation is consistent. Trust the formatter. Do not argue about spacing. Let the tool decide.
Pick the tool that matches the job. Performance matters in hot paths.
When to use what
Use strconv.FormatInt when you need to convert an integer to a string for display or serialization. Use strconv.ParseInt when you receive a numeric string from user input, a file, or an API and need a typed integer. Use strconv.FormatFloat when you need to convert a floating-point number to a string with control over precision and notation. Use strconv.ParseFloat when you need to parse decimal numbers that might contain fractional parts. Use strconv.ParseUint when you expect a non-negative value and want to reject negative signs automatically. Use fmt.Sprintf with %d or %f when you are building a complex string that mixes text and numbers, rather than converting a single value. Use strconv.Itoa when you have an int and want a quick string conversion without specifying the base. Use strconv.Atoi when you have a decimal string and want a quick int result, accepting the default 64-bit parsing under the hood. Use strconv.AppendInt when you are appending numbers to a byte slice in a loop to avoid allocations.