Skip to content

Table of Contents

Strings, Runes & Bytes

Contents

The three types

Type Element Mutable Use for
string bytes (immutable) no text, map keys, comparisons
[]byte uint8 yes I/O, building, mutation
[]rune int32 (code point) yes per-character logic
s := "héllo"        // immutable, UTF-8 encoded
len(s)              // 6  (bytes, not characters — é is 2 bytes)
utf8.RuneCountInString(s) // 5 (characters)

Strings are immutable: s[0] = 'x' is a compile error.

UTF-8 fundamentals

Go source is UTF-8; string literals are UTF-8 byte sequences. A rune is one Unicode code point (1–4 bytes in UTF-8).

import "unicode/utf8"

s := "€"
len(s)                      // 3 (bytes)
utf8.RuneCountInString(s)   // 1
r, size := utf8.DecodeRuneInString(s) // r='€', size=3
utf8.ValidString(s)         // true

'A'                         // rune literal = 65
'世'                        // rune literal = 19990
"é" == "é"             // true

Indexing vs ranging

s := "héllo"

s[0]                        // byte 'h' (104), type byte
s[1]                        // byte 0xC3 — first half of 'é'!

// Indexing gives BYTES. Ranging gives RUNES with byte offsets:
for i, r := range s {
    fmt.Printf("%d:%c ", i, r) // 0:h 1:é 3:l 4:l 5:o  (note index jump)
}

// Convert to []rune for random character access:
rs := []rune(s)
rs[1]                       // 'é'
string(rs[1:])              // "éllo"

Conversions

b := []byte("hi")           // string -> []byte (copies)
s := string(b)              // []byte -> string (copies)
rs := []rune("héllo")       // string -> []rune
s2 := string(rs)            // []rune -> string

string(65)                  // "A"  (rune->string), vet warns; prefer:
string(rune(65))            // "A"
strconv.Itoa(65)            // "65" (int->decimal string)

string([]byte{104, 105})    // "hi"

Each conversion typically allocates and copies. In hot loops, work in []byte and avoid round-tripping.

strings package

import "strings"

strings.Contains("seafood", "foo")      // true
strings.ContainsRune("abc", 'b')        // true
strings.HasPrefix("go.mod", "go")       // true
strings.HasSuffix("a.go", ".go")        // true
strings.Index("chicken", "ken")         // 4 (-1 if absent)
strings.Count("cheese", "e")            // 3
strings.Replace("oink", "o", "0", -1)   // "0ink" (-1 = all)
strings.ReplaceAll("aaa", "a", "b")     // "bbb"
strings.Split("a,b,c", ",")             // ["a" "b" "c"]
strings.SplitN("a,b,c", ",", 2)         // ["a" "b,c"]
strings.Fields("  a  b ")               // ["a" "b"] (split on whitespace)
strings.Join([]string{"a","b"}, "-")    // "a-b"
strings.ToUpper("go"); strings.ToLower("GO")
strings.Title("hi there")               // deprecated; use cases.Title
strings.TrimSpace("  hi  ")             // "hi"
strings.Trim("xxhixx", "x")             // "hi"
strings.TrimPrefix("v1.2", "v")         // "1.2"
strings.TrimSuffix("a.go", ".go")       // "a"
strings.Repeat("ab", 3)                 // "ababab"
strings.EqualFold("Go", "GO")           // true (case-insensitive)
strings.Map(func(r rune) rune {...}, s) // transform runes
before, after, found := strings.Cut("k=v", "=") // "k","v",true (Go 1.18+)

r := strings.NewReader("data")          // io.Reader over a string

strings.Builder

Efficient string concatenation — avoids quadratic copying of += in loops.

var b strings.Builder
b.Grow(64)                  // optional capacity hint
for i := 0; i < 3; i++ {
    fmt.Fprintf(&b, "%d,", i)
    b.WriteString("x")
    b.WriteByte('\n')
    b.WriteRune('€')
}
result := b.String()        // no extra copy
b.Len()                     // current size
b.Reset()                   // reuse

Do not copy a strings.Builder after first use (it holds an internal []byte).

strconv package

import "strconv"

strconv.Itoa(42)                    // "42"
strconv.Atoi("42")                  // 42, nil
strconv.ParseInt("ff", 16, 64)      // 255, nil  (base 16, bitSize 64)
strconv.ParseInt("0x1f", 0, 64)     // base 0 = infer from prefix
strconv.ParseFloat("3.14", 64)      // 3.14, nil
strconv.ParseBool("true")           // true, nil  (1,t,T,TRUE,...)
strconv.FormatInt(255, 16)          // "ff"
strconv.FormatFloat(3.14, 'f', 2, 64) // "3.14"
strconv.Quote(`a"b`)                // "\"a\\\"b\""
strconv.Unquote(`"hi"`)             // "hi", nil
strconv.AppendInt(buf, 42, 10)      // append without alloc

Atoi/Parse* return a *strconv.NumError on failure; check err.

bytes package

Mirror of strings but for []byte, plus a Buffer.

import "bytes"

bytes.Contains(b, []byte("foo"))
bytes.Equal(a, b)                   // fast byte comparison
bytes.Split(b, []byte(","))
bytes.TrimSpace(b)
bytes.ToUpper(b)

var buf bytes.Buffer                // implements io.Reader and io.Writer
buf.WriteString("hello ")
fmt.Fprintf(&buf, "%d", 42)
buf.Write([]byte{'!'})
buf.String()                        // "hello 42!"
buf.Bytes()                         // underlying bytes (no copy)
json.NewEncoder(&buf).Encode(v)     // common with encoders

fmt verbs

type P struct{ X, Y int }
p := P{1, 2}

fmt.Printf("%v\n",  p)   // {1 2}            default
fmt.Printf("%+v\n", p)   // {X:1 Y:2}        with field names
fmt.Printf("%#v\n", p)   // main.P{X:1, Y:2} Go-syntax representation
fmt.Printf("%T\n",  p)   // main.P           the type

fmt.Printf("%d %b %o %x %X\n", 255, 255, 255, 255, 255) // 255 11111111 377 ff FF
fmt.Printf("%c %q %U\n", 65, "hi", '€')   // A "hi" U+20AC
fmt.Printf("%f %.2f %e %g\n", 3.14, 3.14159, 3.14, 3.14) // float forms
fmt.Printf("%s %q\n", "hi", "hi")          // hi "hi"  (%q adds quotes)
fmt.Printf("%p\n", &p)                     // pointer address
fmt.Printf("%t\n", true)                   // boolean
fmt.Printf("%5d|%-5d|%05d\n", 42, 42, 42)  //    42|42   |00042  (width/align/zero)
fmt.Printf("%+d % d\n", 42, 42)            // +42  42 (sign flags)
fmt.Printf("%x\n", []byte("hi"))           // 6869 (hex of bytes)
fmt.Printf("%%\n")                         // literal %

// Errors with %w wrap (see errors.md):
fmt.Errorf("ctx: %w", err)

// Sprint family returns strings; Fprint writes to an io.Writer:
s := fmt.Sprintf("%d-%s", 1, "a")
fmt.Fprintf(os.Stderr, "warn: %v\n", err)

%v on a type implementing fmt.Stringer calls its String() method. %+v/%#v are your debugging best friends.