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
がデフォルトであってほしい ...)
- コピーできなかったら error を戻り値で返す
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("コピー成功しました")
// ...コピー成功した値でなにかする
}
詳しくはリポジトリー参照
便利なパッケージを使ってトイルをなくそう
toil = 苦労、徒労、骨折り損
Go は他の言語なら標準装備なのに… みたいな便利機能がないこともよくあるので、サードパーティパッケージを導入して記述量を減らしていくのが肝心(だと思ってます)
Discussion