String Processing

Lesson 19: String Processing

🎯 Life Analogy

Imagine you are a librarian. Every day you handle a large amount of text work:

String processing is the "text work" in programs — almost every program needs to deal with text.


Core Concepts

Go provides several standard libraries for handling strings:

Package Purpose Common Functions
strings String search, replace, split, join, etc. Contains, Replace, Split, Join, Trim
strconv Conversion between strings and other types Atoi, Itoa, ParseBool, FormatFloat
unicode/utf8 UTF-8 encoding related operations RuneCountInString, ValidString
strings.Builder Efficient concatenation of many strings WriteString, String

Key Points:

  1. Strings in Go are immutable — any modification creates a new string
  2. Strings in Go are UTF-8 encoded byte sequences at the lower level
  3. len(str) returns the number of bytes, not characters
  4. rune is Go's type for representing Unicode code points (essentially int32)

Basic Syntax and Usage

1. strings Package

GO
package main

import (
    "fmt"
    "strings"
)

func main() {
    str := "Hello, GoLang!"

    // Search
    fmt.Println(strings.Contains(str, "Go"))       // true
    fmt.Println(strings.HasPrefix(str, "Hello"))    // true
    fmt.Println(strings.HasSuffix(str, "!"))        // true
    fmt.Println(strings.Index(str, "Go"))           // 7

    // Replace
    result := strings.Replace(str, "Go", "Golang", 1)
    fmt.Println(result) // "Hello, GolangLang!"

    // Replace all
    s := "aabbcc"
    fmt.Println(strings.ReplaceAll(s, "a", "x")) // "xxbbcc"

    // Split and join
    csv := "apple,banana,cherry"
    fruits := strings.Split(csv, ",")
    fmt.Println(fruits) // [apple banana cherry]

    joined := strings.Join(fruits, " | ")
    fmt.Println(joined) // "apple | banana | cherry"

    // Trim
    padded := "  Hello World  "
    fmt.Println(strings.TrimSpace(padded))         // "Hello World"
    fmt.Println(strings.Trim("##Hello##", "#"))     // "Hello"
    fmt.Println(strings.TrimLeft("##Hello##", "#"))  // "Hello##"

    // Case conversion
    fmt.Println(strings.ToUpper("hello")) // "HELLO"
    fmt.Println(strings.ToLower("HELLO")) // "hello"

    // Repeat
    fmt.Println(strings.Repeat("Go", 3)) // "GoGoGo"

    // Count
    fmt.Println(strings.Count("banana", "an")) // 2
}
💡 Tip: When the separator for strings.Split is an empty string, the string is split into a slice of individual characters.

2. strconv Package

GO
package main

import (
    "fmt"
    "strconv"
)

func main() {
    // String → Integer
    num, err := strconv.Atoi("42")
    if err != nil {
        fmt.Println("Conversion failed:", err)
    }
    fmt.Println(num) // 42

    // Integer → String
    str := strconv.Itoa(42)
    fmt.Println(str) // "42"

    // String → Boolean
    b, err := strconv.ParseBool("true")
    fmt.Println(b, err) // true <nil>

    // String → Float
    f, err := strconv.ParseFloat("3.14", 64)
    fmt.Println(f, err) // 3.14 <nil>

    // Float → String
    // 'f' means normal format, -1 means minimum digits, 64 means float64
    s := strconv.FormatFloat(3.14, 'f', -1, 64)
    fmt.Println(s) // "3.14"

    // Formatted output (similar to C's sprintf)
    formatted := strconv.FormatInt(255, 16) // Hexadecimal
    fmt.Println(formatted) // "ff"
}
💡 Tip: strconv.Atoi is equivalent to strconv.ParseInt(s, 10, 0), returning the platform-dependent int type.

3. unicode/utf8 Package

GO
package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    str := "GoLangProg"

    // len() returns the number of bytes
    fmt.Println(len(str)) // 10 (each ASCII char takes 1 byte)

    // utf8.RuneCountInString() returns the number of characters
    fmt.Println(utf8.RuneCountInString(str)) // 10

    // Check if it's valid UTF-8
    fmt.Println(utf8.ValidString(str))  // true
    fmt.Println(utf8.ValidString("abc")) // true

    // Iterate over each rune in the string
    for i, r := range str {
        fmt.Printf("Index:%d Character:%c Unicode:%U\n", i, r, r)
    }
}
💡 Tip: When using range to iterate over a string, Go automatically iterates by rune (Unicode character), not by byte.

4. strings.Builder (Efficient Concatenation)

GO
package main

import (
    "fmt"
    "strings"
)

func main() {
    // ❌ Inefficient: creates a new string on every concatenation
    // result := ""
    // for i := 0; i < 1000; i++ {
    //     result += "a"  // allocates new memory each time
    // }

    // ✅ Efficient: use strings.Builder
    var builder strings.Builder
    for i := 0; i < 1000; i++ {
        builder.WriteString("a")
    }
    result := builder.String()
    fmt.Println(len(result)) // 1000

    // Pre-allocate capacity for even better performance
    var builder2 strings.Builder
    builder2.Grow(1000) // Pre-allocate 1000 bytes
    for i := 0; i < 1000; i++ {
        builder2.WriteString("b")
    }
    fmt.Println(builder2.Len()) // 1000
}
💡 Tip: strings.Builder internally uses a []byte slice, avoiding frequent memory allocations caused by string immutability.


Example Code

Example: String Statistics and Analysis (Difficulty ⭐)

GO
package main

import (
    "fmt"
    "strings"
    "unicode"
)

// Analyze the counts of different character types in a string
func analyzeString(s string) (letters, digits, spaces, others int) {
    for _, r := range s {
        switch {
        case unicode.IsLetter(r):
            letters++
        case unicode.IsDigit(r):
            digits++
        case unicode.IsSpace(r):
            spaces++
        default:
            others++
        }
    }
    return
}

func main() {
    text := "Hello, GoLang! 2024yr Version 2.0"

    letters, digits, spaces, others := analyzeString(text)
    fmt.Printf("Text: %q\n", text)
    fmt.Printf("Letters: %d\n", letters)
    fmt.Printf("Digits: %d\n", digits)
    fmt.Printf("Spaces: %d\n", spaces)
    fmt.Printf("Others: %d\n", others)

    // Count word frequency
    words := strings.Fields("the go the language the world")
    freq := make(map[string]int)
    for _, w := range words {
        freq[strings.ToLower(w)]++
    }
    fmt.Println("\nWord frequency:", freq)
}
▶ Try it Yourself

Output:

TEXT
Text: "Hello, GoLang! 2024yr Version 2.0"
Letters: 18
Digits: 7
Spaces: 5
Others: 2

Word frequency: map[go:1 language:1 the:3 world:1]

Example: CSV Parser (Difficulty ⭐⭐)

GO
package main

import (
    "fmt"
    "strings"
)

// Simple CSV line parser supporting quoted fields
func parseCSVLine(line string) []string {
    var fields []string
    var current strings.Builder
    inQuotes := false

    for _, r := range line {
        switch {
        case r == '"' && !inQuotes:
            // Enter quoted region
            inQuotes = true
        case r == '"' && inQuotes:
            // Leave quoted region
            inQuotes = false
        case r == ',' && !inQuotes:
            // Encountered delimiter, save current field
            fields = append(fields, current.String())
            current.Reset()
        default:
            // Normal character
            current.WriteRune(r)
        }
    }
    // Save the last field
    fields = append(fields, current.String())

    return fields
}

// Clean and format fields
func cleanFields(fields []string) []string {
    cleaned := make([]string, len(fields))
    for i, f := range fields {
        cleaned[i] = strings.TrimSpace(f)
    }
    return cleaned
}

func main() {
    // Simulated CSV data
    csvData := []string{
        `Alice,28,"Beijing, China"`,
        `Bob,35,"New York, USA"`,
        `Charlie,42,"London, UK"`,
    }

    fmt.Println("=== CSV Parse Results ===")
    for _, line := range csvData {
        fields := parseCSVLine(line)
        fields = cleanFields(fields)
        fmt.Printf("Name: %-10s Age: %-4s Location: %s\n",
            fields[0], fields[1], fields[2])
    }

    // Reverse operation: join a slice into a CSV line
    record := []string{"David", "30", "Shanghai, China"}
    csvLine := strings.Join(record, ",")
    fmt.Println("\nGenerated CSV line:", csvLine)
}
▶ Try it Yourself

Output:

TEXT
=== CSV Parse Results ===
Name: Alice      Age: 28   Location: Beijing, China
Name: Bob        Age: 35   Location: New York, USA
Name: Charlie    Age: 42   Location: London, UK

Generated CSV line: David,30,Shanghai, China

Example: Template Engine (Difficulty ⭐⭐⭐)

GO
package main

import (
    "fmt"
    "strconv"
    "strings"
)

// Simple template engine: replaces {{key}} with corresponding values
func renderTemplate(template string, data map[string]string) string {
    var result strings.Builder
    result.Grow(len(template) * 2) // Estimate capacity

    i := 0
    for i < len(template) {
        // Find "{{"
        if i+1 < len(template) && template[i] == '{' && template[i+1] == '{' {
            // Find corresponding "}}"
            end := strings.Index(template[i+2:], "}}")
            if end != -1 {
                key := strings.TrimSpace(template[i+2 : i+2+end])
                if value, ok := data[key]; ok {
                    result.WriteString(value)
                } else {
                    // Key not found, keep as-is
                    result.WriteString("{{" + key + "}}")
                }
                i += end + 4 // Skip "}}"
                continue
            }
        }
        result.WriteByte(template[i])
        i++
    }

    return result.String()
}

// Format table output
func formatTable(headers []string, rows [][]string) string {
    // Calculate max width for each column
    colWidths := make([]int, len(headers))
    for i, h := range headers {
        colWidths[i] = len(h)
    }
    for _, row := range rows {
        for i, cell := range row {
            if i < len(colWidths) && len(cell) > colWidths[i] {
                colWidths[i] = len(cell)
            }
        }
    }

    var b strings.Builder

    // Write header
    for i, h := range headers {
        b.WriteString(fmt.Sprintf("%-*s | ", colWidths[i], h))
    }
    b.WriteString("\n")

    // Write separator line
    for i := range headers {
        b.WriteString(strings.Repeat("-", colWidths[i]) + "-+-")
    }
    b.WriteString("\n")

    // Write data rows
    for _, row := range rows {
        for i, cell := range row {
            if i < len(colWidths) {
                b.WriteString(fmt.Sprintf("%-*s | ", colWidths[i], cell))
            }
        }
        b.WriteString("\n")
    }

    return b.String()
}

// Convert a number string to different base representations
func toBases(numStr string) (map[string]string, error) {
    num, err := strconv.ParseInt(numStr, 10, 64)
    if err != nil {
        return nil, err
    }

    return map[string]string{
        "decimal":     strconv.FormatInt(num, 10),
        "binary":      strconv.FormatInt(num, 2),
        "octal":       strconv.FormatInt(num, 8),
        "hexadecimal": strconv.FormatInt(num, 16),
    }, nil
}

func main() {
    // 1. Template rendering
    fmt.Println("=== Template Rendering ===")
    template := "Hello, {{name}}! Welcome to {{city}}. You have {{count}} new messages."
    data := map[string]string{
        "name":  "Alice",
        "city":  "Beijing",
        "count": "5",
    }
    fmt.Println(renderTemplate(template, data))

    // 2. Table formatting
    fmt.Println("\n=== Table Formatting ===")
    headers := []string{"Name", "Age", "City"}
    rows := [][]string{
        {"Alice", "28", "Beijing"},
        {"Bob", "35", "New York"},
        {"Charlie", "42", "London"},
    }
    fmt.Print(formatTable(headers, rows))

    // 3. Base conversion
    fmt.Println("\n=== Base Conversion ===")
    bases, _ := toBases("255")
    for name, value := range bases {
        fmt.Printf("%-12s: %s\n", name, value)
    }
}
▶ Try it Yourself

Output:

TEXT
=== Template Rendering ===
Hello, Alice! Welcome to Beijing. You have 5 new messages.

=== Table Formatting ===
Name    | Age | City    | 
--------+-----+---------+-
Alice   | 28  | Beijing | 
Bob     | 35  | New York| 
Charlie | 42  | London  | 

=== Base Conversion ===
decimal     : 255
binary      : 11111111
octal       : 377
hexadecimal : ff

Practical Application Scenarios

Scenario 1: Log Analyzer

GO
package main

import (
    "fmt"
    "strconv"
    "strings"
    "time"
)

// Log entry structure
type LogEntry struct {
    Timestamp string
    Level     string
    Message   string
    Source    string
}

// Parse a log line
// Format: [2024-01-15 10:30:00] [ERROR] Database connection failed (db-service)
func parseLogLine(line string) (*LogEntry, error) {
    entry := &LogEntry{}

    // Extract timestamp
    if start := strings.Index(line, "["); start != -1 {
        if end := strings.Index(line, "]"); end != -1 {
            entry.Timestamp = line[start+1 : end]
            line = line[end+1:]
        }
    }

    // Extract log level
    line = strings.TrimSpace(line)
    if start := strings.Index(line, "["); start != -1 {
        if end := strings.Index(line, "]"); end != -1 {
            entry.Level = line[start+1 : end]
            line = line[end+1:]
        }
    }

    // Extract message and source
    line = strings.TrimSpace(line)
    if parenStart := strings.LastIndex(line, "("); parenStart != -1 {
        if parenEnd := strings.LastIndex(line, ")"); parenEnd != -1 {
            entry.Source = line[parenStart+1 : parenEnd]
            entry.Message = strings.TrimSpace(line[:parenStart])
        }
    } else {
        entry.Message = line
    }

    return entry, nil
}

// Analyze log levels
func analyzeLogs(entries []LogEntry) map[string]int {
    stats := make(map[string]int)
    for _, e := range entries {
        stats[strings.ToUpper(e.Level)]++
    }
    return stats
}

// Filter logs containing a keyword
func filterLogs(entries []LogEntry, keyword string) []LogEntry {
    var filtered []LogEntry
    keyword = strings.ToLower(keyword)
    for _, e := range entries {
        if strings.Contains(strings.ToLower(e.Message), keyword) {
            filtered = append(filtered, e)
        }
    }
    return filtered
}

func main() {
    // Simulated log data
    logLines := []string{
        "[2024-01-15 10:30:00] [INFO] Application started (main-service)",
        "[2024-01-15 10:30:05] [INFO] Connected to database (db-service)",
        "[2024-01-15 10:31:00] [WARN] High memory usage detected (monitor)",
        "[2024-01-15 10:32:00] [ERROR] Database connection timeout (db-service)",
        "[2024-01-15 10:32:01] [ERROR] Retry failed, switching to backup (db-service)",
        "[2024-01-15 10:33:00] [INFO] Backup database connected (db-service)",
        "[2024-01-15 10:35:00] [DEBUG] Cache cleared (cache-service)",
    }

    // Parse all logs
    var entries []LogEntry
    for _, line := range logLines {
        entry, err := parseLogLine(line)
        if err == nil {
            entries = append(entries, *entry)
        }
    }

    // Log level statistics
    fmt.Println("=== Log Level Statistics ===")
    stats := analyzeLogs(entries)
    for level, count := range stats {
        fmt.Printf("  %s: %d entries\n", level, count)
    }

    // Filter error logs
    fmt.Println("\n=== Error Logs ===")
    for _, e := range entries {
        if strings.ToUpper(e.Level) == "ERROR" {
            fmt.Printf("  %s | %s | %s\n", e.Timestamp, e.Message, e.Source)
        }
    }

    // Search by keyword
    fmt.Println("\n=== Logs containing 'database' ===")
    filtered := filterLogs(entries, "database")
    for _, e := range filtered {
        fmt.Printf("  [%s] %s\n", e.Level, e.Message)
    }
}

Output:

TEXT
=== Log Level Statistics ===
  INFO: 3 entries
  WARN: 1 entries
  ERROR: 2 entries
  DEBUG: 1 entries

=== Error Logs ===
  2024-01-15 10:32:00 | Database connection timeout | db-service
  2024-01-15 10:32:01 | Retry failed, switching to backup | db-service

=== Logs containing 'database' ===
  [INFO] Connected to database
  [ERROR] Database connection timeout
  [INFO] Backup database connected

Scenario 2: User Input Validation and Sanitization

GO
package main

import (
    "fmt"
    "regexp"
    "strconv"
    "strings"
    "unicode"
)

// Sanitize user input for username
func sanitizeUsername(name string) (string, error) {
    // Remove leading and trailing spaces
    name = strings.TrimSpace(name)

    // Check length
    if len(name) < 3 {
        return "", fmt.Errorf("username too short (minimum 3 characters)")
    }
    if len(name) > 20 {
        return "", fmt.Errorf("username too long (maximum 20 characters)")
    }

    // Only allow letters, digits, and underscores
    var cleaned strings.Builder
    for _, r := range name {
        if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' {
            cleaned.WriteRune(r)
        }
    }

    result := cleaned.String()
    if len(result) < 3 {
        return "", fmt.Errorf("too few valid characters")
    }

    return strings.ToLower(result), nil
}

// Validate and parse phone number (Mainland China)
func parsePhone(phone string) (string, error) {
    // Remove all spaces and hyphens
    phone = strings.ReplaceAll(phone, " ", "")
    phone = strings.ReplaceAll(phone, "-", "")

    // Check if it starts with +86
    if strings.HasPrefix(phone, "+86") {
        phone = phone[3:]
    } else if strings.HasPrefix(phone, "86") {
        phone = phone[2:]
    }

    // Validate length
    if len(phone) != 11 {
        return "", fmt.Errorf("incorrect phone number length: %d digits", len(phone))
    }

    // Validate all digits
    for _, r := range phone {
        if !unicode.IsDigit(r) {
            return "", fmt.Errorf("phone number contains non-digit character: %c", r)
        }
    }

    // Validate starts with 1
    if !strings.HasPrefix(phone, "1") {
        return "", fmt.Errorf("phone number must start with 1")
    }

    return phone, nil
}

// Parse size string with units
func parseSize(sizeStr string) (int64, error) {
    sizeStr = strings.TrimSpace(strings.ToUpper(sizeStr))

    // Extract numeric and unit parts
    var numPart strings.Builder
    var unitPart strings.Builder

    for _, r := range sizeStr {
        if unicode.IsDigit(r) || r == '.' {
            numPart.WriteRune(r)
        } else if unicode.IsLetter(r) {
            unitPart.WriteRune(r)
        }
    }

    num, err := strconv.ParseFloat(numPart.String(), 64)
    if err != nil {
        return 0, fmt.Errorf("invalid number: %s", numPart.String())
    }

    // Convert to bytes based on unit
    unit := unitPart.String()
    multipliers := map[string]int64{
        "B":  1,
        "KB": 1024,
        "MB": 1024 * 1024,
        "GB": 1024 * 1024 * 1024,
        "TB": 1024 * 1024 * 1024 * 1024,
    }

    multiplier, ok := multipliers[unit]
    if !ok {
        return 0, fmt.Errorf("unknown unit: %s", unit)
    }

    return int64(num * float64(multiplier)), nil
}

func main() {
    // Username sanitization tests
    fmt.Println("=== Username Validation ===")
    usernames := []string{"  Alice_123  ", "ab", "A!@#B", "GoDeveloper2024"}
    for _, u := range usernames {
        result, err := sanitizeUsername(u)
        if err != nil {
            fmt.Printf("  %q → Error: %v\n", u, err)
        } else {
            fmt.Printf("  %q → %q\n", u, result)
        }
    }

    // Phone number parsing tests
    fmt.Println("\n=== Phone Number Parsing ===")
    phones := []string{"138 0013 8000", "+86-138-0013-8000", "12345", "23800138000"}
    for _, p := range phones {
        result, err := parsePhone(p)
        if err != nil {
            fmt.Printf("  %q → Error: %v\n", p, err)
        } else {
            fmt.Printf("  %q → %s\n", p, result)
        }
    }

    // File size parsing tests
    fmt.Println("\n=== File Size Parsing ===")
    sizes := []string{"1.5GB", "512MB", "1024KB", "100B", "2TB"}
    for _, s := range sizes {
        bytes, err := parseSize(s)
        if err != nil {
            fmt.Printf("  %s → Error: %v\n", s, err)
        } else {
            fmt.Printf("  %s → %d bytes\n", s, bytes)
        }
    }
}

Output:

TEXT
=== Username Validation ===
"  Alice_123  " → "alice_123"
"ab" → Error: username too short (minimum 3 characters)
"A!@#B" → Error: too few valid characters
"GoDeveloper2024" → "godeveloper2024"

=== Phone Number Parsing ===
"138 0013 8000" → 13800138000
"+86-138-0013-8000" → 13800138000
"12345" → Error: incorrect phone number length: 5 digits
"23800138000" → Error: phone number must start with 1

=== File Size Parsing ===
1.5GB → 1610612736 bytes
512MB → 536870912 bytes
1024KB → 1048576 bytes
100B → 100 bytes
2TB → 2199023255552 bytes

❓ FAQ

Q1: Why does len("GoLang") return 6 instead of 4?

GO
package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    s := "GoLang"

    // len() returns bytes, not characters
    fmt.Println("len():", len(s)) // 6

    // ASCII characters take 1 byte each in UTF-8
    // G(1) + o(1) + L(1) + a(1) + n(1) + g(1) = 6

    // Correct way to get character count
    fmt.Println("RuneCountInString():", utf8.RuneCountInString(s)) // 6

    // Or use range to count
    count := 0
    for range s {
        count++
    }
    fmt.Println("range count:", count) // 6
}

Key Point: When handling multi-byte characters like CJK, always use utf8.RuneCountInString() or range to get the true character count.

Q2: String concatenation — use + or strings.Builder?

GO
package main

import (
    "fmt"
    "strings"
)

func main() {
    // A few concatenations: just use + (compiler optimizes)
    s := "Hello" + " " + "World"
    fmt.Println(s)

    // Many concatenations: must use strings.Builder
    var builder strings.Builder
    for i := 0; i < 10000; i++ {
        builder.WriteString("a")
    }
    fmt.Println("Length:", builder.Len())

    // Pre-allocation further improves performance
    var builder2 strings.Builder
    builder2.Grow(10000) // Pre-allocate 10000 bytes
    for i := 0; i < 10000; i++ {
        builder2.WriteString("b")
    }
    fmt.Println("Length:", builder2.Len())
}

Rules of Thumb:

Scenario Recommended Approach
2-3 string concatenations + or fmt.Sprintf
Concatenation in loop (known count) strings.Builder + Grow()
Concatenation in loop (unknown count) strings.Builder

Q3: How to check if a string contains only specific characters?

GO
package main

import (
    "fmt"
    "strings"
    "unicode"
)

func main() {
    s := "Hello123"

    // Check if it contains only letters and digits
    isAlphanumeric := true
    for _, r := range s {
        if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
            isAlphanumeric = false
            break
        }
    }
    fmt.Println("Alphanumeric only:", isAlphanumeric)

    // Check if it contains only ASCII letters
    isASCII := true
    for _, r := range s {
        if r > 127 {
            isASCII = false
            break
        }
    }
    fmt.Println("ASCII only:", isASCII)

    // Check if it contains only a specific character set
    allowed := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    containsOnlyAllowed := true
    for _, r := range s {
        if !strings.ContainsRune(allowed, r) {
            containsOnlyAllowed = false
            break
        }
    }
    fmt.Println("Within allowed range:", containsOnlyAllowed)
}

Q4: When to use strings.Contains vs regular expressions?

GO
package main

import (
    "fmt"
    "regexp"
    "strings"
)

func main() {
    text := "My email is test@example.com, phone is 13800138000"

    // Simple search → use strings package (faster)
    fmt.Println(strings.Contains(text, "example.com")) // true

    // Pattern matching → use regular expressions
    emailRegex := regexp.MustCompile(`[\w.]+@[\w.]+\.\w+`)
    fmt.Println("Email:", emailRegex.FindString(text))

    phoneRegex := regexp.MustCompile(`1[3-9]\d{9}`)
    fmt.Println("Phone:", phoneRegex.FindString(text))
}

Guidelines:


📖 Summary

Topic Key Content
strings Package Contains, HasPrefix, HasSuffix, Index, Replace, Split, Join, Trim, ToUpper, ToLower, Count, Repeat, Fields
strconv Package Atoi, Itoa, ParseBool, ParseFloat, FormatInt, FormatFloat
unicode/utf8 RuneCountInString, ValidString, unicode.IsLetter/IsDigit/IsSpace
strings.Builder WriteString, WriteRune, WriteByte, Grow, String, Len
Core Principles Strings are immutable, len returns bytes, range iterates by rune, use Builder for many concatenations

📝 Exercises

Exercise 1: Basic — String Reversal

Write a function reverseString(s string) string that reverses a string. It must correctly handle Chinese characters.

Hint: You cannot simply convert the string to []byte and reverse it, because Chinese characters occupy multiple bytes.

GO
// Expected results
reverseString("Hello")   // "olleH"
reverseString("GoLang")  // "gnaLGo"

Exercise 2: Intermediate — CamelCase and Snake_case Conversion

Write two functions:

Hint: Use unicode.IsUpper to detect uppercase letter positions.

GO
// Expected results
camelToSnake("helloWorld")     // "hello_world"
camelToSnake("HTTPResponse")   // "http_response"
snakeToCamel("hello_world")    // "helloWorld"
snakeToCamel("http_response")  // "httpResponse"

Exercise 3: Challenge — Simple Markdown Heading Extractor

Write a function extractHeadings(md string) []string that extracts all headings from a Markdown document.

Hint: Headings start with #, and the number of # symbols indicates the heading level.

GO
// Input
md := `# Heading 1
This is body text
## Heading 2
### Heading 3
## Another Heading 2`

// Expected output
// ["# Heading 1", "## Heading 2", "### Heading 3", "## Another Heading 2"]

Next Lesson

Congratulations on completing string processing! In the next lesson, we will learn about File I/O Operations — how to read and write files, handle directories, and use buffered I/O for better performance.

👉 Lesson 20: File I/O

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏