My External Storage

Aug 17, 2019 - 4 minute read - Comments - Go

[Go] 構造体オブジェクト初期化時にフィールド名を指定することを強制する #golangjp

静的解析に頼らず、コンパイル時に構造体オブジェクトの初期化でフィールド名の指定を強制するためのTips。

TL;DR

  • 構造体初期化時、フィールド名を省略することができる
    • p := &mypkg.Person{"alice", 12}
  • GoのComposite literals
  • 構造体に非公開フィールドを含めることでフィールド名を明示的に指定しないとComposite literalsが使えなくなる
  • 非公開フィールドはstruct{}で宣言すればメモリサイズも増えない
    • reflect.TypeOf(struct{}).Size() // 0

Goにおける構造体の初期化方法

Goでは構造体を初期化するときにnew(mapなどの場合はmake)を使う他にComposite literalsを使うことができる。
このとき、フィールド名を指定せずに値を指定していた場合は、構造体のフィールド定義順に初期化されていく。

type Person struct {
    Name string
    Age  int
}

func foo(){
    p1 := Person{"alice", 10}
    p2 := Person{Name:"bob", Age:20}
}

構造体に非公開フィールドを含めることでフィールド名を明示的に指定しないとComposite literalsが使えなくなる

Struct literalsに関する仕様は2019/08/17時点では以下のように書かれている。

  • A key must be a field name declared in the struct type.
  • An element list that does not contain any keys must list an element for each struct field in the order in which the fields are declared.
  • If any element has a key, every element must have a key.
  • An element list that contains keys does not need to have an element for each struct field. Omitted fields get the zero value for that field.
  • A literal may omit the element list; such a literal evaluates to the zero value for its type.
  • It is an error to specify an element for a non-exported field of a struct belonging to a different

ここで、Composite literalsを使うときに以下の仕様を利用することで、Composite literalsによる初期化を行なうとき、フィールド名の指定を強制することができる。

  • フィールド名を指定しない場合、すべてのフィールドの初期値を指定しないといけない
  • パッケージ外で非公開フィールドの値は指定できない

https://play.golang.org/p/WAynji8JGkt

-- go.mod --
module sample

go 1.12
-- sub/sub.go --
package sub

type MustKey struct {
	Name   string
	Second string
	_hoge  struct{}
}
-- main.go --
package main

import (
	"fmt"
	"sample/sub"
)

func main() {
	// mk := sub.MustKey{"hoge", "bar"} // ./main.go:9:28: too few values in sub.MustKey literal
	// mk := sub.MustKey{"hoge", "bar", struct{}{}} // ./main.go:10:43: implicit assignment of unexported field '_hoge' in sub.MustKey literal
	mk := sub.MustKey{}
	fmt.Printf("%v\n", mk)
}

フィールド名を省略していると、非公開フィールド以外を埋めるだけでは値が少ないとエラーになり、 非公開フィールドの初期化も行おうとすると暗黙的に非公開フィールドを初期化しようとしてエラーになる。

非公開フィールドはsturct{}を使えばメモリサイズも増えない

このような実装は公式パッケージ内でも行われている。

type NamedArg struct {
	_Named_Fields_Required struct{}

	// Name is the name of the parameter placeholder.
	//
	// If empty, the ordinal position in the argument list will be
	// used.
	//
	// Name must omit any symbol prefix.
	Name string

	// Value is the value of the parameter.
	// It may be assigned the same value types as the query
	// arguments.
	Value interface{}
}

チャネル利用時でも使われるstruct{}型(空構造体)はサイズが0なので、パフォーマンス的にも悪影響はない。 同パッケージ内では、次のサンプルコードのようにフィールド名を指定せずに初期化できるが、さすがにstruct{}{}とまで書いて初期化するならば気づくだろう…

package main

import (
	"fmt"
	"reflect"
)

type MustKey struct {
	Name   string
	Second string
	_hoge  struct{}
}

func main() {
	mk := MustKey{"hoge", "bar", struct{}{}}
	fmt.Printf("%d\n", reflect.TypeOf(mk).Size()) // 20
	fmt.Printf("%d\n", reflect.TypeOf(mk._hoge).Size())  // 0
}

終わりに

可読性向上・実装ミス軽減につながるので構造体初期化時は必ずフィールド名を指定するようにしている。 が、永続化データと対応する構造体に非公開フィールドを用意することはないので、コンパイルエラーが出るからしていたわけではなかった。

http.Requestなど、)公式パッケージの構造体を使うときに無意識に使っていたのだろうが、改めて勉強になった。 今回は@podhmoさんのtweetで見かけたのでちゃんと仕様まで読んでみた。

関連記事