Strings and Strconv
Strings and Strconv
strings transforms and inspects string values. strconv converts between strings and numbers. The boring default is: use the strings package instead of hand-rolling loops, and always check the error from strconv.Atoi or strconv.ParseFloat. A string is immutable bytes in Go; mutating a “string” means building a new one.
Mental model
A Go string is a read-only byte slice with a known length. Index s[i] gives a byte, not a rune. range s gives runes. len(s) is bytes. utf8.RuneCountInString(s) is code points.
strings.Contains, strings.HasPrefix, strings.HasSuffix test membership. strings.Split and strings.Join are inverses. strings.TrimSpace removes leading and trailing whitespace. strings.ReplaceAll replaces all occurrences. strings.ToUpper / strings.ToLower fold case. strings.Builder builds incrementally without repeated allocation.
strconv.Atoi(s) parses a decimal integer and returns (int, error). strconv.Itoa(n) formats one. strconv.ParseFloat, strconv.ParseBool, strconv.ParseInt handle the other base types. strconv.FormatFloat and friends go the other direction.
Worked examples
Case 1: Inspect and transform
Save as note_tidy.go. A ticket note arrives with trailing whitespace and mixed case. Normalize it.
// note_tidy.go
package main
import (
"fmt"
"strings"
)
func main() {
raw := " NO Onions "
clean := strings.TrimSpace(raw)
lower := strings.ToLower(clean)
fmt.Println(clean)
fmt.Println(lower)
fmt.Println(strings.HasPrefix(lower, "no"))
fmt.Println(strings.Contains(lower, "onion"))
}Run:
go run note_tidy.goOutput:
NO Onions
no onions
true
true
TrimSpace is the first step when reading user input. Test the cleaned version, not the raw one.
Case 2: Split and Join
Save as tag_split.go. Tags arrive comma-separated. Join them back with a different separator.
// tag_split.go
package main
import (
"fmt"
"strings"
)
func main() {
line := "vegan,gluten-free,no-nuts"
tags := strings.Split(line, ",")
fmt.Println(len(tags))
for i, t := range tags {
fmt.Println(i, t)
}
fmt.Println(strings.Join(tags, " | "))
}Run:
go run tag_split.goOutput:
3
0 vegan
1 gluten-free
2 no-nuts
vegan | gluten-free | no-nuts
Split on an empty string returns one element (the whole string). strings.Fields splits on any whitespace and drops empty tokens — use it for space-separated input instead of Split(" ").
Case 3: strings.Builder
Save as receipt.go. Build a multi-line receipt without string concatenation.
// receipt.go
package main
import (
"fmt"
"strings"
)
type Item struct {
Name string
Price int
}
func receipt(items []Item) string {
var b strings.Builder
b.WriteString("--- receipt ---\n")
total := 0
for _, item := range items {
fmt.Fprintf(&b, "%-15s %3d\n", item.Name, item.Price)
total += item.Price
}
fmt.Fprintf(&b, "%-15s %3d\n", "total", total)
return b.String()
}
func main() {
items := []Item{
{"soup", 8},
{"coffee", 4},
{"cake", 6},
}
fmt.Print(receipt(items))
}Run:
go run receipt.goOutput:
--- receipt ---
soup 8
coffee 4
cake 6
total 18
strings.Builder is the right tool when you are assembling with fmt.Fprintf. bytes.Buffer works too, but Builder cannot be used as a Reader — it is write-only. Use Buffer if downstream code needs an io.Reader.
Case 4: strconv.Atoi and Itoa
Save as ticket_parse.go. A ticket ID arrives as a string from a URL parameter. Parse it; reject junk.
// ticket_parse.go
package main
import (
"fmt"
"strconv"
)
func parseID(s string) (int, error) {
return strconv.Atoi(s)
}
func main() {
good, err := parseID("42")
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("id", good)
_, err = parseID("four")
fmt.Println("parse error:", err)
fmt.Println(strconv.Itoa(good + 1))
}Run:
go run ticket_parse.goOutput:
id 42
parse error: strconv.Atoi: parsing "four": invalid syntax
43
Atoi is ParseInt(s, 10, 0) — base 10, fit in an int. When you need a specific width or base, use ParseInt directly. The error is always non-nil on failure; you do not need a separate “valid” bool.
Case 5: ParseFloat and FormatFloat
Save as price_parse.go. A price arrives as a decimal string. Parse it, add tax, format back.
// price_parse.go
package main
import (
"fmt"
"strconv"
)
func main() {
raw := "12.50"
price, err := strconv.ParseFloat(raw, 64)
if err != nil {
fmt.Println(err)
return
}
withTax := price * 1.10
formatted := strconv.FormatFloat(withTax, 'f', 2, 64)
fmt.Println("price:", price)
fmt.Println("with tax:", formatted)
}Run:
go run price_parse.goOutput:
price: 12.5
with tax: 13.75
'f' means decimal, no exponent. 2 is the number of digits after the decimal. 64 matches the float64 storage. For money in a real desk system, use integer cents or a fixed-precision type — not float64 arithmetic for the canonical value.
The trap
Save as count_rune.go. len counts bytes, not characters. A ticket note in UTF-8 may have fewer runes than bytes.
// count_rune.go
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
note := "jalapeño"
fmt.Println("bytes :", len(note))
fmt.Println("runes :", utf8.RuneCountInString(note))
for i, r := range note {
fmt.Printf("i=%d r=%c\n", i, r)
}
}Run:
go run count_rune.goOutput:
bytes : 9
runes : 8
i=0 r=j
i=1 r=a
i=2 r=l
i=3 r=a
i=4 r=p
i=5 r=e
i=6 r=ñ
i=8 r=o
The index jumps from 6 to 8 because ñ is two bytes. s[i] at index 7 would be the second byte of ñ — not a valid code point. Use range or utf8.DecodeRuneInString to walk code points. Slice by byte only when you know you are dealing with ASCII.
The boring rule
strings.TrimSpacebefore comparing user input.strings.Fieldsfor whitespace-separated tokens;strings.Splitfor a fixed separator.strings.Builderfor incremental assembly;strings.Joinwhen you already have a slice.strconv.Atoi/Itoafor decimal integers.ParseFloat/FormatFloatfor floats. Always check the error.lenis bytes.utf8.RuneCountInStringis code points. Userangeto iterate.- Do not
+concatenate in a loop; useBuilder.
Try this
- In
note_tidy.go, usestrings.ReplaceAllto replace all spaces with underscores before printing. - In
tag_split.go, usestrings.Fieldson" vegan no-nuts "and print the resulting slice length. - In
ticket_parse.go, tryparseID("-5")— print the result. Negative IDs parse fine; you must reject them with a range check. - In
price_parse.go, change'f'to'e'inFormatFloat. Print the result and notice the scientific notation.