ポインタ型での処理の備忘録.Go初心者,ポインタと不仲なら多分あるあるだと思います.
関数とポインタ
まず,構造体は値型です.ですので関数の引数に構造体を渡して加工してあげても元の構造体が変化することはありません.
package main
import (
"fmt"
)
type point struct {
x, y float64
}
func swap(p point) {
x, y := p.x, p.y
p.x = y
p.y = x
}
func main() {
p := point{x: 0, y: 1}
swap(p)
fmt.Printf("%+v\n", p)// {x:0 y:1}
}
これがいわゆる値渡しです.関数の引数の構造体を加工してあげるにはポインタで渡してあげます.
package main
import (
"fmt"
)
type point struct {
x, y float64
}
func swap(p *point) { // 引数をポインタへ
x, y := p.x, p.y
p.x = y
p.y = x
}
func main() {
p := point{x: 0, y: 1}
swap(&p) // 渡す
fmt.Printf("%+v\n", p) // {x:1 y:0}
}
うまく値が入れ替わりました.よかったです.これがいわゆる参照渡しというやつですね.
レシーバとポインタ
Goには構造体に対するメソッドという機能があります(このとき,この構造体をレシーバといいます).一般にメソッドはポインタ型で定義されています.この理由をサンプルを通して確認したいと思います.
まずは値型でメソッドを定義して振る舞いをみます.
package main
import (
"fmt"
)
type point struct {
x, y float64
}
// メソッドを値型で定義
func (p point) init(x, y float64) {
p.x = x
p.y = y
}
func main() {
p1 := point{} // point型
p1.init(5, 10)
fmt.Printf("%+v\n", p1) // {x:0 y:0}
p2 := point{} // *point型
p2.init(5, 10)
fmt.Printf("%+v\n", p2) // {x:0 y:0}
}
point型, *point型にしてもうまく動作しませんでした.これは先ほどの値渡し同様に,メソッドの呼び出し時にはレシーバのコピーが生成され,そのコピーに対して処理が行われているためです.
例によってレシーバをポインタ型で定義してみます.
package main
import (
"fmt"
)
type point struct {
x, y float64
}
// メソッドをポインタ型で定義
func (p *point) init(x, y float64) {
p.x = x
p.y = y
}
func main() {
p1 := point{} // point型
p1.init(5, 10)
fmt.Printf("%+v\n", p1) // {x:5 y:10}
p2 := point{} // *point型
p2.init(5, 10)
fmt.Printf("%+v\n", p2) // {x:5 y:10}
}
想定通りの挙動になりました.point型,*point型ともにメソッドを呼び出せしてちゃんと実行できています.また,メソッドならば,ユーザは対象の構造体を値型かポインタ型どちらにするか気にする必要がない恩恵があることもわかります.(このためにポインタの理解がガバりがちです)
以上のことから構造体に対する操作(関数,メソッド)はポインタ型に対して行うべきだとわかりました.
おまけ:rangeとポインタ
DBへのパッチ処理など,データをGET -> 加工 -> PUTする処理をしたい場面はたまにあると思います.そんな時にハマりかけたの経験がこの記事のきっかけでした.最後にサンプルを置いておきます.
package main
import (
"fmt"
)
type Person struct {
Name string
Age int
}
type userInfo struct {
Name *string
Age *int
}
func main() {
persons := []Person{
{
Name: "Bob",
Age: 20,
},
{
Name: "Alice",
Age: 10,
},
}
data := make([]userInfo, 0)
for _, p := range persons {
input := userInfo{
Name: &p.Name,
Age: &p.Age,
}
data = append(data, input)
}
for i := 0; i < len(data); i++ {
fmt.Printf("data[%d]...Name:%s, Age:%d\n", i, *data[i].Name, *data[i].Age)
}
}
/*
data[0]...Name:Alice, Age:10
data[1]...Name:Alice, Age:10
*/
dataに格納された内容がおかしいようです.rangeは同じポインタがチャネルに入れられており、ループが進むにつれて,参照している値が書き換えられているだけだからです.
そのため,以下のようにインデックスを指定して参照させると思った通りの挙動になります.
https://play.golang.org/p/aO8q4keS5o6
for i, _ := range persons {
input := userInfo{
Name: &persons[i].Name,
Age: &persons[i].Age,
}
data = append(data, input)
}