業務で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
}
大抵のインターネットのコードでは、Challenge
の Histories
のようにポインタ渡しになっている。どういうときに値渡しとポインタ渡しを使うのだろう? また、コードを書く時の注意点はなんだろうか?
構造体のポインタと値渡しの違いと使い分け
ポインタと値渡しの違いと使い分けは明確で、ポインタだとアドレスが渡る。値渡しだと、データがコピーされて渡る。だから、関数の引数として、ポインタ渡しにすると、関数の方で変更されたら、変更が反映される。値渡しだとコピーが渡されるので、関数で値を変更しても、元のアドレスにある構造体の値に変化がない。
構造体が大抵はポインタ渡しになるのは、構造体なのでサイズが大きいのでコピーのコストが馬鹿にならないから。ポインタなら、参照が渡るだけなので、コストが大したことないと言える。だから、たいしてコストのかからない 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 = ¤tChallenges
結論
構造体の入れ子の場合、統一性、コピーの手間を考えると、ポインタ渡しを基本にしたほうがよさげだ。ただ、nilチェックは必要になるので、忘れないようにしよう。
これで、構造体で配列で入れ子などのややこしい構造であっても、ポインタと値渡しを使い分ける方法が理解できた。