LoginSignup
23
16

More than 5 years have passed since last update.

Go の入れ子の配列の構造体をポインタにするか、値渡しにするか?

Posted at

業務でGoプログラミングをする必要があった。過去にTerraform に貢献をしたこともあったが、ちゃんと理解しているかはとても怪しいものだ。この際しっかりと理解して生産性を高めたい。

業務では、複数の構造体が入れ子構造になったもので、頻繁に出てくるタイプのものだ。

type Team struct {
    Id         string
    Challenges *[]Challenge
}
type Challenge struct {
    Id        string
    Histories *[]History
    Logs      []Log
}
type History struct {
    Id string
}
type Log struct {
    Message string
}

大抵のインターネットのコードでは、ChallengeHistories のようにポインタ渡しになっている。どういうときに値渡しとポインタ渡しを使うのだろう? また、コードを書く時の注意点はなんだろうか?

構造体のポインタと値渡しの違いと使い分け

ポインタと値渡しの違いと使い分けは明確で、ポインタだとアドレスが渡る。値渡しだと、データがコピーされて渡る。だから、関数の引数として、ポインタ渡しにすると、関数の方で変更されたら、変更が反映される。値渡しだとコピーが渡されるので、関数で値を変更しても、元のアドレスにある構造体の値に変化がない。

構造体が大抵はポインタ渡しになるのは、構造体なのでサイズが大きいのでコピーのコストが馬鹿にならないから。ポインタなら、参照が渡るだけなので、コストが大したことないと言える。だから、たいしてコストのかからない string や int は大抵は、ポインタではなく、参照渡しになっている。string でサイズがもし馬鹿でかかったらポインタでもよいのだろう。

構造体の初期化と値の変更

構造体の初期化はこんな感じで書ける。

    challenge := &Challenge{
        Id: "1",
    }
    challenges := []Challenge{
        *challenge,
    }
    team := &Team{
        Id:         "1",
        Challenges: &challenges,
    }

ちなみに、Challenge の部分を変更したい場合、例えば新しい要素を追加したい場合は、append 関数が使える。

    newChallenge := append(*team.Challenges,
        Challenge{
            Id: "2",
        })
    team.Challenges = &newChallenge

ちなみにChallenge の中にもさらに入れ子でHistory があるのでこれも更新する必要があるが基本的に同じ。
range で回して、特定の History を更新したい場合のロジック。一つ重要なポイントは、*[]History つまり、ポインタの属性は、初期値がnil になる。通常の型は、初期化の場合それぞれの型の ZERO の値がセットされる。例えば int なら 0 とかになる。ポインタの場合はそれがnilなので、例えば、append を使うときに引数がnilだったらパニックになるので、nil チェックをしてあげる必要が生じる。参考までに、Log の方は、値渡しの[]Log の型にしてみたので、nilチェックは不要だ。ただし、構造体が大きいと、コピーの負荷が大きくなる。

    currentChallenges := *team.Challenges
    for i, v := range currentChallenges {
        if v.Id == "2" {
            var newHistories []History
            if v.Histories != nil {
                newHistories = append(*v.Histories,
                    History{
                        Id: "1",
                    })
            } else {
                newHistories = []History{
                    History{
                        Id: "1",
                    },
                }
            }
            // newHistories = append(*v.Histories, History{Id: "1"})  // This code cause null pointer panic
            newLogs := append(v.Logs, Log{
                Message: "New Message",
            })

            currentChallenges[i].Histories = &newHistories
            currentChallenges[i].Logs = newLogs
        }
    }
    team.Challenges = &currentChallenges

結論

構造体の入れ子の場合、統一性、コピーの手間を考えると、ポインタ渡しを基本にしたほうがよさげだ。ただ、nilチェックは必要になるので、忘れないようにしよう。

これで、構造体で配列で入れ子などのややこしい構造であっても、ポインタと値渡しを使い分ける方法が理解できた。

参照

23
16
1

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
  3. You can use dark theme
What you can do with signing up
23
16