Go String Manipulation for Coding Interviews: The Reference Guide

June 19, 20269 min read
dsaalgorithmsinterview-prepcareer
Go String Manipulation for Coding Interviews: The Reference Guide
TL;DR
  • len(s) counts bytes, not characters — use utf8.RuneCountInString or convert to []rune for any Unicode problem
  • range s yields 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 use strings.Builder for incremental string construction
  • strings.Fields splits on any whitespacestrings.Split(s, " ") leaves empty strings on consecutive spaces and fails on tabs
  • [26]int frequency array beats map[rune]int — faster and clearer for lowercase-ASCII constraints, and interviewers notice the deliberate choice
  • To modify a string, convert to []byte for ASCII or []rune for 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:

  1. You cannot modify a string in place. Any "change" creates a new string.
  2. 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:

MethodWhat 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 s gives (byteIndex, rune) pairs. The byte index can jump by more than 1.
  • Concatenation with + in a loop is O(n²). Use strings.Builder.
  • strings.Split(s, " ") leaves empty strings on consecutive spaces. strings.Fields doesn't.
  • Comparing strings with == is case-sensitive. strings.EqualFold isn'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

TaskGo idiom
Byte at indexs[i]byte
Iterate charactersfor i, r := range s
Length in byteslen(s)
Length in charactersutf8.RuneCountInString(s)
Mutable byte slice[]byte(s)
Mutable rune slice[]rune(s)
Build string efficientlyvar sb strings.Builder
Split on whitespacestrings.Fields(s)
Case-insensitive equalstrings.EqualFold(s, t)
int to stringstrconv.Itoa(n)
string to intstrconv.Atoi(s)
Letter a-z to index 0-25s[i] - 'a'
Lowercasestrings.ToLower(s)
Contains substringstrings.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