概要
DeepCopyを実装している際にjson.Marshal->UnMarshalでnil pointer(にるぽ?...w)と悩んでいた話
環境
tool | Ver |
---|---|
golang | 1.20 |
既存コード
引数にコピー先
とコピー元
を指定する形の関数で問題なく動作していた。(今回はエラー処理を省略してます)
type MyString string
func main() {
input := "test"
var output MyString
deepCopy(input, &output)
log.Printf("output: %+v", output)
}
func deepCopy(src, dst interface{}) error {
b, err := json.Marshal(src)
if err != nil {
return err
}
err = json.Unmarshal(b, &dst)
if err != nil {
return err
}
return nil
}
> output: test
懸念点
上記のコードでも問題なく動いていたのだが、懸念点が二つ存在する。
1.コピー元とコピー先の順番を入れ替えると正しく動作しない
引数がどちらもinterface{}
で受け取っているため、順番が逆でも受け取ることが可能になっている。
input := "test"
var output MyString
deepCopy(output, &input) //順序が逆
log.Printf("output: %+v", output)
output: #outputからinputにコピーしているためエラーが出ている
2.コピー先はポインターを指定する必要がある
UnMarshalによって書き換えられた値を呼び出し元の関数で受け取るには、ポインタを渡して値を変更してもらう必要がある。(値渡しと参照渡し)
input := "test"
var output MyString
deepCopy(input, output) //値渡しになっている
log.Printf("output: %+v", output)
output: #outputの値は変更されていない
改善案
このようなこと事象は実装者本人が気をつけることで回避することができますが、エンジニアたるもの言語仕様として回避できる術があるのでリファクタリングしようと思い立ちました。
本題
ここからが本題です。
解決策として、1.18から追加されたジェネリクスを使用して、コピーしたい型を関数に渡し、UnMarshalされた値をマッピングしようと考えました。こうすることで呼び出す側は引数、コピーしたい型も明確になるため非常に見通しが良くなります。
func deepCopyGeneric[T any](src interface{}) (*T, error) {
}
func main() {
input := "test"
output, _ := deepCopyGeneric[MyString](input)
log.Printf("output: %+v", output)
}
やらかしポイント
突然ですが、問題です。以下の二つのコードはどちらが動くでしょうか?
func DeepCopy[T any](src interface{}) (*T, error) {
var dst T
buf, _ := json.Marshal(src)
json.Unmarshal(buf, &dst)
return &dst, nil
}
func DeepCopy[T any](src interface{}) (*T, error) {
var dst *T
buf, _ := json.Marshal(src)
json.Unmarshal(buf, dst)
return dst, nil
}
...
..
.
正解はその1が正しく動作します。わかる方はそうだよね。となっていると思いますが、完全にハマってました。
エラー内容としてはjson: Unmarshal(nil *MyString)
と出力されます。
関数内でポインタを作成しているのにも関わらずなぜかUnmarshalでこける。呼び出す側の関数は引数を正しく設定しており、関数でもポインタを返却している。エラーになる要素がないはず、、、なぜだと10分ほど悩んでいました。
原因
これです。
var dst *T
この時の処理内容としては、ポインタを生成していますがこの時点ではnil
になります。
盲点でした。
Goでは構造体のポインタを返却するときに&
を使用することで返却できます。(正確には生成した構造体のアドレスを返却してますが)
return &MyStruct{}
この考えが頭に染み付いていたので、変数宣言時にポインタで宣言したのにも関わらず、実態が生成されていないことに頭が回ってませんでした。
後書き
まだまだGopherの道は遠そうです。。
今回の検証コード
DeepCopyの参考にした記事