Exercise: String Builder
Difficulty - Beginner
Learning Objectives
- Master string manipulation in Go
- Understand the efficiency of strings.Builder
- Practice string processing algorithms
- Learn to avoid common string performance pitfalls
Problem Statement
Create a StringUtils package with efficient string manipulation functions:
- WordCount: Count the number of words in a string
- Reverse: Reverse a string
- IsPalindrome: Check if a string reads the same forwards and backwards
- ToCamelCase: Convert "snake_case" or "kebab-case" to "camelCase"
- Truncate: Truncate a string to a maximum length with ellipsis
Function Signatures
1package stringutils
2
3// WordCount returns the number of words in the string
4func WordCount(s string) int
5
6// Reverse returns the reversed string
7func Reverse(s string) string
8
9// IsPalindrome checks if a string is a palindrome
10func IsPalindrome(s string) bool
11
12// ToCamelCase converts snake_case or kebab-case to camelCase
13func ToCamelCase(s string) string
14
15// Truncate shortens a string to maxLen, adding "..." if truncated
16func Truncate(s string, maxLen int) string
Example Usage
1package main
2
3import (
4 "fmt"
5 "stringutils"
6)
7
8func main() {
9 // WordCount
10 text := "Hello, world! How are you?"
11 count := stringutils.WordCount(text)
12 fmt.Println(count) // 5
13
14 // Reverse
15 reversed := stringutils.Reverse("Hello, 世界")
16 fmt.Println(reversed) // 界世 ,olleH
17
18 // IsPalindrome
19 fmt.Println(stringutils.IsPalindrome("racecar")) // true
20 fmt.Println(stringutils.IsPalindrome("hello")) // false
21
22 // ToCamelCase
23 snake := "user_name_field"
24 camel := stringutils.ToCamelCase(snake)
25 fmt.Println(camel) // userNameField
26
27 kebab := "user-name-field"
28 camel2 := stringutils.ToCamelCase(kebab)
29 fmt.Println(camel2) // userNameField
30
31 // Truncate
32 long := "This is a very long string that needs to be truncated"
33 short := stringutils.Truncate(long, 20)
34 fmt.Println(short) // This is a very lo...
35}
Requirements
- WordCount should handle multiple consecutive spaces
- Reverse must properly handle Unicode characters
- IsPalindrome should be case-insensitive and ignore spaces/punctuation
- ToCamelCase should handle both snake_case and kebab-case
- Truncate should add "..." only if the string was actually truncated
Test Cases
1package stringutils
2
3import "testing"
4
5func TestWordCount(t *testing.T) {
6 tests := []struct {
7 input string
8 expected int
9 }{
10 {"Hello world", 2},
11 {"Hello world", 2}, // Multiple spaces
12 {"", 0},
13 {" ", 0},
14 {"One", 1},
15 {"One two three four five", 5},
16 }
17
18 for _, tt := range tests {
19 result := WordCount(tt.input)
20 if result != tt.expected {
21 t.Errorf("WordCount(%q) = %d; want %d", tt.input, result, tt.expected)
22 }
23 }
24}
25
26func TestReverse(t *testing.T) {
27 tests := []struct {
28 input string
29 expected string
30 }{
31 {"hello", "olleh"},
32 {"Hello, 世界", "界世 ,olleH"},
33 {"", ""},
34 {"a", "a"},
35 {"Go", "oG"},
36 }
37
38 for _, tt := range tests {
39 result := Reverse(tt.input)
40 if result != tt.expected {
41 t.Errorf("Reverse(%q) = %q; want %q", tt.input, result, tt.expected)
42 }
43 }
44}
45
46func TestIsPalindrome(t *testing.T) {
47 tests := []struct {
48 input string
49 expected bool
50 }{
51 {"racecar", true},
52 {"RaceCar", true},
53 {"A man a plan a canal Panama", true},
54 {"hello", false},
55 {"", true},
56 {"a", true},
57 }
58
59 for _, tt := range tests {
60 result := IsPalindrome(tt.input)
61 if result != tt.expected {
62 t.Errorf("IsPalindrome(%q) = %v; want %v", tt.input, result, tt.expected)
63 }
64 }
65}
66
67func TestToCamelCase(t *testing.T) {
68 tests := []struct {
69 input string
70 expected string
71 }{
72 {"user_name", "userName"},
73 {"user-name", "userName"},
74 {"user_name_field", "userNameField"},
75 {"already_camel", "alreadyCamel"},
76 {"", ""},
77 {"single", "single"},
78 }
79
80 for _, tt := range tests {
81 result := ToCamelCase(tt.input)
82 if result != tt.expected {
83 t.Errorf("ToCamelCase(%q) = %q; want %q", tt.input, result, tt.expected)
84 }
85 }
86}
87
88func TestTruncate(t *testing.T) {
89 tests := []struct {
90 input string
91 maxLen int
92 expected string
93 }{
94 {"Hello, world!", 10, "Hello, ..."},
95 {"Short", 10, "Short"},
96 {"Exactly10!", 10, "Exactly10!"},
97 {"", 5, ""},
98 {"Hi", 10, "Hi"},
99 }
100
101 for _, tt := range tests {
102 result := Truncate(tt.input, tt.maxLen)
103 if result != tt.expected {
104 t.Errorf("Truncate(%q, %d) = %q; want %q", tt.input, tt.maxLen, result, tt.expected)
105 }
106 }
107}
Hints
Hint 1: WordCount
Use strings.Fields() which splits on whitespace and handles multiple spaces automatically.
Hint 2: Reverse
Convert the string to a slice of runes, reverse the slice, then convert back to a string.
1runes := []rune(s)
2// reverse runes slice
3return string(runes)
Hint 3: IsPalindrome
Convert to lowercase and remove non-alphanumeric characters. Then compare with its reverse.
Hint 4: ToCamelCase
Split on '_' or '-', capitalize the first letter of each word except the first, then join.
Hint 5: Truncate
Check if the string length exceeds maxLen. If so, take substring up to and append "...".
Solution
Click to see the complete solution
1package stringutils
2
3import (
4 "strings"
5 "unicode"
6)
7
8// WordCount returns the number of words in the string
9func WordCount(s string) int {
10 // Fields splits on whitespace and handles multiple spaces
11 fields := strings.Fields(s)
12 return len(fields)
13}
14
15// Reverse returns the reversed string
16func Reverse(s string) string {
17 // Convert to runes to handle Unicode properly
18 runes := []rune(s)
19
20 // Reverse the rune slice
21 for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
22 runes[i], runes[j] = runes[j], runes[i]
23 }
24
25 return string(runes)
26}
27
28// IsPalindrome checks if a string is a palindrome
29func IsPalindrome(s string) bool {
30 // Normalize: lowercase and keep only alphanumeric
31 var normalized strings.Builder
32 for _, r := range s {
33 if unicode.IsLetter(r) || unicode.IsDigit(r) {
34 normalized.WriteRune(unicode.ToLower(r))
35 }
36 }
37
38 cleaned := normalized.String()
39
40 // Compare with reverse
41 return cleaned == Reverse(cleaned)
42}
43
44// ToCamelCase converts snake_case or kebab-case to camelCase
45func ToCamelCase(s string) string {
46 if s == "" {
47 return ""
48 }
49
50 // Replace hyphens with underscores for uniform handling
51 s = strings.ReplaceAll(s, "-", "_")
52
53 // Split on underscores
54 parts := strings.Split(s, "_")
55
56 var result strings.Builder
57 for i, part := range parts {
58 if part == "" {
59 continue
60 }
61
62 if i == 0 {
63 // First word: keep lowercase
64 result.WriteString(strings.ToLower(part))
65 } else {
66 // Subsequent words: capitalize first letter
67 result.WriteString(strings.Title(strings.ToLower(part)))
68 }
69 }
70
71 return result.String()
72}
73
74// Truncate shortens a string to maxLen, adding "..." if truncated
75func Truncate(s string, maxLen int) string {
76 if len(s) <= maxLen {
77 return s
78 }
79
80 if maxLen <= 3 {
81 return s[:maxLen]
82 }
83
84 return s[:maxLen-3] + "..."
85}
Explanation
WordCount:
- Uses
strings.Fields()which automatically handles multiple spaces - Returns 0 for empty or whitespace-only strings
- Simple and efficient O(n) solution
Reverse:
- Converts string to []rune to handle Unicode correctly
- Uses two-pointer technique for in-place reversal
- Converting to runes is essential for proper Unicode handling
- Example: "Hello, 世界" has 9 characters but only 8 runes
IsPalindrome:
- Normalizes by removing non-alphanumeric characters and converting to lowercase
- Uses strings.Builder for efficient string construction
- Compares normalized string with its reverse
- Case-insensitive and ignores punctuation/spaces
ToCamelCase:
- Handles both snake_case and kebab-case by normalizing to underscores
- Splits on underscores to get word parts
- First word remains lowercase, subsequent words are title-cased
- Uses strings.Builder for efficient concatenation
Truncate:
- Simple length check to avoid unnecessary work
- Handles edge case where maxLen <= 3
- Subtracts 3 from maxLen to account for "..." suffix
- Only adds "..." if string was actually truncated
Performance Considerations
Avoid String Concatenation in Loops:
1// Bad: Creates new string on each iteration
2func badConcat(words []string) string {
3 result := ""
4 for _, word := range words {
5 result += word // O(n²) complexity
6 }
7 return result
8}
9
10// Good: Use strings.Builder
11func goodConcat(words []string) string {
12 var builder strings.Builder
13 for _, word := range words {
14 builder.WriteString(word) // O(n) complexity
15 }
16 return builder.String()
17}
Runes vs Bytes:
1// For ASCII-only strings, byte operations are faster
2func reverseASCII(s string) string {
3 bytes := []byte(s)
4 for i, j := 0, len(bytes)-1; i < j; i, j = i+1, j-1 {
5 bytes[i], bytes[j] = bytes[j], bytes[i]
6 }
7 return string(bytes)
8}
9
10// But for Unicode, must use runes
11func reverseUnicode(s string) string {
12 runes := []rune(s)
13 // ... reverse logic
14 return string(runes)
15}
Bonus Challenges
- Word Wrap: Implement a function that wraps text at a given line width
1func WordWrap(text string, width int) string
- Levenshtein Distance: Calculate edit distance between two strings
1func LevenshteinDistance(s1, s2 string) int
- Template Engine: Simple string template with variable substitution
1func Template(template string, vars map[string]string) string
2// Example: Template("Hello, {{name}}!", map[string]string{"name": "World"})
3// Returns: "Hello, World!"
- String Compression: Implement run-length encoding
1func Compress(s string) string
2// "aaabbbccc" -> "a3b3c3"
Key Takeaways
- Use strings.Builder for efficient string concatenation in loops
- String are immutable in Go - operations create new strings
- Handle Unicode properly by converting to []rune when necessary
- strings package provides many useful functions - learn them
- Performance matters - string operations can be expensive at scale
String manipulation is fundamental in programming. Go's strings package is powerful, but understanding when to use []byte vs []rune and when to use strings.Builder vs simple concatenation is crucial for writing efficient code.