32
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Goの構造体リテラルでフィールド名の指定を強制する

Posted at

はじめに

Goにおける型によってSQLインジェクションを防ぐ方法という記事でsafesqlというライブラリで行っているGoの型をうまくつかってSQLインジェクションを防ぐ方法を解説しました。

この他にも型を使って何かを制限する方法ということで、本記事では構造体リテラルでフィールド指定を強制する方法について解説します。

構造体リテラル

構造体リテラルは、次のように構造体の値をリテラルで記述したものです。

T1{N: 100, S: "hoge"}

なお、型T1は次のように定義されているとします。

type T1 struct {
    N int
    S string
}

また、型T1の構造体リテラルは、次のようにフィールド名を省略して記述できます。

T1{100, "hoge"}

フィールド名の省略

フィールド指定を省略した場合、うっかり間違った書き方をしてしまうと、気付きにくいバグにつながってしまいます。
たとえば、次のような型T2を考えます。2つのフィールド、NMを持ち、どちらもint型です。

type T2 struct {
    N int
    M int
}

フィールド名を指定して構造体リテラルを書くと次のようになります。

T2{N: 100, M: 200}

一方、次のようにフィールド名を省略して記述できます。

T2{100, 200}

これを次のようにうっかり逆に書いてしまうと、気づきにくいバグに繋がります。

T2{200, 100}

また、次のように型宣言のフィールドの順番が変わってしまっても特にコンパイルエラーになる訳ではありませんが、フィールドに設定される値は逆になります。

type T2 struct {
    M int
    N int
}

このように、無用なバグを避けるには、フィールド名を指定して構造体リテラルを記述すると良いでしょう。

フィールド名の省略を防ぐ

golang.org/x/text/encodingパッケージで定義されている、Encoder型とDecoder型は次のように定義されています。

type Encoder struct {
	transform.Transformer
	_ struct{}
}

type Decoder struct {
	transform.Transformer
	_ struct{}
}

どちらの型もtransform.Transformer型を埋め込んでいます。Goでは埋め込みは継承ではなく、匿名フィールドです。匿名フィールドなので、フィールド名の代わりに、型名でフィールドにアクセスします。

埋め込まれた型は構造体リテラルで型名を指定して値をセットできます。

encoding.Encoder{Transformer: t}

フィールド名(この場合は型名)を省略して次のように書けそうですが、Encoder型とDecoder型の場合はできません。

encoding.Encoder{t}

Encoder型とDecoder型は_フィールドがあります。フィールド名を省略した場合、すべてフィールドに設定する値をフィールドが宣言された順に並べなくてはいけません。そのため、次の書き方をするしかありません。

encoding.Encoder{t, struct{}{}}

しかし、あまりにも煩わしくフィールド名を指定した方が楽です。このように、Encoder型とDecoder型では、_フィールドを設けることでフィールド名の省略を防いでいます。

ブランク識別子

Goには、ブランク識別子という特別な識別子が存在します。ブランク識別子は、宣言や代入には使用できますが、識別子を用いて参照を行えません。

たとえば、名前が_である変数、ブランク変数を考えます。ブランク変数は、どんな型の値も代入でき、かつどんな型の値の変数としても宣言できます。しかし、代入文の右辺や他の式などでは使用できません。

たとえば、ブランク変数への代入は次のように行えます。第2戻り値のエラーを無視するために用いています。

n, _ := strconv.Atoi("100")

このように、ブランク変数を用いると、使用する予定のない変数を省略できます。ブランク識別子は変数だけではなく、レシーバ、引数、名前付き戻り値、インポート宣言などで利用できます。

構造体フィールドの名前もブランク識別子にできます。フィールド名がブランク識別子の場合、次のようにフィールド名を指定してフィールドにアクセスできません。

// ブランク識別子のフィールドを指定して構造体リテラルは記述できない
v := encoding.Encoder{_:struct{}{}}

// フィールド名を指定してアクセスできない
fmt.Println(v._)

struct{}型

struct{}型はempty struct型と呼び、フィールドを持たない構造体を型リテラルで記述した型です。型リテラルとは、型名ではなく型の定義をそのまま書き下したような型の記述方法で、[]int型やmap[string]int型なども型リテラル表記です。型リテラルは、よく構造体、マップ、配列、スライスなどのコンポジット型を記述するために用います。

empty struct型や長さ0の配列、およびそれらだけを要素に持つ構造体や配列のサイズは0です。つまり、次のようにunsafe.Sizeofで型のサイズを測定すると0となります。

fmt.Println(unsafe.Sizeof(struct{}{}))

The Go Playgroundで動かす

サイズが0の型をフィールドの型とした場合、そのフィールドは型情報としては存在しますが、値としてメモリ上に存在するわけではありません。そのため、empty struct型は容量のくわない便利な型として用いられます。

つまり、empty struct型で名前がブランク識別子のフィールドを設けた場合、フィールド名を省略して構造体リテラルを記述することが難しくなります。もちろん、struct{}{}を構造体リテラルに記述すれば、フィールド名を省略することは可能です。しかし、面倒なので現実的ではありません。なお、struct{}{}はempty struct型の構造体リテラルです。

empty struct型以外の型をブランク識別子のフィールドにした場合、その構造体型のサイズにそのフィールドの型のサイズは含まれます。つまり、アクセスできない領域であっても、その分メモリは確保されてしまいます。

empty struct型以外の型をブランク識別子のフィールドするメリットはなさそうですが、encoding/binaryパッケージを使って、構造体の値をバイト列にしたい場合などは、余白をあけるために_ [4]byteのようにフィールドを設ける場合があります。

おわりに

本記事では、golang.org/x/text/encodingパッケージで用いられている構造体リテラルでフィールド名の指定を強制する方法について解説しました。型の作りによって何かを予防するというのは、実用性の議論を置いておいても面白いものです。

他に何か面白い例があったら、また記事にします。

32
17
0

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
  3. You can use dark theme
What you can do with signing up
32
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?