【Go】フィールド名を書かずに構造体を初期化するのはやめよう
はじめに
こんにちは、ken です。お仕事では Go を書いています。
突然ですが、みなさんはGoで構造体を初期化するときにフィールド名を指定していますか?
Go では構造体を初期化する際に、フィールド名を指定せずに値だけを渡すことができます。
これはunkeyed literalsと呼ばれているみたいです。
type Point struct {
X, Y float64
}
// unkeyed literalsによる初期化
p := Point{1, 2}
// フィールド名を指定した初期化
q := Point{X: 1, Y: 2}
フィールド名を指定しないこの書き方は一見便利そうに見えますが、その便利さの裏には落とし穴が潜んでいます。
今回はその問題点と、unkeyed literalsを使わないようにするテクニックを紹介します。
unkeyed literalsの問題点
unkeyed literalsは、構造体の変更に弱い という問題点があります。
例えば、次のようなコードを見てください。
package main
import (
"fmt"
)
type User struct {
Name string
Age int
AnnualIncome int
}
func main() {
u := User{"John", 20, 4000000}
fmt.Printf("My name is %s.\n", u.Name)
fmt.Printf("I am %d years old.\n", u.Age)
fmt.Printf("My annual salary is %d yen.", u.AnnualIncome)
/*
出力:
My name is John.
I am 20 years old.
My annual salary is 4000000 yen.
*/
}
自己紹介を出力する簡単なコードです。Johnさんは20歳で年収400万円の青年らしいですね。
一見問題なさそうですが、後から構造体のフィールドの順序が変更されるとどうなるでしょうか。
package main
import (
"fmt"
)
type User struct {
Name string
AnnualIncome int // Age と AnnualIncome の順序が変更された
Age int
}
func main() {
u := User{"John", 20, 4000000}
fmt.Printf("My name is %s.\n", u.Name)
fmt.Printf("I am %d years old.\n", u.Age)
fmt.Printf("My annual salary is %d yen.", u.AnnualIncome)
/*
出力:
My name is John.
I am 4000000 years old.
My annual salary is 20 yen.
*/
}
Johnさんが4000000歳で年収20円の青年になってしまいました...。
一方で、新しいフィールドが追加されたとき、逆に既存のフィールドが削除されたときにはエラーが発生します。
./prog.go:13:24: too many values in struct literal of type User
./prog.go:15:31: too few values in struct literal of type User
追加・削除の場合はエラーが発生するためすぐに気づけそうですが、フィールドの順序変更はエラーにならないため気づきにくいですね。
また構造体のフィールドの追加や削除の修正は注意して行う一方で、順序を変更する修正は比較的気軽におこなってしまいそうです。
このバグを未然に防ぐ方法はないのでしょうか?
_ struct{}
による解決策
実は未然に防ぐ方法があります!
それは構造体に_ struct{}
フィールドを追加するというテクニックです。
package main
import (
"fmt"
)
type User struct {
Name string
Age int
AnnualIncome int
_ struct{} // 追加
}
func main() {
u := User{"John", 20, 4000000} // コンパイルエラー: too few values in struct literal of type User
fmt.Printf("My name is %s.\n", u.Name)
fmt.Printf("I am %d years old.\n", u.Age)
fmt.Printf("My annual salary is %d yen.", u.AnnualIncome)
}
_ struct{}
を追加すると、unkeyed literalsを使った初期化がコンパイルエラーになります。構造体のフィールドの数と初期化時に渡す値の数が一致してないからですね。
もしunkeyed literalsを使おうとすると
u := User{"John", 20, 4000000, struct{}{}}
と書く必要がありますが、わざわざこうしてまでunkeyed literalsで書こうとする人はいないでしょう。
またこの_ struct{}
は非公開フィールドなので、外部パッケージからこの構造体を利用した場合は上記の書き方でも初期化できません。
さらに struct{}
は 0 バイトのため、メモリのオーバーヘッドもありません。
これにより意図しないバグの発生を防ぐことができます...!賢い解決策ですね。
さいごに
unkeyed literals は一見便利なものの、長期的な保守性を考えると避けたほうが無難ですね。
そして_ struct{}
を使って unkeyed literals の書き方を防ぐテクニックは、シンプルながら賢いやり方だなと思いました!
間違いなどありましたら、コメントにてご指摘ください。ここまで読んでいただき、ありがとうございました!
出典
このテクニックはgo-adviceというgoのテクニックがまとめられているリポジトリで知りました。他にも有用そうなテクが色々書かれているのでまた面白そうなのがあったら記事にしようかなと思います。
Discussion