go-printf - 解説

実装の詳細解説

フォーマット文字列の解析

printf の核心はフォーマット文字列のパースです。状態機械で実装するのが一般的です:

// フォーマット指定子の構造
// %[フラグ][幅][.精度]指定子
// 例: %-10.5d

type formatSpec struct {
    flags     string  // "-", "+", " ", "0", "#"
    width     int     // 最小幅
    precision int     // 精度
    specifier rune    // 'd', 's', 'x' など
}

状態遷移の実装

func parseSpec(runes []rune, pos *int) formatSpec {
    spec := formatSpec{}

    // 状態1: フラグ解析
    // '-', '+', ' ', '0', '#' を連続して読む
    for isFlag(runes[*pos]) {
        spec.flags += string(runes[*pos])
        (*pos)++
    }

    // 状態2: 幅解析
    // 数字を読む(または * で引数から取得)
    if isDigit(runes[*pos]) {
        spec.width = parseNumber(runes, pos)
    }

    // 状態3: 精度解析
    // '.' の後の数字を読む
    if runes[*pos] == '.' {
        (*pos)++
        spec.precision = parseNumber(runes, pos)
    }

    // 状態4: 指定子
    spec.specifier = runes[*pos]

    return spec
}

進数変換のアルゴリズム

任意の基数への変換は、繰り返し除算で行います:

func intToBase(n uint64, base int, upper bool) string {
    if n == 0 {
        return "0"
    }

    digits := "0123456789abcdef"
    if upper {
        digits = "0123456789ABCDEF"
    }

    var result []byte
    for n > 0 {
        remainder := n % uint64(base)
        result = append([]byte{digits[remainder]}, result...)
        n /= uint64(base)
    }

    return string(result)
}

型アサーションのパターン

Go の可変長引数 interface{} から具体的な型を取り出すには、型スイッチを使います:

func formatInt(arg interface{}, base int) string {
    var n int64

    switch v := arg.(type) {
    case int:
        n = int64(v)
    case int8:
        n = int64(v)
    case int16:
        n = int64(v)
    case int32:
        n = int64(v)
    case int64:
        n = v
    default:
        return "%!d(BADTYPE)"
    }

    // n を使って変換
    return intToBase(uint64(n), base, false)
}

よくある間違いと対策

1. 負数の処理

// 間違い: 負数で無限ループ
func intToBase(n int64, base int) string {
    for n > 0 {  // 負数は常に > 0 でない
        // ...
    }
}

// 正しい: 符号を分離して処理
func formatInt(n int64, base int) string {
    negative := n < 0
    if negative {
        n = -n
    }
    s := intToBase(uint64(n), base)
    if negative {
        s = "-" + s
    }
    return s
}

2. フラグの競合

// フラグの優先順位
// 1. '-' と '0' が両方ある場合、'-' が優先
// 2. '+' と ' ' が両方ある場合、'+' が優先

if hasFlag(flags, '-') {
    // 左寄せ('0' は無視される)
    padChar = ' '
} else if hasFlag(flags, '0') {
    padChar = '0'
}

3. 精度の意味の違い

// 整数の精度: 最小桁数(ゼロ埋め)
Sprintf("%.5d", 42)  // "00042"

// 文字列の精度: 最大文字数(切り詰め)
Sprintf("%.3s", "hello")  // "hel"

最適化のヒント

1. バッファの事前確保

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

// 効率的: 事前にバッファを確保
result := make([]byte, 0, estimatedSize)
for _, c := range chars {
    result = append(result, byte(c))
}

2. 文字列の直接構築

// strings.Builder を使わない場合
// バイトスライスを使用
buf := make([]byte, 0, 64)
buf = append(buf, prefix...)
buf = append(buf, digits...)
return string(buf)

テストのポイント

エッジケースの網羅

tests := []struct {
    format   string
    args     []interface{}
    expected string
}{
    // 基本ケース
    {"%d", []interface{}{42}, "42"},

    // エッジケース
    {"%d", []interface{}{0}, "0"},
    {"%d", []interface{}{-1}, "-1"},
    {"%d", []interface{}{-2147483648}, "-2147483648"},

    // フラグの組み合わせ
    {"%-+10d", []interface{}{42}, "+42       "},
    {"%+-10d", []interface{}{42}, "+42       "},

    // 精度
    {"%.0d", []interface{}{0}, ""},  // 精度0でゼロは空文字
    {"%.5d", []interface{}{42}, "00042"},

    // 幅と精度の組み合わせ
    {"%10.5d", []interface{}{42}, "     00042"},
}

発展的なトピック

浮動小数点の変換(ボーナス)

IEEE 754 形式の浮動小数点数を文字列に変換するアルゴリズム:

  • 符号ビットを抽出
  • 指数部を抽出
  • 仮数部を抽出
  • 適切な形式で出力

func formatFloat(f float64, spec formatSpec) string {
    // 簡易実装
    // 実際には Dragon4 や Grisu3 アルゴリズムが使われる
}

カラー出力(ボーナス)

ANSI エスケープシーケンスを使用:

var colors = map[string]string{
    "red":    "\033[31m",
    "green":  "\033[32m",
    "reset":  "\033[0m",
}

func expandColors(s string) string {
    // {red} → \033[31m に変換
}