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 とオーバーラップ
Memcpy と Memmove の違いは、オーバーラップ対応の有無です:
// オーバーラップの例
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.gosrc/strconv/atoi.gosrc/unicode/letter.go
ただし、課題では禁止されているので、参考程度にしてください。