The integer won't fit in the string box
You're writing a CLI tool that generates report filenames. The report number is an integer, but the filename needs to be a string like report_42.txt. You try filename := "report_" + id + ".txt". The compiler stops you. Go doesn't allow mixing types in concatenation. You have to convert the integer to a string first. This explicit step is a feature, not a bug. It forces you to think about the representation. Are you converting for display? For storage? For a URL? The tool you choose depends on the answer.
Go is statically typed. An int is a sequence of bits representing a number. A string is a sequence of bytes representing text. They are fundamentally different data structures. Converting an integer to a string means encoding the numeric value into a sequence of character codes. Go requires this conversion to be explicit. Implicit conversions can hide bugs. If you accidentally concatenate a number where you meant a string, the compiler catches it. This saves you from runtime errors that are hard to debug.
The toolbox: strconv versus fmt
The standard library provides two main packages for this job. strconv handles conversions between basic types and their string representations. It is focused and fast. fmt handles formatted I/O. It can convert types, but it also handles padding, alignment, bases, and complex layouts. fmt is more flexible but carries more overhead. You choose between them based on what you need.
strconv.Itoa is the direct path for converting an int to its decimal string. strconv.FormatInt handles fixed-width integers like int64 and lets you pick the base. fmt.Sprintf is the Swiss Army knife. It uses format verbs to control output. It is slower than strconv because it parses the format string and handles variadic arguments. Use strconv for raw conversion speed. Use fmt when you need formatting control.
Minimal example: the direct conversion
Here's the simplest conversion: take an integer, turn it into its decimal string representation.
package main
import (
"fmt"
"strconv"
)
func main() {
count := 42
// Itoa converts int to decimal string directly.
// It's the fastest path for simple base-10 conversion.
text := strconv.Itoa(count)
// text is now "42"
fmt.Println(text)
}
When you call strconv.Itoa, the runtime allocates a new string buffer. It divides the integer by 10 repeatedly to find digits. If the number is negative, it prepends a minus sign. The result is a new string value. This allocation happens on the heap if the string escapes, or the stack if it doesn't. The compiler optimizes small allocations, but you should know a new string is created every time. The function returns the string by value. Strings are immutable in Go, so the caller gets a safe copy of the data.
Realistic example: formatting and bases
Sometimes you need more than decimal. You might need hex for memory addresses, binary for flags, or zero-padded numbers for IDs. fmt.Sprintf handles these cases with format verbs.
package main
import "fmt"
func main() {
port := 8080
// Sprintf handles formatting verbs.
// %04d pads the number with zeros to width 4.
padded := fmt.Sprintf("%04d", port)
// padded is "8080" (no change needed here, but try 80 -> "0080")
fmt.Println(padded)
ip := 0xC0A80001
// %#x prints hexadecimal with the 0x prefix.
// This is standard for representing memory addresses or flags.
hex := fmt.Sprintf("%#x", ip)
fmt.Println(hex)
flags := 0b10110
// %b prints the binary representation.
// Useful for debugging bitmasks and low-level protocols.
binary := fmt.Sprintf("%b", flags)
fmt.Println(binary)
}
The format string is parsed every time Sprintf runs. The % introduces a verb. d means decimal. x means hex. b means binary. The # flag adds the prefix for hex or octal. The 0 flag enables zero-padding. The width controls the minimum number of characters. This flexibility comes at a cost. The parser has to scan the string, match verbs, and dispatch to formatters. If you only need a plain decimal string, strconv.Itoa skips all that machinery.
Fixed-width integers and FormatInt
When you work with fixed-width integers like int64, strconv.Itoa won't work. Itoa accepts only int. The size of int depends on the platform. On a 32-bit system, int is 32 bits. On a 64-bit system, it is 64 bits. If you have an int64, you must use strconv.FormatInt. This function takes the integer and a base. Base 10 gives decimal. Base 16 gives hex. Base 2 gives binary. Base 0 is a shortcut for decimal.
package main
import (
"fmt"
"strconv"
)
func main() {
timestamp := int64(1715623400)
// FormatInt handles int64 and allows base selection.
// Base 10 produces the standard decimal string.
dec := strconv.FormatInt(timestamp, 10)
fmt.Println(dec)
// Base 16 produces hexadecimal without the 0x prefix.
// Use %#x in Sprintf if you need the prefix.
hex := strconv.FormatInt(timestamp, 16)
fmt.Println(hex)
}
The compiler rejects strconv.Itoa(myInt64) with cannot use myInt64 (type int64) as type int in argument. You need to cast or use FormatInt. Casting int64 to int can truncate data on 32-bit systems. Use FormatInt to be safe. FormatInt also handles negative numbers correctly, adding the minus sign to the output string.
Pitfalls and compiler errors
Go's type system catches mistakes early. If you try to assign an int to a string variable, the compiler rejects it with cannot use x (type int) as type string in assignment. You must use a conversion function. If you forget to import strconv, you get undefined: strconv. If you use fmt.Sprintf with the wrong verb, you might get unexpected output, but it won't crash. The verb %s expects a string. Passing an int to %s results in the string representation of the int, but the behavior depends on the implementation. Relying on this is fragile. Use %d for integers.
Performance is a common pitfall. In a tight loop converting millions of integers, fmt.Sprintf is slower. It parses the format string and handles variadic arguments. strconv.Itoa does exactly one thing. If profiling shows string conversion is a bottleneck, switch to strconv. The difference can be significant in high-throughput services.
Another pitfall is memory allocation. Every conversion creates a new string. If you are building a large string from many integers, avoid repeated concatenation. Use bytes.Buffer. It pre-allocates memory and reduces allocations. strconv.AppendInt writes directly to a byte slice. This is the ultimate optimization for hot paths.
package main
import (
"fmt"
"strconv"
)
func main() {
// Pre-allocate a buffer to hold the result.
// This avoids repeated allocations during concatenation.
buf := make([]byte, 0, 64)
// AppendInt writes the integer to the buffer and returns the extended slice.
// It's the fastest way to build strings from numbers in tight loops.
buf = strconv.AppendInt(buf, 123, 10)
buf = append(buf, '/')
buf = strconv.AppendInt(buf, 456, 10)
// Convert the byte slice to a string once at the end.
result := string(buf)
fmt.Println(result)
}
Conventions and community wisdom
Trust gofmt. The community uses gofmt to format all Go code. It removes debates about indentation and braces. Run gofmt on save. Focus your energy on logic, not style. Most editors integrate gofmt and run it automatically.
Don't pass a *string to avoid allocation. Strings are already cheap to pass by value. A string header is two words: a pointer and a length. Passing a pointer to a string adds an indirection without saving memory. Pass the string directly. The compiler handles small string headers efficiently.
If you need to discard a return value, use the blank identifier _. For example, result, _ := strconv.Atoi(text) says "I considered the error and chose to drop it". Use this sparingly with errors. Ignoring errors is a common source of bugs. The community prefers if err != nil { return err } to make the unhappy path visible.
Decision matrix
Use strconv.Itoa when you need the decimal string of an int and performance matters. Use strconv.FormatInt when you have a fixed-width integer like int64 or need a specific base other than 10. Use fmt.Sprintf when you need to embed the number in a larger formatted string, add padding, or change the base with a prefix. Use fmt.Sprint when you are already building a string with multiple values and the performance difference is negligible. Use strconv.AppendInt when you are constructing a large string in a hot path and want to minimize allocations. Use plain string concatenation with strconv.Itoa when you are building a simple path or key and readability is the priority.
Go doesn't guess. You convert explicitly. Pick the tool that matches your needs.