Last Updated: May 17, 2026
User input arrives as strings. Prices typed into a checkout form, quantities pulled from a CSV row, product IDs read from a URL path, boolean flags from a query string. None of it is a number or a boolean yet, and Go won't quietly convert it for you. The strconv package is the standard library's answer to that problem: a focused set of functions for parsing strings into numeric and boolean types, and formatting them back out.
You might wonder why Go bothers with a dedicated package when fmt.Sprintf("%d", n) and fmt.Sscanf exist. The short answer is speed and clarity. strconv functions know exactly one type and one direction, so they skip the format-string parsing that fmt has to do on every call. The longer answer is that strconv returns errors in a structured way (strconv.NumError), while fmt.Sscanf returns a generic error and a count of values parsed. When you're doing one conversion and you care about whether it succeeded, strconv is the right tool.
We'll come back to the fmt comparison at the end. Most of this lesson is about the conversions themselves.
That's the shape of every strconv parse function. You hand it a string, you get back a value and an error. Check the error, then use the value.
The two most common functions in strconv are Atoi (ASCII to integer) and Itoa (integer to ASCII). The names are inherited from C, but the behavior is pure Go: explicit error handling, fixed base 10, no surprises.
Atoi returns an int, not an int64 or int32. On a 64-bit platform that's 64 bits, and on a 32-bit platform it's 32 bits. If you need a specific width or a different base, use ParseInt instead (covered in a moment).
Itoa is the inverse: it takes an int and returns its decimal representation as a string. It never fails, so there's no error to check. Internally Itoa is just FormatInt(int64(n), 10), but the dedicated function is faster because the compiler can specialize it.
Cost: strconv.Itoa(n) is roughly 2-3x faster than fmt.Sprintf("%d", n) in microbenchmarks. The fmt version has to parse the format string and dispatch through reflect. If you're building log lines or labels in a hot loop, prefer Itoa.
What does "error" actually look like? Try parsing something that isn't a number.
Notice the error message format: function name, the offending input quoted, and the reason. That structure isn't a coincidence. strconv errors are values of type strconv.NumError, which we'll dig into in the error-handling section below.
Atoi and Itoa are the everyday tools. When you need control over the base or the target bit width, reach for ParseInt and FormatInt.
Three things stand out compared to Atoi:
int64, regardless of bitSize. You convert it down to the type you actually want.base selects the numeric base. Valid values are 2, 8, 10, 16, or 0 (auto-detect).bitSize tells the parser what range to accept. 8 means "must fit in int8" (-128 to 127), 16 for int16, 32 for int32, 64 for int64. Use 0 as a shorthand for "the platform's int width".Here's a tour of the common bases.
Every one of those represents the same number, 42, in a different base. Base 0 is the one to remember: it inspects the prefix and picks for you. 0x or 0X means hex, 0b or 0B means binary, 0o or 0O means octal, and a plain leading 0 followed by digits also means octal (a legacy from C).
This is your reference table for the base parameter.
base | Accepts | Example |
|---|---|---|
2 | 0 and 1 | "101010" -> 42 |
8 | 0-7 | "52" -> 42 |
10 | 0-9 | "42" -> 42 |
16 | 0-9, a-f, A-F | "2A" -> 42 |
0 | Auto-detect from 0x, 0b, 0o, or leading 0 | "0x2A" -> 42 |
The bitSize argument is where overflow gets caught. If the parsed value doesn't fit in the requested width, you get an error instead of silent truncation.
The bitSize check is what makes ParseInt safer than parsing into an int64 and casting yourself. With the cast, a value like 2147483648 would silently wrap around. With bitSize=32, you get an explicit value out of range error.
Cost: ParseInt with bitSize=0 is equivalent to using the platform's int width. On 64-bit platforms this is the same as bitSize=64. There's no extra cost to specifying a smaller bitSize, the check is a single comparison after parsing.
FormatInt is the inverse and is simpler since it never fails.
A few details worth knowing. FormatInt produces lowercase hex digits. If you want uppercase (FF instead of ff), wrap with strings.ToUpper or use fmt.Sprintf("%X", n). Bases outside 2-36 cause FormatInt to panic, so don't pass arbitrary user input as the base argument.
The unsigned versions follow the same pattern. ParseUint returns uint64 and rejects negative inputs. FormatUint takes a uint64 and a base.
Use ParseUint when your domain genuinely doesn't include negatives. Order counts, page views, byte sizes, port numbers. Doing the work in uint64 means you can't accidentally pass a negative number to a function that doesn't expect one.
Floating-point conversions are where most of the trouble lives. Prices, ratings, percentages, weights: all the typed-in numbers in an online store that aren't whole.
ParseFloat is the easier of the two. bitSize is either 32 or 64, and it controls how the result rounds. The return type is always float64, but with bitSize=32 the value is the nearest representable float32 value converted back to float64. Use 64 unless you specifically need 32-bit float behavior.
Notice ParseFloat accepts "Inf", "-Inf", and "NaN" (case-insensitive). It does not, however, accept currency symbols. Parsing "$29.99" directly will fail with invalid syntax. If users type a $, strip it first.
That pattern, normalize the string first then parse, is what most real-world price-parsing code looks like.
FormatFloat is denser. It takes four arguments: the value, a format byte, a precision, and a bit size.
The format byte controls the output shape. Here's the reference table.
fmt byte | Shape | Example for 29.99 | Notes |
|---|---|---|---|
'f' | Fixed-point, no exponent | "29.99" | The usual choice for prices |
'e' | Scientific, lowercase e | "2.999e+01" | Lowercase exponent |
'E' | Scientific, uppercase E | "2.999E+01" | Uppercase exponent |
'g' | 'e' for large or small, 'f' otherwise | "29.99" | "Whichever looks better" |
'G' | Like 'g' but uppercase | "29.99" | Same logic, uppercase E |
'b' | Binary exponent | "8443137936750346p-48" | Lossless, but ugly |
'x' | Hex mantissa, decimal exponent | "0x1.df5c28f5c28f6p+04" | Lossless and compact |
'X' | Same as 'x' but uppercase | "0X1.DF5C28F5C28F6P+04" |
The prec argument changes meaning by format byte:
'f', 'e', 'E': number of digits after the decimal point.'g', 'G': maximum number of significant digits.'b', 'x', 'X': precision in the exponent form, usually -1.And prec = -1 is the special "shortest representation that round-trips" mode. It produces the fewest digits such that ParseFloat of the result gives back the exact same float. This is what you want when storing or transmitting a number without losing information.
That last line is the canonical floating-point demo: 0.1 + 0.2 isn't actually 0.3 in binary. With prec=2 it rounds to "0.30" and looks fine. With prec=-1 you see the full mess. Use fixed precision for display, use -1 when you need to preserve the exact value.
Cost: FormatFloat allocates a string each call. In hot paths that build many float strings (analytics dashboards, log streaming), pool the conversions or use strconv.AppendFloat, which writes into a byte slice you provide.
The bitSize argument on FormatFloat should match the source. Pass 32 if the value came from a float32, otherwise pass 64. Mismatching bitSize gives results that don't quite round-trip.
Boolean conversions are the simplest of the bunch. There are only two values and a small set of accepted strings.
ParseBool accepts exactly these inputs:
Accepted as true | Accepted as false |
|---|---|
"1", "t", "T", "TRUE", "true", "True" | "0", "f", "F", "FALSE", "false", "False" |
Everything else returns an error.
Two surprises worth flagging. "yes" and "no" are not accepted, even though they look bool-ish. Neither is "on" and "off". If your form lets users type those, you need to normalize them yourself before calling ParseBool. The second surprise is whitespace: "True " with a trailing space fails. ParseBool does not trim.
FormatBool is trivial. It returns either "true" or "false" exactly.
This is useful when you're building a query string or a config key by hand. For typical print-and-debug, fmt.Println(b) is shorter and produces the same output.
A practical pattern: parse a user-supplied flag string with a wider set of accepted forms.
The wrapper handles the common informal forms and delegates to ParseBool for the standard ones. Note the last line: strconv.ParseBool(" 1 ") failed because of the whitespace. The wrapper didn't trim before calling, only inside its switch. A real wrapper would trim once at the top and pass the trimmed string to ParseBool too.
The quoting functions are about Go syntax, not user input. strconv.Quote takes a string and returns a Go-syntax double-quoted string literal, with special characters escaped. strconv.Unquote does the reverse.
Quote is useful when you need to print a string in a form you could paste back into Go source, or when you're generating code, or when you want to make whitespace visible in a debug log. It escapes quotes, newlines, tabs, backslashes, and any non-printable runes. It also surrounds the result with double quotes.
Unquote does the reverse, accepting a Go string literal (double-quoted, single-quoted, or backtick-quoted) and returning the value.
QuoteRune is the rune-only version of Quote, producing single-quoted output:
AppendQuote is the allocation-free variant. Instead of returning a new string, it appends the quoted form to an existing []byte. This is the shape every strconv formatting function offers (AppendInt, AppendFloat, AppendBool, AppendQuote) for code that's building up a buffer.
For a single conversion this is more work than strconv.Quote(...). For a tight loop building hundreds of lines into one buffer, the Append* functions are noticeably faster because they don't allocate a new string each call.
Cost: the Append* family writes into a caller-provided byte slice, avoiding the allocation that Format* and Quote would make. If you're building one string in a hot loop, prefer strconv.AppendInt(buf, n, 10) plus a final string(buf).
Every parsing function in strconv returns an error of type *strconv.NumError when it fails. The type has three fields you can inspect.
Two sentinel errors live in the package:
| Sentinel | When | Example |
|---|---|---|
strconv.ErrSyntax | Input couldn't be parsed at all | Atoi("twelve") |
strconv.ErrRange | Input parsed but doesn't fit in the requested type | ParseInt("9999999999", 10, 8) |
You compare against these with errors.Is, which is the standard way to test error identity in Go.
This distinction matters in real applications. If a user types "twelve" you want to say "please enter a number". If a user types a value that's a valid number but too large for the field, you want to say "that's too big". Both come back from strconv with the same outer error message, but errors.Is lets you tell them apart cleanly.
You can also type-assert the error if you need the input string back from the error itself.
errors.As extracts the typed error from a wrapped chain. The fields tell you exactly which function rejected which input and why.
Here's the parsing-with-error-handling flow as a diagram. It applies to every strconv.Parse* and Atoi function.
There are three terminal states: a valid value, a syntax error, or a range error. Every strconv parse function follows that same shape, which is what makes the error-handling pattern uniform.
Go forces you to deal with the error because parsing failure is genuinely common. Strings come from users, files, network requests, environment variables. A function that just returned an int would have to either panic on bad input or quietly return zero, and both of those choices are worse than making you write if err != nil. Treat every strconv.Parse* call as needing an explicit check.
To pull the pieces together, here's a small program that takes a CSV-style row of order data and parses each field with the right strconv function. It mirrors what a real importer might do.
Four different strconv calls in one function, each wrapped with fmt.Errorf and %w to preserve the underlying error. The last row demonstrates that a single bad field is enough to fail the whole row, which is usually what you want for data import. The %w verb keeps the chain intact, so a caller can still use errors.Is(err, strconv.ErrSyntax) to inspect the cause.
A common question: when should you reach for strconv and when should you use fmt? They overlap, but each has a sweet spot.
| Task | strconv | fmt |
|---|---|---|
| One value, one type | strconv.Itoa(n) | fmt.Sprintf("%d", n) |
| Multiple values, layout | Glue strings yourself | fmt.Sprintf("Order %d total $%.2f", id, total) |
| Parsing one value | strconv.Atoi(s) | fmt.Sscanf(s, "%d", &n) |
| Tokenizing structured input | Parse field by field | fmt.Sscanf("Order 42", "Order %d", &n) |
| Custom base for integers | strconv.FormatInt(n, 16) | fmt.Sprintf("%x", n) |
| Reflection-driven formatting | Not supported | fmt.Sprintf("%v", anyValue) |
The rough rule of thumb: use strconv when you're doing a single conversion of a known type, and use fmt when you're formatting multiple values together with surrounding text. The performance difference exists but rarely matters outside of hot loops.
A quick comparison in code:
strconv gave us the number-as-string. fmt.Sprintf with %05d did the same plus zero-padding to width 5, which strconv has no way to express directly. If you find yourself building a format string just to convert one value, switch to strconv. If you're stitching multiple values into a sentence, stay in fmt.
The same logic applies in reverse. strconv.Atoi is a single-value parser. fmt.Sscanf can pull multiple values from a structured string in one call.
Sscanf returns the count of values it parsed plus an error. It's the right tool when you have a known string template and want to pull several values out in one pass. For single-value parsing, strconv.Atoi is faster, returns a more useful error, and is what idiomatic Go code uses.
Cost: in microbenchmarks, strconv.Atoi(s) is roughly an order of magnitude faster than fmt.Sscanf(s, "%d", &n) because Sscanf parses the format string and reflects on the argument types. In normal application code the difference is negligible; in a tight parsing loop it matters.
strconv.Atoi and strconv.Itoa are the everyday string-to-int and int-to-string functions. Use them when you're working in base 10 and a plain int.strconv.ParseInt and strconv.ParseUint take an explicit base (2, 8, 10, 16, or 0 for auto-detect) and bitSize. The bitSize argument catches overflow at parse time instead of silently wrapping.strconv.ParseFloat and strconv.FormatFloat handle floats. Use prec = -1 for shortest round-trip output, and a fixed precision like 2 for display.strconv.ParseBool accepts a narrow, fixed set of strings ("1", "t", "true", and so on). Normalize informal forms like "yes" and "on" yourself before calling it.strconv.Quote and strconv.Unquote convert between strings and their Go-syntax literal form, including escapes. The Append* variants write into a buffer without allocating.*strconv.NumError wrapping either strconv.ErrSyntax (not a number) or strconv.ErrRange (number out of range). Use errors.Is to tell them apart.strconv is faster than fmt for single-value conversions because it skips format-string parsing and reflection. Prefer fmt only when you're formatting multiple values together with layout.In the next lesson, strings.Builder, we'll look at how to assemble a long string from many small pieces efficiently, which often comes up right after parsing a batch of values and turning them back into formatted output.