LoginSignup
0

posted at

updated at

スライスのゼロ値を Print すると [] が出力される理由を探ってみた

この記事はフラー株式会社 Advent Calendar 2022の21日目の記事です。

前書き

以下を実行すると <nil>が出力されます。

func main() {
	fmt.Print(nil) // -> <nil>
}

ところで、Goの言語仕様により、スライスのゼロ値はnilです。そのため、スライスを宣言しプリントすると、<nil> が出力されると期待しますが、実際は[]が出力されます。この記事はその謎を追いました。

func main() {
	var a []int
	fmt.Print(a) // -> []
}

ネタバレ

[]が出力される理由は、fmt.Printを適用したaの実行時の型がスライス型であり、
fmt.Printaの実行時の型をみて、空のスライスである[]を出力したためです。

こう書くと、当たり前のような感じもしてきます。
これだけだと味気ないため、fmt.Printの中身を掘り、[]が出力されるまでの過程を説明します。

トップレベルから

ソースコードはこちら

Printでは可変長引数を取れるようになっていて、doPrintでバラしています。
個々の出力は p.printArg(arg, 'v') で行われています。

func Print(a ...any) (n int, err error) {
	return Fprint(os.Stdout, a...)
}
func Fprint(w io.Writer, a ...any) (n int, err error) {
	p := newPrinter()
	p.doPrint(a)
	n, err = w.Write(p.buf)
	p.free()
	return
}
func (p *pp) doPrint(a []any) {
	prevString := false
	for argNum, arg := range a {
		isString := arg != nil && reflect.TypeOf(arg).Kind() == reflect.String
		// Add a space between two non-string arguments.
		if argNum > 0 && !isString && !prevString {
			p.buf.writeByte(' ')
		}
		p.printArg(arg, 'v') 
		prevString = isString
	}
}

p.printArg(arg, 'v') から

以下のifブロックに遷移すると、nilAngleString(=<nil>) が出力され処理が終わります。ここで、ifに遷移しない理由ですが、nilの型がanyである一方、argの(動的な)型が[]intであるため、型が一致せず、arg == nilfalseを返すためです。

ただし、この挙動は仕様に明記されていないため、厳密さに欠く部分があります。以上の結論は、こちらの記事を参考に出しました。
https://forum.golangbridge.org/t/a-nil-a-b-b-nil-with-pointers-and-interface/10593

func (p *pp) printArg(arg any, verb rune) {
	p.arg = arg
	p.value = reflect.Value{}

	if arg == nil {
		switch verb {
		case 'T', 'v':
			p.fmt.padString(nilAngleString) // verb は 'v' なのでこちらに遷移する
		default:
			p.badVerb(verb)
		}
		return
	}
...

p.printArg(arg, 'v') 続き

argの型情報をチェックし遷移します。[]int はどの型にも当てはまらないため、defaultに遷移し、最終的にp.printValueが呼び出されます。

	// Some types can be done without reflection.
	switch f := arg.(type) {
	case bool:
		p.fmtBool(f, verb)
	case float32:
		p.fmtFloat(float64(f), 32, verb)
	case float64:
		p.fmtFloat(f, 64, verb)
	case complex64:
		p.fmtComplex(complex128(f), 64, verb)
	case complex128:
		p.fmtComplex(f, 128, verb)
	case int:
		p.fmtInteger(uint64(f), signed, verb)
	case int8:
		p.fmtInteger(uint64(f), signed, verb)
	case int16:
		p.fmtInteger(uint64(f), signed, verb)
	case int32:
		p.fmtInteger(uint64(f), signed, verb)
	case int64:
		p.fmtInteger(uint64(f), signed, verb)
	case uint:
		p.fmtInteger(uint64(f), unsigned, verb)
	case uint8:
		p.fmtInteger(uint64(f), unsigned, verb)
	case uint16:
		p.fmtInteger(uint64(f), unsigned, verb)
	case uint32:
		p.fmtInteger(uint64(f), unsigned, verb)
	case uint64:
		p.fmtInteger(f, unsigned, verb)
	case uintptr:
		p.fmtInteger(uint64(f), unsigned, verb)
	case string:
		p.fmtString(f, verb)
	case []byte:
		p.fmtBytes(f, verb, "[]byte")
	case reflect.Value: // fmt.Print に reflect.Value(hoge) を渡した場合
		// Handle extractable values with special methods
		// since printValue does not handle them at depth 0.
		if f.IsValid() && f.CanInterface() {
			p.arg = f.Interface()
			if p.handleMethods(verb) {
				return
			}
		}
		p.printValue(f, verb, 0)
	default:
		// If the type is not simple, it might have methods.
		if !p.handleMethods(verb) {
			// Need to use reflection, since the type had no
			// interface methods that could be used for formatting.
			p.printValue(reflect.ValueOf(f), verb, 0)
		}
	}

printValueの中へ (ここで最後です)

リフレクションを用いて、型の種類から出力を出し分けています。
最初に宣言したvar a []intavalue.Kind()reflect.Sliceであるため、空のスライス([]int{})をfmt.Printに渡した場合と同じ出力結果になります。

func (p *pp) printValue(value reflect.Value, verb rune, depth int) {
	// ...
	switch f := value; value.Kind() {
	// ...
	case reflect.Array, reflect.Slice:
		// ...
		if p.fmt.sharpV {
        // ...
		} else { // ここ!
			p.buf.writeByte('[')
			for i := 0; i < f.Len(); i++ { // f.Len() == 0 なので for文に遷移しない
				if i > 0 {
					p.buf.writeByte(' ')
				}
				p.printValue(f.Index(i), verb, depth+1)
			}
			p.buf.writeByte(']')
		}
    // ...
	case reflect.Chan, reflect.Func, reflect.UnsafePointer:
		p.fmtPointer(f, verb)
	default:
		p.unknownType(f)
	}
}

まとめ

  • [] が出力される理由を知るため、fmt.Printのソースコードを追った
  • fmt.Print は型情報から出力を出し分けている
  • arg == nilの下り、根拠を仕様から辿れるようになりたい(無理かも)

22日目は、@Daiji256さんの記事です。それでは。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
0