Strings in Go
A string in Go is an immutable, read-only slice of bytes — UTF-8 encoded by default.
📖 Analogy: Think of a string as a sealed envelope. You can read what’s written, copy it, slice it — but you can’t erase and rewrite inside the same envelope. Every “change” creates a new one.
1. The Critical Distinction — byte vs rune 🔥
This is the #1 interview trap for Go strings.
s := "héllo" // 'é' is a 2-byte UTF-8 character
fmt.Println(len(s)) // 6 — bytes, NOT characters!
fmt.Println(utf8.RuneCountInString(s)) // 5 — actual characters (runes)
fmt.Println(s[1]) // 195 — a raw byte, NOT 'é'
fmt.Printf("%c\n", s[1]) // Ã — garbage for multi-byte charsbyte | rune | |
|---|---|---|
| Type | uint8 | int32 |
| Represents | One raw byte | One Unicode code point (character) |
len(s) | ✅ counts bytes | ❌ wrong for non-ASCII |
Index s[i] | ✅ gets a byte | ❌ breaks multi-byte chars |
range s | ❌ by byte | ✅ by rune (use this!) |
Rule: For pure ASCII strings,
byteindexing is fine. For any user-facing text or Unicode, always think in runes.
2. Declaration
s := "Hello, Go" // inferred type string
var s string // zero value = "" (empty string, not nil)
s := `raw
string` // raw/backtick literal — newlines and \ are literalStrings in Go are never nil — zero value is
"".
3. Core Operations
s := "Hello, Go!"
// Length
len(s) // 10 (bytes)
utf8.RuneCountInString(s) // 10 (runes — same here, all ASCII)
// Indexing — returns a byte
s[0] // 72 (byte value of 'H')
fmt.Printf("%c", s[0]) // H
// Slicing — returns a string
s[0:5] // "Hello"
s[7:] // "Go!"
// Concatenation
s1 := "Hello, " + "Go!" // creates a new string
// Comparison — lexicographic, works directly with ==, <, >
"abc" == "abc" // true
"abc" < "abd" // true4. Iteration — byte vs rune
s := "héllo"
// ❌ Byte iteration — breaks on multi-byte chars
for i := 0; i < len(s); i++ {
fmt.Printf("%c ", s[i]) // h à © l l o — corrupted!
}
// ✅ Rune iteration — correct for all Unicode
for i, r := range s {
fmt.Printf("[%d]=%c ", i, r) // [0]=h [1]=é [3]=l [4]=l [5]=o
}
// Note: index jumps 1→3 because 'é' is 2 bytes5. Concatenation Performance
| Method | When to use | Notes |
|---|---|---|
+ operator | 1–2 joins | Creates a new string each time |
fmt.Sprintf | Formatting only | Slowest — uses reflection |
strings.Join | Joining a slice | Efficient, single allocation |
strings.Builder | Loop / many joins | Fastest — zero-copy growth |
// ❌ Loop with + — O(n²) allocations
result := ""
for _, w := range words {
result += w + " "
}
// ✅ strings.Builder — O(n) single allocation
var b strings.Builder
for _, w := range words {
b.WriteString(w)
b.WriteByte(' ')
}
result := b.String()
// ✅ strings.Join — cleanest for slice → string
result := strings.Join(words, " ")6. strings Package Cheat Sheet
import "strings"| Function | Example | Output | Notes |
|---|---|---|---|
Contains | strings.Contains("Golang", "Go") | true | substring check |
HasPrefix | strings.HasPrefix("Go", "G") | true | starts with |
HasSuffix | strings.HasSuffix("Go", "o") | true | ends with |
Count | strings.Count("cheese", "e") | 3 | non-overlapping count |
Index | strings.Index("Go", "o") | 1 | first index, -1 if not found |
ToUpper | strings.ToUpper("go") | "GO" | |
ToLower | strings.ToLower("GO") | "go" | |
Split | strings.Split("a,b,c", ",") | ["a","b","c"] | empty sep splits by char |
Join | strings.Join([]string{"a","b"}, "-") | "a-b" | |
Replace | strings.Replace("foo", "o", "0", 1) | "f0o" | -1 replaces all |
ReplaceAll | strings.ReplaceAll("foo", "o", "0") | "f00" | cleaner than Replace with -1 |
Trim | strings.Trim("!!Go!!", "!") | "Go" | trims leading+trailing cutset |
TrimSpace | strings.TrimSpace(" hi ") | "hi" | whitespace only |
TrimPrefix | strings.TrimPrefix("GoLang", "Go") | "Lang" | removes prefix if present |
TrimSuffix | strings.TrimSuffix("GoLang", "Lang") | "Go" | removes suffix if present |
Repeat | strings.Repeat("Go", 3) | "GoGoGo" | |
ContainsAny | strings.ContainsAny("abc", "xyz") | false | any char in set |
Fields | strings.Fields(" a b c ") | ["a","b","c"] | splits on any whitespace |
EqualFold | strings.EqualFold("Go", "go") | true | case-insensitive compare |
7. strconv — Type Conversion
import "strconv"
// int → string
s := strconv.Itoa(42) // "42"
// string → int
n, err := strconv.Atoi("42") // 42, nil
n, err := strconv.Atoi("abc") // 0, error
// string → float
f, err := strconv.ParseFloat("3.14", 64)
// bool → string / string → bool
s := strconv.FormatBool(true) // "true"
b, err := strconv.ParseBool("true")⚠️
strconv.Atoialways returns(int, error)— always handle the error in production.
8. Common Patterns
Reverse a string (Unicode-safe)
func reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}Check palindrome
func isPalindrome(s string) bool {
r := []rune(strings.ToLower(s))
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
if r[i] != r[j] { return false }
}
return true
}Count character frequency (CP pattern)
freq := make(map[rune]int)
for _, r := range s {
freq[r]++
}Check if two strings are anagrams
func isAnagram(a, b string) bool {
if len(a) != len(b) { return false }
freq := make(map[rune]int)
for _, r := range a { freq[r]++ }
for _, r := range b {
freq[r]--
if freq[r] < 0 { return false }
}
return true
}9. Common Mistakes ⚠️
| Mistake | Problem | Fix |
|---|---|---|
len(s) for character count | counts bytes, not chars | utf8.RuneCountInString(s) |
s[i] on non-ASCII | returns raw byte, not char | []rune(s)[i] or range |
+ in a loop | O(n²) allocations | strings.Builder or strings.Join |
fmt.Sprintf for simple concat | slowest option | use + or Builder |
| Modify string directly | compile error — strings are immutable | convert to []byte or []rune first |
| Byte-iterate Unicode string | corrupts multi-byte chars | use for _, r := range s |
10. Interview Cheat Sheet
Q: What is a string in Go internally?
An immutable, read-only slice of bytes (
[]byte), UTF-8 encoded. It is not a slice — it has no capacity, only a pointer and a length.
Q: What does len(s) return?
The number of bytes, not characters. For
"héllo",lenreturns6(é is 2 bytes), but there are 5 characters. Useutf8.RuneCountInString(s)for true character count.
Q: How do you safely iterate over a string character by character?
Use
for i, r := range s— this yieldsrunevalues and handles multi-byte UTF-8 correctly. Never usefor i := 0; i < len(s)for character-level work.
Q: Are strings mutable in Go?
No. Strings are immutable. To modify, convert to
[]byte(for ASCII) or[]rune(for Unicode), modify, then convert back withstring(...).
Q: What is the fastest way to build a string in a loop?
strings.Builder— it maintains an internal buffer and only allocates once atb.String(). Using+in a loop creates a new string on every iteration.
Q: Difference between string(65) and strconv.Itoa(65)?
string(65)produces"A"(rune 65 = ‘A’).strconv.Itoa(65)produces"65"(the decimal digits). A very common mistake.