🖨️

Go : Copier で部分一致な構造体をマッピングする

に公開

最近、仕事で Go を書き始めました。
書くのが結構、久しぶりです。

実はエンジニア業界入るときの最初のポートフォリオを Go で書いたりしたこともありまして(4 ~ 5 年前くらい)。

当時は新鋭の新言語でシステム開発、組み込み開発、や大規模開発への適正や、高速な実行速度など良いことがいっぱい言われてました。
自分のキャリアのファーストチョイスとしたのもそれが理由です。

が、あらためて書いてるうちに、実は結構アレだな ... と思い始めてきた ...

構造体の型変換 Go のつらみ

Go の前は TypeScript の仕事が多かったので、オブジェクトの一部分だけ変更したコピーを作りたかったり、構造が部分的に一致しているものを変換するときに...スプレッド演算子をよく使ってました。

const postRequestBody = {
	firstName:  "太郎",
	lastName:   "山田",
	birthDate:  "1990-01-01",
	email:      "password123",
	password:   "xxx@example.com",
	phone:      "1234567890",
	address:    "東京都千代田区",
}


const insertSqlArgs = {
	id: crypto.randomUUID()
	...postRequestBody,
}

// ... みたいな?

が、Go にはそういうのがない
(標準機能には)

愚直にやるとこうなる

// import (
// 	"github.com/google/uuid"
// )

func main() {
	postRequestBody := PostRequestBody{
		FirstName: "太郎",
		LastName:  "山田",
		BirthDate: "1990-01-01",
		Password:  "password123",
		Email:     "xxx@example.com",
		Phone:     "1234567890",
		Address:   "東京都千代田区",
	}
	// 部分一致している構造体をコピーして、一部変更したもの
	insertSqlArgs := InsertSqlArgs{
		Id:        uuid.New().String(),
		FirstName: postRequestBody.FirstName,
		LastName:  postRequestBody.LastName,
		BirthDate: postRequestBody.BirthDate,
		Email:     postRequestBody.Email,
		Password:  postRequestBody.Password,
		Phone:     postRequestBody.Phone,
		Address:   postRequestBody.Address,
	}
}

// type PostRequestBody struct {
// 	FirstName string `json:"first_name"`
// 	LastName  string `json:"last_name"`
// 	BirthDate string `json:"birth_date"`
// 	Email     string `json:"email"`
// 	Password  string `json:"password"`
// 	Phone     string `json:"phone"`
// 	Address   string `json:"address"`
// }

// type InsertSqlArgs struct {
// 	Id        string
// 	FirstName string
// 	LastName  string
// 	BirthDate string
// 	Email     string
// 	Password  string
// 	Phone     string
// 	Address   string
// }

めちゃ大変です。
フィールド: {コピー元インスタンス}.{フィールド} をひたすら書かないといけないので記述量がすごい。

手書きなので、間違えてないかも心配
(最近は AI に書かせてる人もいると思うけど)

Copier を使おう

Copier を使おう

go mod init copier-demo
go get -u github.com/jinzhu/copier

touch main.go
import (
	"github.com/google/uuid"
	"github.com/jinzhu/copier"
)

func main() {
	postRequestBody := PostRequestBody{
		FirstName: "太郎",
		LastName:  "山田",
		BirthDate: "1990-01-01",
		Password:  "password123",
		Email:     "xxx@example.com",
		Phone:     "1234567890",
		Address:   "東京都千代田区",
	}
	insertSqlArgs := InsertSqlArgs{
		Id: uuid.New().String(),
	}
	// copier.Copy(&to, &from)
	_err := copier.Copy(&insertSqlArgs, &postRequestBody)
}

コピー先が左、コピー元が右です。
あと、ポインターを渡す必要があります。(書き込み操作が必要)
間違えないようにしましょう。

失敗する場合はある? (error/panic)

まず、デフォルト動作では panic は起きません(自分が知る限り)。
使い方を間違えない限り error も特に起きません。

まず使い方を間違えるケース。
ポインターでなく構造体の値を渡した場合は失敗します(書き込めないので)
copier.Copy() の引数は interface{} で TS の any 型のようなものなので、間違えてもコンパイルエラーが出ないので注意です。

幸い、 Go はテストを書くのがとても簡単なので、 copier を使うコードはテストしましょう!
*_test.go を書くだけ)

func Test(t *testing.T) {
	// ポインタはOK
	assert.Error(t, copier.Copy(&b, &a))
	// 値渡しするとコピーを書き込みできず Error!
	assert.Error(t, copier.Copy(b, a))
}

じゃあ、左右の型が違う場合は?
型が完全一致しない構造体同士は部分的にコピーします。
全く一致しなくてもエラーは発生しません。
アドレス渡しさえ忘れなければ、デフォルト動作は error や panic とは無縁で安全そうです。

type StructA struct {
	A int
	B int
}

type StructB struct {
	A int
	C int
	D int
}

type StructC struct {
	E int
}

func Test(t *testing.T) {
	a := StructA{A: 1, B: 2}
	var b StructB // ゼロ値初期化
	var c StructC

	assert.NoError(t, copier.Copy(&b, &a))
	fmt.Printf("%v\n", b) // Output: {1 0 0} 一致する部分だけコピー

	assert.NoError(t, copier.Copy(&c, &a))
	fmt.Printf("%v\n", c) // Output: {0} 全く一致しない型でも、エラーは返さない(全くコピーされないだけ)
}

構造体タグによる挙動変更 (例: copier:"must")

タグによって挙動を変えることができ、構造体コピーにある程度の厳密さをもたらします。ビジネスによっては、失敗時にエラーを報告したり、後続処理を中断することが信頼性になるはずです。

例えば、構造体フィールドのタグに

  • copier:"must"
    • タグを付けたフィールドがコピーできなかったら panic を起こす
  • copier:"must,nopanic"
    • コピーできなかったら error を戻り値で返す
      (個人的にはnopanicがデフォルトであってほしい ...)
type SafeSource struct {
	ID string
}

type SafeTarget struct {
	Code string `copier:"must,nopanic"` // Enforce copying without panic.
}

func Test(t *testing.T) {
	source := SafeSource{ID: "123"}
	var target SafeTarget

	err := copier.Copy(&source, &target)
	if err != nil {
		fmt.Println("コピー失敗しました")
		return
	}

	fmt.Println("コピー成功しました")
	// ...コピー成功した値でなにかする
}

詳しくはリポジトリー参照
https://github.com/jinzhu/copier

便利なパッケージを使ってトイルをなくそう

toil = 苦労、徒労、骨折り損

Go は他の言語なら標準装備なのに… みたいな便利機能がないこともよくあるので、サードパーティパッケージを導入して記述量を減らしていくのが肝心(だと思ってます)

GitHubで編集を提案

Discussion