goft - 解説

実装の詳細解説

文字判定関数のポイント

rune 型の理解

Go では、文字を表現するために rune 型(int32 のエイリアス)を使用します。これは Unicode コードポイントを表現します。

// 文字リテラルは rune 型
var r rune = 'あ'  // U+3042

// string を range で回すと rune が得られる
for _, r := range "Hello世界" {
    fmt.Printf("%c ", r)  // H e l l o 世 界
}

ASCII コード範囲の活用

文字判定は ASCII コード範囲の比較で効率的に実装できます:

// 'A' = 65, 'Z' = 90
func IsUpper(r rune) bool {
    return r >= 'A' && r <= 'Z'  // 65-90 の範囲
}

// 大文字小文字の変換: 差は32
// 'a' - 'A' = 97 - 65 = 32
func ToUpper(r rune) rune {
    if IsLower(r) {
        return r - 32  // または r - 'a' + 'A'
    }
    return r
}

文字列操作関数のポイント

UTF-8 と string の関係

Go の string は UTF-8 エンコードされたバイト列です。len() はバイト数を返します。

s := "Hello世界"
fmt.Println(len(s))           // 11 (5 + 3 + 3 バイト)
fmt.Println(Strlen(s))        // 7 (rune 数)
fmt.Println(len([]rune(s)))   // 7 (rune スライスに変換)

Strlen の実装バリエーション

// 方法1: range ループ
func Strlen(s string) int {
    count := 0
    for range s {
        count++
    }
    return count
}

// 方法2: rune スライス変換(メモリを多く使う)
func Strlen(s string) int {
    return len([]rune(s))
}

// 方法3: utf8.RuneCountInString(禁止)
// return utf8.RuneCountInString(s)

Split の実装パターン

区切り文字による分割は、ステートマシン的に考えると理解しやすい:

func Split(s string, sep rune) []string {
    // 状態: 現在のトークン、結果リスト
    var result []string
    var current []rune

    for _, r := range s {
        if r == sep {
            // 区切り文字を見つけた → 現在のトークンを確定
            result = append(result, string(current))
            current = nil
        } else {
            // 通常文字 → 現在のトークンに追加
            current = append(current, r)
        }
    }
    // 最後のトークンを追加
    result = append(result, string(current))

    return result
}

数値変換関数のポイント

Atoi の状態遷移

[空白] → [符号?] → [数字] → [終了]

func Atoi(s string) int {
    runes := []rune(s)
    i := 0

    // 状態1: 空白スキップ
    for i < len(runes) && IsSpace(runes[i]) {
        i++
    }

    // 状態2: 符号処理
    sign := 1
    if i < len(runes) && runes[i] == '-' {
        sign = -1
        i++
    } else if i < len(runes) && runes[i] == '+' {
        i++
    }

    // 状態3: 数字変換
    result := 0
    for i < len(runes) && IsDigit(runes[i]) {
        digit := int(runes[i] - '0')
        result = result*10 + digit  // ホーナー法
        i++
    }

    return sign * result
}

Itoa のアルゴリズム

整数を文字列に変換する際は、下位桁から取り出します:

func Itoa(n int) string {
    if n == 0 {
        return "0"
    }

    negative := n < 0
    if negative {
        n = -n
    }

    // 下位桁から取り出し、先頭に追加
    var digits []rune
    for n > 0 {
        digit := rune(n%10 + '0')  // 0-9 → '0'-'9'
        digits = append([]rune{digit}, digits...)  // 先頭に追加
        n /= 10
    }

    if negative {
        digits = append([]rune{'-'}, digits...)
    }

    return string(digits)
}

メモリ操作関数のポイント

Memmove とオーバーラップ

MemcpyMemmove の違いは、オーバーラップ対応の有無です:

// オーバーラップの例
src := []byte("ABCDEF")
dst := src[2:]  // src[2:] = "CDEF" と共有

// Memcpy だと問題が起きる可能性
Memcpy(dst, src, 4)
// 期待: "ABCDEF" → "ABABCD"
// 実際: オーバーラップで破壊される可能性

// Memmove は一時バッファで安全にコピー
Memmove(dst, src, 4)
// 確実に "ABCDEF" → "ABABCD"

よくある間違いと対策

1. バイト数と文字数の混同

// 間違い
func Strlen(s string) int {
    return len(s)  // バイト数を返してしまう
}

// 正しい
func Strlen(s string) int {
    count := 0
    for range s {
        count++
    }
    return count
}

2. 境界チェックの欠如

// 間違い(パニックする)
func Substr(s string, start, length int) string {
    runes := []rune(s)
    return string(runes[start:start+length])
}

// 正しい
func Substr(s string, start, length int) string {
    runes := []rune(s)
    if start < 0 || start >= len(runes) {
        return ""
    }
    end := start + length
    if end > len(runes) {
        end = len(runes)
    }
    return string(runes[start:end])
}

3. nil スライスの扱い

// 間違い
func Memset(b []byte, c byte, n int) []byte {
    for i := 0; i < n; i++ {
        b[i] = c  // b が nil だとパニック
    }
    return b
}

// 正しい
func Memset(b []byte, c byte, n int) []byte {
    if b == nil || n <= 0 {
        return b
    }
    // ...
}

最適化のヒント

1. メモリアロケーションの削減

// 非効率: 毎回アロケーション
var result string
for _, r := range runes {
    result += string(r)
}

// 効率的: 事前にキャパシティを確保
result := make([]rune, 0, len(runes))
for _, r := range runes {
    result = append(result, r)
}
return string(result)

2. strings.Builder の内部動作の理解

goft では使えませんが、原理を理解しておくと有用:

// strings.Builder は内部でバッファを2倍に拡張
// キャパシティを事前設定すると効率的
var b strings.Builder
b.Grow(expectedSize)

3. コピーの最適化

// copy() 組み込み関数は最適化されている
// goft では使えないが、原理は同じ
copy(dst, src)

// 同等の実装
for i := 0; i < len(src) && i < len(dst); i++ {
    dst[i] = src[i]
}

テストの書き方

テーブル駆動テスト

Go の慣用的なテストパターン:

func TestAtoi(t *testing.T) {
    tests := []struct {
        name     string
        input    string
        expected int
    }{
        {"zero", "0", 0},
        {"positive", "42", 42},
        {"negative", "-42", -42},
        {"leading space", "  123", 123},
        {"with plus", "+456", 456},
        {"trailing chars", "123abc", 123},
        {"empty", "", 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Atoi(tt.input)
            if result != tt.expected {
                t.Errorf("Atoi(%q) = %d, want %d", tt.input, result, tt.expected)
            }
        })
    }
}

エッジケースの網羅

// 境界値
{"max int", "2147483647", 2147483647},
{"min int", "-2147483648", -2147483648},

// 特殊ケース
{"only sign", "-", 0},
{"only spaces", "   ", 0},
{"unicode digits", "123", 0},  // 全角数字は変換しない

参考実装

Go の標準ライブラリのソースコードを読むことで、より深い理解が得られます:

  • src/strings/strings.go
  • src/strconv/atoi.go
  • src/unicode/letter.go

ただし、課題では禁止されているので、参考程度にしてください。