Strings and Strconv

Updated

September 13, 2026

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.go

Output:

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.go

Output:

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.go

Output:

--- 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.go

Output:

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.go

Output:

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.go

Output:

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.TrimSpace before comparing user input.
  • strings.Fields for whitespace-separated tokens; strings.Split for a fixed separator.
  • strings.Builder for incremental assembly; strings.Join when you already have a slice.
  • strconv.Atoi / Itoa for decimal integers. ParseFloat / FormatFloat for floats. Always check the error.
  • len is bytes. utf8.RuneCountInString is code points. Use range to iterate.
  • Do not + concatenate in a loop; use Builder.

Try this

  1. In note_tidy.go, use strings.ReplaceAll to replace all spaces with underscores before printing.
  2. In tag_split.go, use strings.Fields on " vegan no-nuts " and print the resulting slice length.
  3. In ticket_parse.go, try parseID("-5") — print the result. Negative IDs parse fine; you must reject them with a range check.
  4. In price_parse.go, change 'f' to 'e' in FormatFloat. Print the result and notice the scientific notation.