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 に変換
}