Polyglot
Languages Go strings
Go § strings

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 s for 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:

VerbMeaning
%vDefault format (any type)
%+vDefault with struct field names
%#vGo-syntax representation
%TType name
%dDecimal integer
%bBinary
%oOctal
%x, %XHex (lower/upper)
%cRune (Unicode character)
%UUnicode format U+1234
%f, %g, %eFloating-point forms
%sString
%qQuoted string
%pPointer (hex)
%tBoolean
%%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("<", "&lt;", ">", "&gt;", "&", "&amp;")
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 ... range for Unicode-aware iteration.
  • Use []rune(s) when rune-indexed access is needed.
  • Use strings.Builder for substantial concatenation.
  • Use strings.Join for joining a slice of strings.
  • Use the strings package for the conventional manipulations.
  • Use strconv for numeric conversions; fmt.Sprintf for elaborate formatting.
  • Use raw strings (``) for regex, JSON, multi-line text.
  • Use EqualFold for 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.