LoginSignup
0

Goの変数宣言には気をつけよう。〜ポインタはnilになる〜

Last updated at Posted at 2023-08-31

概要

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{}で受け取っているため、順番が逆でも受け取ることが可能になっている。

NG1
	input := "test"
	var output MyString
	deepCopy(output, &input)		//順序が逆
	log.Printf("output: %+v", output)
出力
output:        #outputからinputにコピーしているためエラーが出ている

2.コピー先はポインターを指定する必要がある

UnMarshalによって書き換えられた値を呼び出し元の関数で受け取るには、ポインタを渡して値を変更してもらう必要がある。(値渡しと参照渡し)

NG2
	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)
}

やらかしポイント

突然ですが、問題です。以下の二つのコードはどちらが動くでしょうか?

その1
func DeepCopy[T any](src interface{}) (*T, error) {
    var dst T

	buf, _ := json.Marshal(src)
    json.Unmarshal(buf, &dst)
    return &dst, nil
}
その2
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の参考にした記事

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
0