文字列処理

レッスン19:文字列処理

🎯 生活での例え

あなたが図書館の司書であると想像してください。毎日大量のテキスト作業を処理しています:

文字列処理はプログラムにおける「テキスト作業」です。ほぼすべてのプログラムがテキストを扱う必要があります。


コアコンセプト

Goは文字列を処理するためのいくつかの標準ライブラリを提供しています:

パッケージ 用途 主な関数
strings 文字列の検索、置換、分割、結合など Contains, Replace, Split, Join, Trim
strconv 文字列と他の型間の変換 Atoi, Itoa, ParseBool, FormatFloat
unicode/utf8 UTF-8エンコーディング関連の操作 RuneCountInString, ValidString
strings.Builder 多数の文字列の効率的な連結 WriteString, String

重要なポイント:

  1. Goの文字列は不変です — 修正するたびに新しい文字列が作成されます
  2. Goの文字列は底层ではUTF-8エンコードされたバイト列です
  3. len(str)バイト数を返します。文字数ではありません
  4. runeはGoのUnicodeコードポイントを表現する型です(本质上はint32

基本構文と使い方

1. stringsパッケージ

GO
package main

import (
    "fmt"
    "strings"
)

func main() {
    str := "Hello, Go语言!"

    // 検索
    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

    // 置換
    result := strings.Replace(str, "Go", "Golang", 1)
    fmt.Println(result) // "Hello, Golang语言!"

    // 全体置換
    s := "aabbcc"
    fmt.Println(strings.ReplaceAll(s, "a", "x")) // "xxbbcc"

    // 分割と結合
    csv := "apple,banana,cherry"
    fruits := strings.Split(csv, ",")
    fmt.Println(fruits) // [apple banana cherry]

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

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

    // 大文字小文字変換
    fmt.Println(strings.ToUpper("hello")) // "HELLO"
    fmt.Println(strings.ToLower("HELLO")) // "hello"

    // 繰り返し
    fmt.Println(strings.Repeat("Go", 3)) // "GoGoGo"

    // カウント
    fmt.Println(strings.Count("banana", "an")) // 2
}
💡 ヒント: strings.Splitの区切り文字が空文字列の場合、文字列は個々の文字のスライスに分割されます。

2. strconvパッケージ

GO
package main

import (
    "fmt"
    "strconv"
)

func main() {
    // 文字列 → 整数
    num, err := strconv.Atoi("42")
    if err != nil {
        fmt.Println("変換失敗:", err)
    }
    fmt.Println(num) // 42

    // 整数 → 文字列
    str := strconv.Itoa(42)
    fmt.Println(str) // "42"

    // 文字列 → ブール値
    b, err := strconv.ParseBool("true")
    fmt.Println(b, err) // true <nil>

    // 文字列 → 浮動小数点数
    f, err := strconv.ParseFloat("3.14", 64)
    fmt.Println(f, err) // 3.14 <nil>

    // 浮動小数点数 → 文字列
    // 'f'は通常フォーマット、-1は最小桁数、64はfloat64を意味します
    s := strconv.FormatFloat(3.14, 'f', -1, 64)
    fmt.Println(s) // "3.14"

    // フォーマット出力(C言語のsprintfに類似)
    formatted := strconv.FormatInt(255, 16) // 16進数
    fmt.Println(formatted) // "ff"
}
💡 ヒント: strconv.Atoistrconv.ParseInt(s, 10, 0)と等価で、プラットフォーム依存のint型を返します。

3. unicode/utf8パッケージ

GO
package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    str := "Go语言编程"

    // len()はバイト数を返します
    fmt.Println(len(str)) // 14(中国語の各文字は3バイト)

    // utf8.RuneCountInString()は文字数を返します
    fmt.Println(utf8.RuneCountInString(str)) // 7

    // 有効なUTF-8かチェック
    fmt.Println(utf8.ValidString(str))  // true
    fmt.Println(utf8.ValidString("abc")) // true

    // 文字列の各runeを繰り返し処理
    for i, r := range str {
        fmt.Printf("インデックス:%d 文字:%c Unicode:%U\n", i, r, r)
    }
}
💡 ヒント: rangeで文字列を繰り返し処理する際、Goは自動的にバイト単位ではなくrune(Unicode文字)単位で繰り返します。

4. strings.Builder(効率的な連結)

GO
package main

import (
    "fmt"
    "strings"
)

func main() {
    // ❌ 非効率:連結のたびに新しい文字列が作成される
    // result := ""
    // for i := 0; i < 1000; i++ {
    //     result += "a"  // 毎回新しいメモリを割り当てる
    // }

    // ✅ 効率的:strings.Builderを使用
    var builder strings.Builder
    for i := 0; i < 1000; i++ {
        builder.WriteString("a")
    }
    result := builder.String()
    fmt.Println(len(result)) // 1000

    // 事前割り当てでさらにパフォーマンス向上
    var builder2 strings.Builder
    builder2.Grow(1000) // 1000バイトを事前割り当て
    for i := 0; i < 1000; i++ {
        builder2.WriteString("b")
    }
    fmt.Println(builder2.Len()) // 1000
}
💡 ヒント: strings.Builderは内部的に[]byteスライスを使用し、文字列の不変性による頻繁なメモリ割り当てを回避します。


例題コード

例:文字列の統計と分析(難易度⭐)

GO
package main

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

// 文字列内の異なる文字タイプのカウントを分析
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, Go语言! 2024年 Version 2.0"

    letters, digits, spaces, others := analyzeString(text)
    fmt.Printf("テキスト: %q\n", text)
    fmt.Printf("文字数: %d\n", letters)
    fmt.Printf("数字: %d\n", digits)
    fmt.Printf("スペース: %d\n", spaces)
    fmt.Printf("その他: %d\n", others)

    // 単語頻度をカウント
    words := strings.Fields("the go the language the world")
    freq := make(map[string]int)
    for _, w := range words {
        freq[strings.ToLower(w)]++
    }
    fmt.Println("\n単語頻度:", freq)
}
▶ 試してみよう

出力:

TEXT
テキスト: "Hello, Go语言! 2024年 Version 2.0"
文字数: 17
数字: 6
スペース: 5
その他: 3

単語頻度: map[go:1 language:1 the:3 world:1]

例:CSVパーサー(難易度⭐⭐)

GO
package main

import (
    "fmt"
    "strings"
)

// 引用符付きフィールドをサポートするシンプルなCSV行パーサー
func parseCSVLine(line string) []string {
    var fields []string
    var current strings.Builder
    inQuotes := false

    for _, r := range line {
        switch {
        case r == '"' && !inQuotes:
            // 引用符領域に入る
            inQuotes = true
        case r == '"' && inQuotes:
            // 引用符領域を出る
            inQuotes = false
        case r == ',' && !inQuotes:
            // 区切り文字に到達、現在のフィールドを保存
            fields = append(fields, current.String())
            current.Reset()
        default:
            // 通常の文字
            current.WriteRune(r)
        }
    }
    // 最後のフィールドを保存
    fields = append(fields, current.String())

    return 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() {
    // シミュレーション用CSVデータ
    csvData := []string{
        `Alice,28,"Beijing, China"`,
        `Bob,35,"New York, USA"`,
        `Charlie,42,"London, UK"`,
    }

    fmt.Println("=== CSV解析結果 ===")
    for _, line := range csvData {
        fields := parseCSVLine(line)
        fields = cleanFields(fields)
        fmt.Printf("名前: %-10s 年齢: %-4s 場所: %s\n",
            fields[0], fields[1], fields[2])
    }

    // 逆操作:スライスをCSV行に結合
    record := []string{"David", "30", "Shanghai, China"}
    csvLine := strings.Join(record, ",")
    fmt.Println("\n生成されたCSV行:", csvLine)
}
▶ 試してみよう

出力:

TEXT
=== CSV解析結果 ===
名前: Alice      年齢: 28   場所: Beijing, China
名前: Bob        年齢: 35   場所: New York, USA
名前: Charlie    年齢: 42   場所: London, UK

生成されたCSV行: David,30,Shanghai, China

例:テンプレートエンジン(難易度⭐⭐⭐)

GO
package main

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

// シンプルなテンプレートエンジン:{{key}}を対応する値に置換
func renderTemplate(template string, data map[string]string) string {
    var result strings.Builder
    result.Grow(len(template) * 2) // 容量を推定

    i := 0
    for i < len(template) {
        // "{{"を探す
        if i+1 < len(template) && template[i] == '{' && template[i+1] == '{' {
            // 対応する "}}"を探す
            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 {
                    // キーが見つからない場合はそのまま保持
                    result.WriteString("{{" + key + "}}")
                }
                i += end + 4 // "}}"をスキップ
                continue
            }
        }
        result.WriteByte(template[i])
        i++
    }

    return result.String()
}

// テーブル出力をフォーマット
func formatTable(headers []string, rows [][]string) string {
    // 各列の最大幅を計算
    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

    // ヘッダーを書き込み
    for i, h := range headers {
        b.WriteString(fmt.Sprintf("%-*s | ", colWidths[i], h))
    }
    b.WriteString("\n")

    // 区切り線を書き込み
    for i := range headers {
        b.WriteString(strings.Repeat("-", colWidths[i]) + "-+-")
    }
    b.WriteString("\n")

    // データ行を書き込み
    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()
}

// 数値文字列を異なる基数表現に変換
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. テンプレートレンダリング
    fmt.Println("=== テンプレートレンダリング ===")
    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. テーブルフォーマット
    fmt.Println("\n=== テーブルフォーマット ===")
    headers := []string{"Name", "Age", "City"}
    rows := [][]string{
        {"Alice", "28", "Beijing"},
        {"Bob", "35", "New York"},
        {"Charlie", "42", "London"},
    }
    fmt.Print(formatTable(headers, rows))

    // 3. 基数変換
    fmt.Println("\n=== 基数変換 ===")
    bases, _ := toBases("255")
    for name, value := range bases {
        fmt.Printf("%-12s: %s\n", name, value)
    }
}
▶ 試してみよう

出力:

TEXT
=== テンプレートレンダリング ===
Hello, Alice! Welcome to Beijing. You have 5 new messages.

=== テーブルフォーマット ===
Name    | Age | City    | 
--------+-----+---------+-
Alice   | 28  | Beijing | 
Bob     | 35  | New York| 
Charlie | 42  | London  | 

=== 基数変換 ===
decimal     : 255
binary      : 11111111
octal       : 377
hexadecimal : ff

実践的な応用シナリオ

シナリオ1:ログアナライザー

GO
package main

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

// ログエントリー構造体
type LogEntry struct {
    Timestamp string
    Level     string
    Message   string
    Source    string
}

// ログ行を解析
// フォーマット: [2024-01-15 10:30:00] [ERROR] Database connection failed (db-service)
func parseLogLine(line string) (*LogEntry, error) {
    entry := &LogEntry{}

    // タイムスタンプを抽出
    if start := strings.Index(line, "["); start != -1 {
        if end := strings.Index(line, "]"); end != -1 {
            entry.Timestamp = line[start+1 : end]
            line = line[end+1:]
        }
    }

    // ログレベルを抽出
    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:]
        }
    }

    // メッセージとソースを抽出
    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
}

// ログレベルを分析
func analyzeLogs(entries []LogEntry) map[string]int {
    stats := make(map[string]int)
    for _, e := range entries {
        stats[strings.ToUpper(e.Level)]++
    }
    return stats
}

// キーワードを含むログをフィルタリング
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() {
    // シミュレーション用ログデータ
    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)",
    }

    // すべてのログを解析
    var entries []LogEntry
    for _, line := range logLines {
        entry, err := parseLogLine(line)
        if err == nil {
            entries = append(entries, *entry)
        }
    }

    // ログレベル統計
    fmt.Println("=== ログレベル統計 ===")
    stats := analyzeLogs(entries)
    for level, count := range stats {
        fmt.Printf("  %s: %d件\n", level, count)
    }

    // エラーログをフィルタリング
    fmt.Println("\n=== エラーログ ===")
    for _, e := range entries {
        if strings.ToUpper(e.Level) == "ERROR" {
            fmt.Printf("  %s | %s | %s\n", e.Timestamp, e.Message, e.Source)
        }
    }

    // キーワードで検索
    fmt.Println("\n=== 'database'を含むログ ===")
    filtered := filterLogs(entries, "database")
    for _, e := range filtered {
        fmt.Printf("  [%s] %s\n", e.Level, e.Message)
    }
}

出力:

TEXT
=== ログレベル統計 ===
  INFO: 3件
  WARN: 1件
  ERROR: 2件
  DEBUG: 1件

=== エラーログ ===
  2024-01-15 10:32:00 | Database connection timeout | db-service
  2024-01-15 10:32:01 | Retry failed, switching to backup | db-service

=== 'database'を含むログ ===
  [INFO] Connected to database
  [ERROR] Database connection timeout
  [INFO] Backup database connected

シナリオ2:ユーザー入力のバリデーションとサニタイズ

GO
package main

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

// ユーザー名の入力をサニタイズ
func sanitizeUsername(name string) (string, error) {
    // 先頭と末尾のスペースを削除
    name = strings.TrimSpace(name)

    // 長さをチェック
    if len(name) < 3 {
        return "", fmt.Errorf("ユーザー名が短すぎます(最低3文字)")
    }
    if len(name) > 20 {
        return "", fmt.Errorf("ユーザー名が長すぎます(最大20文字)")
    }

    // 文字、数字、アンダースコアのみ許可
    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("有効な文字が少なすぎます")
    }

    return strings.ToLower(result), nil
}

// 電話番号をバリデーションして解析(中国大陆)
func parsePhone(phone string) (string, error) {
    // すべてのスペースとハイフンを削除
    phone = strings.ReplaceAll(phone, " ", "")
    phone = strings.ReplaceAll(phone, "-", "")

    // +86で始まるかチェック
    if strings.HasPrefix(phone, "+86") {
        phone = phone[3:]
    } else if strings.HasPrefix(phone, "86") {
        phone = phone[2:]
    }

    // 長さをバリデーション
    if len(phone) != 11 {
        return "", fmt.Errorf("電話番号の長さが正しくありません: %d桁", len(phone))
    }

    // すべて数字かバリデーション
    for _, r := range phone {
        if !unicode.IsDigit(r) {
            return "", fmt.Errorf("電話番号に数字以外の文字が含まれています: %c", r)
        }
    }

    // 1で始まるかバリデーション
    if !strings.HasPrefix(phone, "1") {
        return "", fmt.Errorf("電話番号は1で始まる必要があります")
    }

    return phone, nil
}

// 単位付きサイズ文字列を解析
func parseSize(sizeStr string) (int64, error) {
    sizeStr = strings.TrimSpace(strings.ToUpper(sizeStr))

    // 数値部分と単位部分を抽出
    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("無効な数値: %s", numPart.String())
    }

    // 単位に基づいてバイトに変換
    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("不明な単位: %s", unit)
    }

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

func main() {
    // ユーザー名サニタイズテスト
    fmt.Println("=== ユーザー名バリデーション ===")
    usernames := []string{"  Alice_123  ", "ab", "A!@#B", "GoDeveloper2024"}
    for _, u := range usernames {
        result, err := sanitizeUsername(u)
        if err != nil {
            fmt.Printf("  %q → エラー: %v\n", u, err)
        } else {
            fmt.Printf("  %q → %q\n", u, result)
        }
    }

    // 電話番号解析テスト
    fmt.Println("\n=== 電話番号解析 ===")
    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 → エラー: %v\n", p, err)
        } else {
            fmt.Printf("  %q → %s\n", p, result)
        }
    }

    // ファイルサイズ解析テスト
    fmt.Println("\n=== ファイルサイズ解析 ===")
    sizes := []string{"1.5GB", "512MB", "1024KB", "100B", "2TB"}
    for _, s := range sizes {
        bytes, err := parseSize(s)
        if err != nil {
            fmt.Printf("  %s → エラー: %v\n", s, err)
        } else {
            fmt.Printf("  %s → %dバイト\n", s, bytes)
        }
    }
}

出力:

TEXT
=== ユーザー名バリデーション ===
  "  Alice_123  " → "alice_123"
  "ab" → エラー: ユーザー名が短すぎます(最低3文字)
  "A!@#B" → エラー: 有効な文字が少なすぎます
  "GoDeveloper2024" → "godeveloper2024"

=== 電話番号解析 ===
  "138 0013 8000" → 13800138000
  "+86-138-0013-8000" → 13800138000
  "12345" → エラー: 電話番号の長さが正しくありません: 5桁
  "23800138000" → エラー: 電話番号は1で始まる必要があります

=== ファイルサイズ解析 ===
1.5GB → 1610612736バイト
512MB → 536870912バイト
1024KB → 1048576バイト
100B → 100バイト
2TB → 2199023255552バイト

❓ よくある質問

質問1:なぜlen("Go语言")は4ではなく8を返すのですか?

GO
package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    s := "Go语言"

    // len()はバイト数を返します。文字数ではありません
    fmt.Println("len():", len(s)) // 8

    // 中国語の文字はUTF-8で各3バイト
    // G(1) + o(1) + 语(3) + 言(3) = 8

    // 文字数を取得する正しい方法
    fmt.Println("RuneCountInString():", utf8.RuneCountInString(s)) // 4

    // またはrangeでカウント
    count := 0
    for range s {
        count++
    }
    fmt.Println("range count:", count) // 4
}

重要なポイント: 中国語のようなマルチバイト文字を扱う場合は、常にutf8.RuneCountInString()またはrangeを使用して真の文字数を取得してください。

質問2:文字列連結 — +strings.Builderのどちらを使うべきですか?

GO
package main

import (
    "fmt"
    "strings"
)

func main() {
    // 少数の連結:+を使用即可(コンパイラが最適化)
    s := "Hello" + " " + "World"
    fmt.Println(s)

    // 多数の連結:strings.Builderを使用すべき
    var builder strings.Builder
    for i := 0; i < 10000; i++ {
        builder.WriteString("a")
    }
    fmt.Println("長さ:", builder.Len())

    // 事前割り当てでさらにパフォーマンス向上
    var builder2 strings.Builder
    builder2.Grow(10000) // 10000バイトを事前割り当て
    for i := 0; i < 10000; i++ {
        builder2.WriteString("b")
    }
    fmt.Println("長さ:", builder2.Len())
}

目安:

シナリオ 推奨方法
2〜3個の文字列連結 +またはfmt.Sprintf
ループ内の連結(回数既知) strings.Builder + Grow()
ループ内の連結(回数不明) strings.Builder

質問3:文字列が特定の文字のみを含むかチェックするにはどうすればいいですか?

GO
package main

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

func main() {
    s := "Hello123"

    // 文字と数字のみを含むかチェック
    isAlphanumeric := true
    for _, r := range s {
        if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
            isAlphanumeric = false
            break
        }
    }
    fmt.Println("英数字のみ:", isAlphanumeric)

    // ASCII文字のみを含むかチェック
    isASCII := true
    for _, r := range s {
        if r > 127 {
            isASCII = false
            break
        }
    }
    fmt.Println("ASCIIのみ:", isASCII)

    // 特定の文字セットのみを含むかチェック
    allowed := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    containsOnlyAllowed := true
    for _, r := range s {
        if !strings.ContainsRune(allowed, r) {
            containsOnlyAllowed = false
            break
        }
    }
    fmt.Println("許可範囲内:", containsOnlyAllowed)
}

質問4:strings.Containsと正規表現のどちらを什么时候使用すべきですか?

GO
package main

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

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

    // シンプルな検索 → stringsパッケージを使用(高速)
    fmt.Println(strings.Contains(text, "example.com")) // true

    // パターンマッチング → 正規表現を使用
    emailRegex := regexp.MustCompile(`[\w.]+@[\w.]+\.\w+`)
    fmt.Println("メール:", emailRegex.FindString(text))

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

ガイドライン:


📖 まとめ

トピック 主な内容
stringsパッケージ Contains, HasPrefix, HasSuffix, Index, Replace, Split, Join, Trim, ToUpper, ToLower, Count, Repeat, Fields
strconvパッケージ Atoi, Itoa, ParseBool, ParseFloat, FormatInt, FormatFloat
unicode/utf8 RuneCountInString, ValidString, unicode.IsLetter/IsDigit/IsSpace
strings.Builder WriteString, WriteRune, WriteByte, Grow, String, Len
コア原則 文字列は不変、lenはバイト数を返す、rangeはrune単位で繰り返し、多数の連結にはBuilderを使用

📝 演習

演習1:基礎 — 文字列の反転

文字列を反転する関数reverseString(s string) stringを書いてください。中国語の文字を正しく処理できる必要があります。

ヒント: 文字列を単純に[]byteに変換して反転することはできません。中国語の文字は複数バイトを占めるためです。

GO
// 期待結果
reverseString("Hello")   // "olleH"
reverseString("Go语言")  // "言语oG"

演習2:中級 — キャメルケースとスネークケースの変換

2つの関数を書いてください:

ヒント: unicode.IsUpperを使用して大文字の位置を検出してください。

GO
// 期待結果
camelToSnake("helloWorld")     // "hello_world"
camelToSnake("HTTPResponse")   // "http_response"
snakeToCamel("hello_world")    // "helloWorld"
snakeToCamel("http_response")  // "httpResponse"

演習3:チャレンジ — シンプルなMarkdown見出し抽出器

Markdown文書からすべての見出しを抽出する関数extractHeadings(md string) []stringを書いてください。

ヒント: 見出しは#で始まり、#の数が見出しレベルを示します。

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

// 期待出力
// ["# Heading 1", "## Heading 2", "### Heading 3", "## Another Heading 2"]

次のレッスン

文字列処理を完了しました!次のレッスンでは、ファイルI/O操作について学びます。ファイルの読み書き、ディレクトリの処理、バッファリングされたI/Oによるパフォーマンス向上について解説します。

👉 レッスン20:ファイルI/O

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%