Strings in Go — Find the Bug¶
Bug 1: Off-by-One in String Slicing¶
package main
import "fmt"
func getMiddle(s string) string {
if len(s) < 3 {
return s
}
return s[1 : len(s)-2]
}
func main() {
fmt.Println(getMiddle("hello")) // wants "ell"
fmt.Println(getMiddle("abcde")) // wants "bcd"
}
What is the bug?
Hint
Count the indices carefully. What byte index does `len(s)-2` point to? Remember that slicing `s[a:b]` excludes byte at index `b`.Solution
**Bug**: The slice `s[1:len(s)-2]` excludes the second-to-last character. For `"hello"` (len=5): `s[1:3]` = `"el"`, missing the second 'l'. **Fix**: For `"hello"`: `s[1:4]` = `"ell"` ✓ For `"abcde"`: `s[1:4]` = `"bcd"` ✓Bug 2: Rune vs Byte Confusion¶
package main
import "fmt"
func reverseString(s string) string {
b := []byte(s)
for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}
func main() {
fmt.Println(reverseString("Hello")) // "olleH" ✓
fmt.Println(reverseString("世界")) // should be "界世" — is it?
}
What is the bug?
Hint
The Chinese character '世' is encoded as 3 bytes in UTF-8. What happens when you swap individual bytes instead of whole characters?Solution
**Bug**: Reversing bytes of a multi-byte UTF-8 string corrupts the encoding. "世界" is 6 bytes (3 per character). Reversing the bytes produces invalid UTF-8, not "界世". **Fix**: Convert to `[]rune` first:Bug 3: Missing Error Check on Index¶
package main
import (
"fmt"
"strings"
)
func extractValue(s, key string) string {
idx := strings.Index(s, key+"=")
start := idx + len(key) + 1
end := strings.Index(s[start:], "&")
if end == -1 {
return s[start:]
}
return s[start : start+end]
}
func main() {
query := "user=alice&token=secret&page=1"
fmt.Println(extractValue(query, "token")) // "secret"
fmt.Println(extractValue(query, "missing")) // PANIC!
}
What is the bug?
Hint
What does `strings.Index` return when the key is not found? What happens when you use that return value as an index?Solution
**Bug**: When `strings.Index` returns `-1` (key not found), `start = -1 + len(key) + 1` may be a valid-looking index, leading to wrong results or panic. **Fix**:Bug 4: Concatenation in Loop Creates O(n²) Work¶
package main
import "fmt"
func buildReport(items []string) string {
report := "=== Report ===\n"
for i, item := range items {
report += fmt.Sprintf("%d. %s\n", i+1, item)
}
report += "=== End ==="
return report
}
func main() {
items := make([]string, 10000)
for i := range items {
items[i] = fmt.Sprintf("Item number %d", i)
}
fmt.Println(len(buildReport(items)))
}
What is the bug?
Hint
Each `report += ...` allocates a new string. For 10,000 items, how much memory is allocated in total?Solution
**Bug**: String concatenation with `+=` inside a loop creates O(n²) allocations. For 10,000 items, this allocates and copies approximately 1+2+...+10000 = 50M characters. **Fix**:Bug 5: String(int) Misuse¶
package main
import "fmt"
func statusMessage(code int) string {
return "Status code: " + string(code)
}
func main() {
fmt.Println(statusMessage(200)) // wants "Status code: 200"
fmt.Println(statusMessage(65)) // wants "Status code: 65"
}
What is the bug?
Hint
What does `string(65)` produce in Go? Is it `"65"` or something else?Solution
**Bug**: `string(int)` interprets the integer as a Unicode code point, not a number. `string(200)` produces `"È"` (U+00C8), and `string(65)` produces `"A"`. **Fix**:Bug 6: Case-Sensitive Comparison¶
package main
import "fmt"
func isAdminRole(role string) bool {
return role == "admin" || role == "ADMIN"
}
func main() {
roles := []string{"admin", "ADMIN", "Admin", "AdMiN", "user"}
for _, r := range roles {
fmt.Printf("%-10s → isAdmin=%v\n", r, isAdminRole(r))
}
// "Admin" and "AdMiN" return false — is that intended?
}
What is the bug?
Hint
The function only handles two specific cases. What about other capitalizations?Solution
**Bug**: The function only handles `"admin"` and `"ADMIN"` but not other capitalizations like `"Admin"` or `"AdMiN"`. This could allow privilege escalation if a user sends `"Admin"`. **Fix**:Bug 7: Infinite Loop from Missing Advance¶
package main
import (
"fmt"
"strings"
)
func countOccurrences(s, substr string) int {
count := 0
for {
idx := strings.Index(s, substr)
if idx == -1 {
break
}
count++
s = s[idx:] // BUG: should advance past the found match
}
return count
}
func main() {
fmt.Println(countOccurrences("abcabc", "a")) // should be 2
}
What is the bug?
Hint
After finding `substr` at index `idx`, what does `s = s[idx:]` do? Does it advance past the match?Solution
**Bug**: `s = s[idx:]` moves the string to start at the found substring — but since `idx` is where the match starts, the next iteration will find the same match again, causing an infinite loop. **Fix**:Bug 8: Slice Out of Bounds¶
package main
import "fmt"
func truncate(s string, maxLen int) string {
if len(s) > maxLen {
return s[:maxLen] + "..."
}
return s
}
func main() {
fmt.Println(truncate("Hello, World!", 5)) // "Hello..."
fmt.Println(truncate("Hi", 5)) // "Hi"
fmt.Println(truncate("Hello, 世界!", 9)) // PANIC or garbled?
}
What is the bug?
Hint
"世界" has multi-byte characters. What does `s[:9]` do when byte 9 is in the middle of a multi-byte rune?Solution
**Bug**: Slicing at a byte position that falls in the middle of a multi-byte UTF-8 character produces a string with invalid UTF-8 at the cut point (or the slice may panic in some contexts). For "Hello, 世界!" where '世' starts at byte 7, `s[:9]` cuts through the middle of '世'. **Fix**:Bug 9: strings.Split Unexpected Result¶
package main
import (
"fmt"
"strings"
)
func parseConfig(cfg string) map[string]string {
result := make(map[string]string)
lines := strings.Split(cfg, "\n")
for _, line := range lines {
parts := strings.Split(line, "=")
if len(parts) == 2 {
result[parts[0]] = parts[1]
}
}
return result
}
func main() {
cfg := `host=localhost
port=5432
dsn=postgres://user:pass@localhost/db?sslmode=disable`
config := parseConfig(cfg)
fmt.Println(config["dsn"]) // should be full DSN — is it?
}
What is the bug?
Hint
The DSN value contains an `=` sign. What does `strings.Split(line, "=")` produce when the line has multiple `=` characters?Solution
**Bug**: `strings.Split(line, "=")` splits on ALL `=` characters. For the DSN line `dsn=postgres://user:pass@localhost/db?sslmode=disable`, it produces 3 parts, so `len(parts) == 2` is false, and the DSN is silently dropped. **Fix**: Use `strings.SplitN` to split at most once:Bug 10: Nil Pointer Panic with fmt.Sprintf and %s¶
package main
import "fmt"
type User struct {
Name string
Email *string
}
func formatUser(u User) string {
email := ""
if u.Email != nil {
email = *u.Email
}
return fmt.Sprintf("Name: %s, Email: %s", u.Name, email)
}
func getUserInfo(users []User) []string {
result := make([]string, len(users))
for i, u := range users {
// BUG: what if someone changes this line?
result[i] = fmt.Sprintf("Name: %s, Email: %s", u.Name, u.Email)
}
return result
}
func main() {
email := "alice@example.com"
users := []User{
{Name: "Alice", Email: &email},
{Name: "Bob", Email: nil},
}
for _, info := range getUserInfo(users) {
fmt.Println(info)
}
}
What is the bug?
Hint
What does `fmt.Sprintf("%s", (*string)(nil))` produce? Does it panic or print something unexpected?Solution
**Bug**: `fmt.Sprintf("%s", u.Email)` where `u.Email` is `*string` and nil. For `%s` with a `*string`, fmt will print `%!s(*string=Bug 11: Comparing Strings with Different Normalizations¶
package main
import "fmt"
func isDuplicate(a, b string) bool {
return a == b
}
func main() {
// Both look like "café" to the user
s1 := "caf\u00e9" // é as single code point U+00E9
s2 := "cafe\u0301" // e + combining accent U+0301
fmt.Println(s1) // café
fmt.Println(s2) // café (looks identical!)
fmt.Println(isDuplicate(s1, s2)) // false — BUG: looks like duplicate!
fmt.Println(len(s1), len(s2)) // 5, 6 — different byte lengths!
}
What is the bug?
Hint
Unicode has multiple ways to represent the same visible character. The `==` operator compares bytes, not visual appearance.Solution
**Bug**: Two strings that look visually identical can have different UTF-8 representations due to Unicode normalization forms. `"caf\u00e9"` uses a precomposed character (NFC) while `"cafe\u0301"` uses decomposed characters (NFD). They compare as unequal with `==`. **Fix**: Normalize both strings to NFC before comparison: Or use the `collate` package for full Unicode-aware comparison.Bug 12: Reading One Character Too Many¶
package main
import "fmt"
func splitAtCapital(s string) []string {
var result []string
start := 0
for i, r := range s {
if i > 0 && r >= 'A' && r <= 'Z' {
result = append(result, s[start:i+1]) // BUG!
start = i
}
}
result = append(result, s[start:])
return result
}
func main() {
fmt.Println(splitAtCapital("CamelCaseString"))
// wants: ["Camel", "Case", "String"]
}
What is the bug?
Hint
`s[start:i+1]` includes the character at index `i`. But when you find a capital letter at `i`, should it be included in the current word or start the next word?Solution
**Bug**: `s[start:i+1]` includes the capital letter that starts the new word in the CURRENT word. For "CamelCaseString", the first split should be `s[0:5]` = "Camel", but the code produces `s[0:6]` = "CamelC". **Fix**:Bug 13: Builder Used Concurrently¶
package main
import (
"fmt"
"strings"
"sync"
)
func buildConcurrent(items []string) string {
var wg sync.WaitGroup
var b strings.Builder
for _, item := range items {
wg.Add(1)
go func(s string) {
defer wg.Done()
b.WriteString(s) // DATA RACE!
b.WriteByte('\n')
}(item)
}
wg.Wait()
return b.String()
}
func main() {
items := []string{"one", "two", "three", "four", "five"}
fmt.Println(buildConcurrent(items))
}
What is the bug?
Hint
`strings.Builder` is not safe for concurrent use. What happens when two goroutines call `WriteString` simultaneously?Solution
**Bug**: `strings.Builder` is not goroutine-safe. Concurrent writes create a data race, potentially corrupting the buffer, causing panics, or producing garbled output. **Fix**: Either use a mutex, or collect results and join:func buildConcurrent(items []string) string {
results := make([]string, len(items))
var wg sync.WaitGroup
for i, item := range items {
wg.Add(1)
go func(idx int, s string) {
defer wg.Done()
results[idx] = s // each goroutine writes to its own slot
}(i, item)
}
wg.Wait()
return strings.Join(results, "\n")
}