Strings
A Go string is an immutable sequence of bytes. The conventional encoding is UTF-8, but the language does not enforce this — a string may contain any byte sequence. The string type admits indexing (returning the byte at the given position), length (the number of bytes), slicing (a substring), and concatenation. For Unicode-aware operations, the unicode/utf8 package, the for ... range form (which yields runes, not bytes), and conversions to []rune are conventional. The strings package provides substantial string-manipulation utilities; the strconv package provides numeric conversions.
String literals
Two forms:
"interpreted" // admits escape sequences
"line one\nline two"
"unicode: é" // é
"raw byte: \x41" // 'A'
`raw string` // no escape interpretation
`first line
second line` // multi-line
`tab in raw: \t (literal backslash + t)`
The raw-string form is conventional for regular expressions, JSON, multi-line code samples, and any text where backslashes are common.
Basic operations
s := "hello world"
len(s) // 11 (byte count)
s[0] // 104 (byte 'h' as uint8)
s[0:5] // "hello" (slicing; substring)
s[6:] // "world"
s[:5] // "hello"
s + " from Go" // concatenation
Indexing returns a byte, not a character:
s := "héllo" // 6 bytes (é is 2 bytes in UTF-8)
fmt.Println(len(s)) // 6
fmt.Println(s[0]) // 104 ('h')
fmt.Println(s[1]) // 195 (first byte of é)
fmt.Println(s[2]) // 169 (second byte of é)
The mechanism is fast (constant-time index, no UTF-8 decoding) but requires care when handling non-ASCII text. The conventional defence:
- Use
for i, r := range sfor rune-based iteration. - Use
[]rune(s)for rune-indexed slicing. - Use
utf8.RuneCountInString(s)for the rune count.
Immutability
Strings cannot be modified in place:
s := "hello"
// s[0] = 'H' // ERROR: cannot assign to s[0]
// To "modify" a string, build a new one:
b := []byte(s)
b[0] = 'H'
s = string(b) // "Hello"
The immutability admits substantial efficiencies — strings can be shared without copying — and eliminates aliasing bugs.
Iteration
Two principal forms:
Byte-level iteration
s := "héllo"
for i := 0; i < len(s); i++ {
fmt.Printf("%d: %d\n", i, s[i])
}
// Outputs:
// 0: 104 ('h')
// 1: 195 (first byte of é)
// 2: 169 (second byte of é)
// 3: 108 ('l')
// 4: 108 ('l')
// 5: 111 ('o')
The form is appropriate for ASCII text and byte-level operations.
Rune-level iteration with range
s := "héllo"
for i, r := range s {
fmt.Printf("byte %d: rune %c (%d)\n", i, r, r)
}
// Outputs:
// byte 0: rune h (104)
// byte 1: rune é (233)
// byte 3: rune l (108)
// byte 4: rune l (108)
// byte 5: rune o (111)
The i is the byte index (not a rune index); the r is a rune (Unicode code point). The form is the conventional Unicode-aware iteration.
For an explicit rune slice:
runes := []rune(s) // ['h', 'é', 'l', 'l', 'o']
fmt.Println(len(runes)) // 5 (rune count)
fmt.Println(runes[1]) // 233 (é)
Conversions
// String ↔ []byte:
b := []byte("hello") // []byte{104, 101, 108, 108, 111}
s := string(b)
// String ↔ []rune:
r := []rune("héllo") // [104, 233, 108, 108, 111]
s := string(r)
// Number ↔ string:
import "strconv"
s := strconv.Itoa(42) // "42"
n, err := strconv.Atoi("42") // 42, nil
s := strconv.FormatFloat(3.14, 'f', 2, 64) // "3.14"
f, err := strconv.ParseFloat("3.14", 64) // 3.14, nil
s := strconv.FormatBool(true) // "true"
b, err := strconv.ParseBool("true") // true, nil
// fmt.Sprintf for elaborate formatting:
s := fmt.Sprintf("%d items at $%.2f", 5, 3.14) // "5 items at $3.14"
A common pitfall:
n := 65
s := string(n) // "A" (not "65")
// string(int) is rune-to-string
// For numeric conversion: strconv.Itoa(n)
The compiler issues a warning for string(int) since Go 1.15.
The strings package
The strings package provides the conventional manipulation surface:
import "strings"
strings.Contains("hello world", "lo wo") // true
strings.HasPrefix("hello", "he") // true
strings.HasSuffix("hello", "lo") // true
strings.Index("hello", "ll") // 2
strings.LastIndex("hello", "l") // 3
strings.Count("hello", "l") // 2
strings.ToUpper("hello") // "HELLO"
strings.ToLower("HELLO") // "hello"
strings.Title("hello world") // deprecated; use cases.Title
strings.TrimSpace(" hello ") // "hello"
strings.Trim("xxhelloyy", "xy") // "hello"
strings.TrimLeft("xxhello", "x") // "hello"
strings.TrimRight("helloyy", "y") // "hello"
strings.TrimPrefix("hello world", "hello ") // "world"
strings.TrimSuffix("hello world", " world") // "hello"
strings.Replace("hello hello", "hello", "hi", 1) // "hi hello"
strings.ReplaceAll("hello hello", "hello", "hi") // "hi hi"
strings.Repeat("ab", 3) // "ababab"
strings.Split("a,b,c", ",") // []string{"a", "b", "c"}
strings.SplitN("a,b,c", ",", 2) // []string{"a", "b,c"}
strings.Join([]string{"a", "b", "c"}, ",") // "a,b,c"
strings.Fields(" hello world ") // []string{"hello", "world"}
The strings.Builder admits efficient string construction:
import "strings"
var b strings.Builder
for i := 0; i < 100; i++ {
fmt.Fprintf(&b, "line %d\n", i)
}
result := b.String()
The mechanism avoids quadratic-time concatenation (s = s + "..." in a loop produces a new string each time).
The unicode and unicode/utf8 packages
import (
"unicode"
"unicode/utf8"
)
unicode.IsDigit('5') // true
unicode.IsLetter('A') // true
unicode.IsSpace(' ') // true
unicode.IsUpper('A') // true
unicode.ToUpper('a') // 'A'
unicode.ToLower('A') // 'a'
utf8.RuneCountInString("héllo") // 5
utf8.ValidString("héllo") // true
utf8.RuneLen('é') // 2 (bytes in UTF-8)
// Iterating runes:
for i := 0; i < len(s); {
r, size := utf8.DecodeRuneInString(s[i:])
fmt.Printf("rune: %c\n", r)
i += size
}
The conventional discipline is to use for ... range for ordinary Unicode-aware iteration; the utf8 package admits low-level operations.
The fmt package
The fmt package admits formatted I/O. The principal verbs:
| Verb | Meaning |
|---|---|
%v | Default format (any type) |
%+v | Default with struct field names |
%#v | Go-syntax representation |
%T | Type name |
%d | Decimal integer |
%b | Binary |
%o | Octal |
%x, %X | Hex (lower/upper) |
%c | Rune (Unicode character) |
%U | Unicode format U+1234 |
%f, %g, %e | Floating-point forms |
%s | String |
%q | Quoted string |
%p | Pointer (hex) |
%t | Boolean |
%% | Literal % |
Width and precision:
fmt.Printf("%5d\n", 42) // " 42"
fmt.Printf("%-5d\n", 42) // "42 "
fmt.Printf("%05d\n", 42) // "00042"
fmt.Printf("%.2f\n", 3.14159) // "3.14"
fmt.Printf("%8.2f\n", 3.14159) // " 3.14"
The principal functions:
fmt.Println("hello") // print with newline
fmt.Printf("%d\n", 42) // formatted print
fmt.Print("hello") // print without newline
s := fmt.Sprintf("%d items", n) // format to string
s := fmt.Sprint("hello", " ", "world") // concatenate and format
// To io.Writer:
fmt.Fprintln(os.Stderr, "error message")
fmt.Fprintf(file, "%d\n", n)
// Reading:
var n int
fmt.Scan(&n) // from stdin
fmt.Sscanf("42", "%d", &n) // from string
Common patterns
Building a string
// Inefficient (O(n²)):
s := ""
for _, x := range items {
s = s + x.String() + "\n"
}
// Efficient (O(n)):
var b strings.Builder
for _, x := range items {
b.WriteString(x.String())
b.WriteByte('\n')
}
s := b.String()
// Or using strings.Join for known slices:
s := strings.Join(strs, "\n")
Parsing a number
n, err := strconv.Atoi(input)
if err != nil {
return fmt.Errorf("invalid number %q: %w", input, err)
}
f, err := strconv.ParseFloat(input, 64)
b, err := strconv.ParseBool(input)
i, err := strconv.ParseInt(input, 10, 64) // base 10, 64-bit
Formatting a number
s := strconv.Itoa(42) // "42"
s := strconv.FormatFloat(3.14, 'f', 2, 64) // "3.14"
s := strconv.FormatInt(255, 16) // "ff"
// Or with fmt:
s := fmt.Sprintf("%d", 42)
s := fmt.Sprintf("%.2f", 3.14)
s := fmt.Sprintf("%x", 255) // "ff"
Splitting and joining
parts := strings.Split("a,b,c", ",") // ["a", "b", "c"]
joined := strings.Join(parts, "-") // "a-b-c"
words := strings.Fields(" hello world ") // ["hello", "world"]
lines := strings.Split(text, "\n")
for _, line := range lines {
process(strings.TrimSpace(line))
}
Case-insensitive comparison
strings.EqualFold("Hello", "hello") // true
The form is faster and clearer than strings.ToLower(a) == strings.ToLower(b).
Replacing with a function
The strings.Replacer admits efficient multi-pattern replacement:
r := strings.NewReplacer("<", "<", ">", ">", "&", "&")
escaped := r.Replace(html)
For complex patterns, the regexp package:
import "regexp"
re := regexp.MustCompile(`\d+`)
matches := re.FindAllString("abc 123 def 456", -1) // ["123", "456"]
result := re.ReplaceAllString("abc 123", "X") // "abc X"
result := re.ReplaceAllStringFunc("abc 123", func(s string) string {
return strings.Repeat(s, 2)
})
Reading lines from stdin
import (
"bufio"
"os"
)
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := scanner.Text()
process(line)
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
}
Multi-line raw strings
const sql = `
SELECT id, name, email
FROM users
WHERE active = true
ORDER BY created_at DESC
`
const config = `
host: localhost
port: 8080
debug: true
`
The raw-string form is conventional for embedded SQL, JSON, configuration, and templates.
A note on the conventional discipline
The contemporary Go strings advice:
- Use
for ... rangefor Unicode-aware iteration. - Use
[]rune(s)when rune-indexed access is needed. - Use
strings.Builderfor substantial concatenation. - Use
strings.Joinfor joining a slice of strings. - Use the
stringspackage for the conventional manipulations. - Use
strconvfor numeric conversions;fmt.Sprintffor elaborate formatting. - Use raw strings (
``) for regex, JSON, multi-line text. - Use
EqualFoldfor case-insensitive comparison.
The combination — immutable byte-indexed strings, UTF-8 by convention, for ... range for Unicode iteration, the strings/strconv/unicode/fmt packages, the Builder for efficient construction — is the substance of Go’s string surface. The discipline produces clear, efficient text handling.