【Go】別パッケージで定義した構造体を埋め込んだ構造体のリテラルの書き方
TL;DR
package foo
type BaseModel struct {
ID string
}
package main
type Model struct {
foo.BaseModel
Name string
}
func main() {
m := Model{
Name: "fjnkt98",
BaseModel: foo.BaseModel{ // NOT `foo.BaseModel: ...`
ID: "001",
},
}
}
概要
GoでWeb APIを書いていたとき、以下のような要件で実装を進めていました。
- 複数のエンドポイントで似たようなクエリパラメータを受け取って処理を行う
- 共通のクエリパラメータを表す構造体を独立したパッケージで定義して、その構造体を各ユースケースで用いる構造体に埋め込んで使う
コードを書いているうちに、「共通構造体を埋め込んだ構造体の初期化のやり方がわからん!」となってしまったので備忘録として残します。(軽く調べてもそれっぽい記事が出てこなかった)
おさらい: Goの構造体
構造体リテラル
構造体リテラルを使うことで新しい構造体のインスタンスを作成することができます。
構造体が持つフィールドの値を列挙することで初期値を割り当てることができます。
type Vertex struct {
X int
Y int
}
m := Vertex{1, 2} // X = 1, Y = 2
<フィールド名>: <値>
構文を使うことでフィールド名を指定して順不同で値を初期化することもできます。
m := Vertex{X: 1, Y: 2} // X = 1, Y = 2
Embedding
構造体に別の構造体を埋め込む(embedding)ことができます。埋め込まれた構造体のフィールドやメソッドを、あたかも埋め込んだ構造体のフィールドやメソッドであるかのように呼び出すことができるようになります。
type Vertex struct {
X int
Y int
}
type Vertex3D struct {
Vertex // `Vertex`構造体を埋め込む
Z int
}
func main() {
var v Vertex3D
v.X // v.Vertex.Xのシンタックスシュガー
}
Embedded structの初期化
別の構造体を埋め込んだ構造体のリテラルは以下のように書きます。
func main() {
v := Vertex3D{
Vertex{1, 2},
3
}
}
<フィールド名>: <値>
構文を使う際は、構造体名をフィールド名として使います。
func main() {
v := Vertex3D{
Vertex: Vertex{
X: 1,
Y: 2,
},
Z: 3,
}
}
「埋め込んだ構造体名がフィールド名であるフィールド」があると考えるとよいです。
上記2つの例で作成したv
はどちらも等価です。
別パッケージで定義した構造体を埋め込んだ構造体のリテラル
本題です。別パッケージで定義した構造体を埋め込んだ構造体を考えます。
foo
というパッケージでBaseModel
構造体を定義したとします。
package foo
type BaseModel struct {
ID string
}
このBaseModel
構造体を埋め込んで、新しくModel
構造体を作ったとします。
package main
type Model struct {
foo.BaseModel
Name string
}
このModel
構造体をリテラルを使って初期化したい場合、以下のように書く必要があります。
m := Model{
Name: "fjnkt98",
BaseModel: foo.BaseModel{
ID: "001",
},
}
フィールド名にはfoo.BaseModel
ではなくBaseModel
を指定する必要があります。(私はfoo.BaseModel
って書かなきゃいけないんじゃないのと思って小一時間悩みました)
「構造体を埋め込んだら、フィールド名が『埋め込んだ構造体の名前』、型が『埋め込んだ構造体』であるフィールドが暗黙的に定義される」という考え方を持っていれば迷わずに済みそうです。
埋め込まれた構造体がジェネリクスを持っていた場合も同様です。
package foo
type Vertex[T comparable] struct {
X T
Y T
}
type Vertex3D struct {
foo.Vertex[int]
Z int
}
func main() {
v := Vertex3D{
Vertex: foo.Vertex[int]{ // `Vertex: foo.Vertex[int]{...}`と書く
X: 1,
Y: 2,
},
Z: 3,
}
fmt.Printf("%+v\n", v) // {Vertex:{X:1 Y:2} Z:3}
}
まとめ
- Embeddingした構造体をリテラルで初期化する場合は
Vertex: foo.Vertex{...}
のように書く必要がある - 構造体を埋め込むとフィールド名が『埋め込んだ構造体の名前』、型が『埋め込んだ構造体』であるフィールドが暗黙的に定義されると考えるとよい
別々のパッケージで定義された同名の2つの構造体を埋め込む場合
foo
パッケージとbar
パッケージでそれぞれBaseModel
という名前の構造体が定義されていて、それら両方を埋め込んだ構造体を新たに定義したいとします。
package foo
type BaseModel struct {
ID string
}
package bar
type BaseModel struct {
CreatedAt int64
UpdatedAt int64
}
このとき、そのまま埋め込むとコンパイルエラーになります。
type Model struct {
foo.BaseModel
bar.BaseModel
}
//./main.go:18:6: BaseModel redeclared
// ./main.go:17:6: other declaration of BaseModel
これを回避するには、型エイリアスを定義します。
type FooBaseModel = foo.BaseModel
type BarBaseModel = bar.BaseModel
type Model struct {
FooBaseModel
BarBaseModel
}
func main() {
m := Model{
FooBaseModel: foo.BaseModel{ID: "001"},
BarBaseModel: bar.BaseModel{CreatedAt: 1704425779, UpdatedAt: 1704425779},
}
fmt.Printf("%+v\n", m)
}
構造体でこういうシチュエーションが起こることはあまり無いとは思いますが...(interfaceならありそう?)
参考文献
- A Tour of Go Struct Literals
- stack overflow Embed two structs with the same name in a struct
Discussion