💬

【Go】別パッケージで定義した構造体を埋め込んだ構造体のリテラルの書き方

2024/01/05に公開

TL;DR

foo.go
package foo

type BaseModel struct {
	ID string
}
main.go
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を書いていたとき、以下のような要件で実装を進めていました。

  1. 複数のエンドポイントで似たようなクエリパラメータを受け取って処理を行う
  2. 共通のクエリパラメータを表す構造体を独立したパッケージで定義して、その構造体を各ユースケースで用いる構造体に埋め込んで使う

コードを書いているうちに、「共通構造体を埋め込んだ構造体の初期化のやり方がわからん!」となってしまったので備忘録として残します。(軽く調べてもそれっぽい記事が出てこなかった)

おさらい: 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構造体を定義したとします。

foo.go
package foo

type BaseModel struct {
	ID string
}

このBaseModel構造体を埋め込んで、新しくModel構造体を作ったとします。

main.go
package main

type Model struct {
	foo.BaseModel
	Name string
}

このModel構造体をリテラルを使って初期化したい場合、以下のように書く必要があります。

main.go
	m := Model{
		Name: "fjnkt98",
		BaseModel: foo.BaseModel{
			ID: "001",
		},
	}

フィールド名にはfoo.BaseModelではなくBaseModelを指定する必要があります。(私はfoo.BaseModelって書かなきゃいけないんじゃないのと思って小一時間悩みました)
「構造体を埋め込んだら、フィールド名が『埋め込んだ構造体の名前』、型が『埋め込んだ構造体』であるフィールドが暗黙的に定義される」という考え方を持っていれば迷わずに済みそうです。

埋め込まれた構造体がジェネリクスを持っていた場合も同様です。

foo.go
package foo

type Vertex[T comparable] struct {
	X T
	Y T
}
main.go
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という名前の構造体が定義されていて、それら両方を埋め込んだ構造体を新たに定義したいとします。

foo.go
package foo

type BaseModel struct {
	ID string
}
bar.go
package bar

type BaseModel struct {
	CreatedAt int64
	UpdatedAt int64
}

このとき、そのまま埋め込むとコンパイルエラーになります。

main.go
type Model struct {
	foo.BaseModel
	bar.BaseModel
}
//./main.go:18:6: BaseModel redeclared
//        ./main.go:17:6: other declaration of BaseModel

これを回避するには、型エイリアスを定義します。

main.go
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ならありそう?)

参考文献

GitHubで編集を提案

Discussion