Go String Manipulation for Coding Interviews: The Reference Guide

len(s)counts bytes, not characters — useutf8.RuneCountInStringor convert to[]runefor any Unicode problemrange syields runes;s[i]yields bytes — they differ on any multi-byte character, silently breaking palindrome checks and length calculations- String concatenation with
+in a loop is O(n²) — always usestrings.Builderfor incremental string construction strings.Fieldssplits on any whitespace —strings.Split(s, " ")leaves empty strings on consecutive spaces and fails on tabs[26]intfrequency array beatsmap[rune]int— faster and clearer for lowercase-ASCII constraints, and interviewers notice the deliberate choice- To modify a string, convert to
[]bytefor ASCII or[]runefor Unicode first — Go strings are read-only byte slices
Go strings look simple. That's the trap.
The moment you try to modify one, the rune/byte distinction bites you. len() returns a number that seems wrong. Your palindrome check silently fails on anything a barista might type. And the bug doesn't surface until dry run, in front of your interviewer, who is now writing something in their notes.
This guide covers what Go strings actually are, which standard library functions matter, and the pitfalls that cost candidates points on otherwise clean solutions.
If you're newer to Go in interviews, Go for Coding Interviews has the broader picture. This post goes deep on strings specifically.
A Go String Is a Read-Only Byte Slice
A Go string is a read-only slice of bytes, and those bytes are UTF-8 encoded. Two things follow directly:
- You cannot modify a string in place. Any "change" creates a new string.
s[i]gives you a byte, not a character. For ASCII input that's fine. For anything else, they diverge.
s := "hello" fmt.Println(s[1]) // 101 (byte value of 'e') fmt.Println(string(s[1])) // "e"
Most interview problems constrain input to lowercase English letters, all of which fit in a single byte. You can treat s[i] as a character safely for those. Understand the model anyway, because the exceptions do appear.
Why range and s[i] Disagree
Go has two character types. byte is an alias for uint8. rune is an alias for int32 and represents a Unicode code point.
The difference surfaces when you iterate:
s := "café" // Index loop: gives bytes for i := 0; i < len(s); i++ { fmt.Printf("index %d: %v\n", i, s[i]) } // index 0: 99 (c) // index 1: 97 (a) // index 2: 102 (f) // index 3: 195 ← first byte of é // index 4: 169 ← second byte of é // Range loop: gives runes for i, r := range s { fmt.Printf("index %d: %c\n", i, r) } // index 0: c // index 1: a // index 2: f // index 3: é ← decoded correctly, byte index still jumps to 5 next
range decodes UTF-8 and yields runes. Index access yields raw bytes.
For ASCII-only problems, both work identically. When a problem mentions arbitrary Unicode input, use range or convert to []rune.
len() Lies. Sort Of.
s := "café" fmt.Println(len(s)) // 5 (bytes) fmt.Println(utf8.RuneCountInString(s)) // 4 (characters)
len(s) counts bytes, not characters. For lowercase-ASCII input they're equal, which covers the vast majority of interview problems. But "café" has four characters and five bytes, because é takes two bytes in UTF-8. If the problem says "the string may contain any Unicode characters," use utf8.RuneCountInString from unicode/utf8, or convert to []rune and check len on that slice.
The fix is simple: ask. Raise this during clarification. "Is the input ASCII-only?" Your interviewer will probably say yes, which means you can use len(s) safely and move on. The alternative is assuming and finding out during dry run, when your character count is off by one and your input string has a coffee shop name in it.
To Edit a String, Escape It First
Since strings are immutable, character-level modifications require a round trip through a slice:
s := "hello" // Byte slice: fast, correct for ASCII b := []byte(s) b[0] = 'H' result := string(b) // "Hello" // Rune slice: correct for all Unicode r := []rune(s) r[0] = 'H' result := string(r) // "Hello"
Both conversions are O(n) copies. For ASCII problems, prefer []byte. It's faster because it skips UTF-8 decoding. For problems where the input may contain multi-byte characters, []rune is necessary.
Building Strings: Always Use strings.Builder
String concatenation with + in a loop is O(n²). Every iteration allocates a new string and copies everything accumulated so far. It's less "building a string" and more "photocopying an ever-growing document, throwing the original away, and photocopying the copy." Use strings.Builder any time you're building a string incrementally.
// Wrong: O(n²), each + allocates a new string result := "" for _, ch := range input { result += string(ch) } // Right: amortized O(n) var sb strings.Builder for _, ch := range input { sb.WriteRune(ch) } result := sb.String()
Key methods:
| Method | What it does |
|---|---|
sb.WriteString(s) | Append a string |
sb.WriteByte(b) | Append a byte |
sb.WriteRune(r) | Append a rune |
sb.String() | Get the result (no extra copy) |
sb.Reset() | Clear and reuse |
sb.Len() | Current byte count |
sb.Grow(n) | Pre-allocate capacity |
strings.Builder also implements io.Writer, so fmt.Fprintf(&sb, "format", args...) works when you need formatted output. If you know the final length upfront, call sb.Grow(n) before writing to eliminate mid-build reallocations.
The strings Package: What You Actually Need
Most interview problems draw from a small subset of the package:
import "strings" // Search and check strings.Contains(s, substr) // bool strings.HasPrefix(s, prefix) // bool strings.HasSuffix(s, suffix) // bool strings.Count(s, substr) // non-overlapping occurrences strings.Index(s, substr) // first byte index, or -1 strings.LastIndex(s, substr) // last byte index, or -1 // Case strings.ToLower(s) strings.ToUpper(s) strings.EqualFold(s, t) // case-insensitive equality // Whitespace and trimming strings.TrimSpace(s) // strip leading/trailing whitespace strings.Trim(s, cutset) // strip any char in cutset from both ends strings.TrimLeft(s, cutset) strings.TrimRight(s, cutset) // Splitting and joining strings.Split(s, sep) // []string; empty strings if sep is adjacent strings.Fields(s) // split on any whitespace, handles multiple spaces strings.Join(parts, sep) // []string → string // Modification strings.Replace(s, old, new, n) // n=-1 replaces all strings.ReplaceAll(s, old, new) // shorthand for n=-1 strings.Repeat(s, n) // "ab" repeated 3 times = "ababab"
strings.Fields is the one most candidates forget. It splits on any whitespace and handles consecutive spaces correctly. strings.Split(s, " ") leaves empty strings between consecutive spaces and falls apart on tabs. strings.Fields doesn't. Use it whenever you're parsing word-separated input, unless you enjoy debugging why your word count is off because the input had two spaces somewhere in the middle.
Parsing and Formatting Numbers
import "strconv" strconv.Itoa(42) // int → "42" strconv.Atoi("42") // "42" → 42, nil (err if invalid) strconv.ParseInt("ff", 16, 64) // parse hex → 255, nil strconv.FormatInt(255, 16) // int64 → "ff" strconv.ParseFloat("3.14", 64)
Atoi returns an error. In an interview setting, if the input is guaranteed valid, the blank identifier is fine. If the problem involves parsing user-provided strings, handle the error:
n, err := strconv.Atoi(s) if err != nil { // handle invalid input }
fmt.Sprintf("%d", n) is fine for one-offs. strconv.Itoa is faster in a loop.
Characters Are Just Integers
Runes and bytes are integers. You can do arithmetic on them directly, and this pattern appears constantly in frequency-counting problems:
ch := byte('a') index := ch - 'a' // 0 for 'a', 25 for 'z' // Frequency array instead of a map: faster, O(1) space var freq [26]int for i := 0; i < len(s); i++ { freq[s[i]-'a']++ }
For problems constrained to lowercase ASCII, a [26]int array beats a map[rune]int on both speed and clarity. Interviewers notice the deliberate choice.
The unicode package handles the general case:
import "unicode" unicode.IsLetter(r) unicode.IsDigit(r) unicode.IsSpace(r) unicode.IsUpper(r) unicode.ToLower(r)
You only need this when the problem explicitly mentions non-ASCII input. For "lowercase English letters only," byte arithmetic is faster and clearer.
Patterns Worth Having Automatic
Reversing a string:
func reverse(s string) string { b := []byte(s) left, right := 0, len(b)-1 for left < right { b[left], b[right] = b[right], b[left] left++ right-- } return string(b) }
Anagram detection:
func isAnagram(s, t string) bool { if len(s) != len(t) { return false } var count [26]int for i := 0; i < len(s); i++ { count[s[i]-'a']++ count[t[i]-'a']-- } for _, c := range count { if c != 0 { return false } } return true }
Sliding window on a string follows the same structure as on an array. Use a [26]int frequency array for lowercase-ASCII constraints; a map[byte]int when the character set is broader.
These patterns come up enough that you want them automatic before the interview, not recalled under pressure. That's where voice practice helps. SpaceComplexity runs mock DSA interviews with real-time feedback, so you can drill string problems out loud until the patterns are reflexive.
What Actually Catches Candidates
The Go coding interview gotchas post covers the full list. The string-specific ones:
s[i]is a byte, not a rune. It looks like a character and usually acts like one, until the input has multi-byte characters.len(s)is byte count. It equals character count only for ASCII.range sgives(byteIndex, rune)pairs. The byte index can jump by more than 1.- Concatenation with
+in a loop is O(n²). Usestrings.Builder. strings.Split(s, " ")leaves empty strings on consecutive spaces.strings.Fieldsdoesn't.- Comparing strings with
==is case-sensitive.strings.EqualFoldisn't.
None of these are obscure. They all look correct at a glance and reveal themselves only during the dry run you did five minutes too late.
Go String Manipulation: Quick Reference
| Task | Go idiom |
|---|---|
| Byte at index | s[i] → byte |
| Iterate characters | for i, r := range s |
| Length in bytes | len(s) |
| Length in characters | utf8.RuneCountInString(s) |
| Mutable byte slice | []byte(s) |
| Mutable rune slice | []rune(s) |
| Build string efficiently | var sb strings.Builder |
| Split on whitespace | strings.Fields(s) |
| Case-insensitive equal | strings.EqualFold(s, t) |
| int to string | strconv.Itoa(n) |
| string to int | strconv.Atoi(s) |
| Letter a-z to index 0-25 | s[i] - 'a' |
| Lowercase | strings.ToLower(s) |
| Contains substring | strings.Contains(s, sub) |
For time complexity of the operations here, the Go built-in time complexity cheat sheet has the full breakdown. For idiomatic Go patterns that save real minutes in interviews, see Go interview idioms. Choosing between Go and another language? Go vs Java and Go vs Python both have direct comparisons.
Further Reading
- Strings, bytes, runes and characters in Go, the official Go blog post on this topic, written by Rob Pike
- strings package documentation, full function reference
- strconv package documentation, all number/string conversion functions
- unicode/utf8 package documentation,
RuneCountInString,DecodeRuneInString, and friends - unicode package documentation,
IsLetter,IsDigit,ToLower, etc.