CYDAS Developer's Blog

サイダス技術者ブログ

【Go言語】 構造体、スライス、マップ、ポインターの組み合わせで割とつまずく

こんにちは。もうすぐ終了するGoogle+に未だに投稿するよく訓練されたGoogle信者の長谷川です。

今回はGo言語の話。Go言語は最近では珍しいポインターがある言語です。
ポインターというとC言語初心者がつまずく鬼門とされて忌み嫌われてる感がありますが、メモリの使用量を抑えて高速化するにはやっぱりポインターがあると有効です。
そうはいってもちょくちょくポインター関連でやらかします。特にスライス、マップ、構造体が絡むとよく失敗するんでここで書き留めておきます。

配列は実体、スライスはポインター

Goでは配列とスライスがありますが扱いが異なります。
配列は値がコピーされますが、スライスはポインターとして扱われます。

package main
import "fmt"

func main(){
    ar := [3]string{}
    sl := make([]string, 3)
    mp := make(map[int]string, 3)

    setFunc(ar, sl, mp)
    fmt.Println(ar) // [  ]
    fmt.Println(sl) // [star  ]
    fmt.Println(mp) // map[0:sun]
}

func setFunc(ar [3]string, sl []string, mp map[int]string) {
    ar[0] = "moon"
    sl[0] = "star"
    mp[0] = "sun"
}

ついでにmapも比較してます。
スライス、マップは変更内容が反映されてますが、配列は変更内容が失われます。

スライスはポインターだけではない

しかし、スライスはポインターとしてアドレスを持っていますが長さとキャパシティーの要素も持っており、これらは変更が反映されません。

// 省略
func main() {
    sl := make([]string, 3)
    sl[0] = "uno"
    fmt.Println(len(sl), cap(sl), sl) // 3 3 [uno  ]

    setFunc(sl)
    fmt.Println(len(sl), cap(sl), sl) // 3 3 [uno due tre]
}

func setFunc(sl []string) {
    sl[1] = "due"
    sl = append(sl, "quatro") // appendは反映されない
    sl[2] = "tre"

    fmt.Println(len(sl), cap(sl), sl) // 4 6 [uno due tre quatro]
}

関数内でappendしてスライスを拡張してますがその内容は失われています。

for文で取得した要素はコピー

あと構造体のスライス操作しようとしてやってしまうんですが、for文で取得した値はコピーなんで反映しません。 ポインター関係ないですね。

// 省略
type Person struct {
    name string
    age  int
}

func main() {
    persons := make([]Person, 3)
    setFunc(persons)
    fmt.Println(persons) // [{ 0} { 0} { 0}]
}

func setFunc(persons []Person) {
    for _, p := range persons {
        p.name = "fool"
        p.age = 1
    }
  // そもそもスライスには反映しない
    fmt.Println(persons) // [{ 0} { 0} { 0}]
}

インデックス指定して操作すればいいんですけどね。

for i := range persons {
  persons[i].name = "fool"
  persons[i].age = 1
}

mapではキー指定で操作できない

しかし、これもmapだとできません。

// 省略
func main() {
    persons := map[int]Person{
        1: Person{},
        2: Person{},
        3: Person{},
    }
    setFunc(persons)
    fmt.Println(persons)
}

func setFunc(persons map[int]Person) {
    for key := range persons {
        // ここがエラーになる
        persons[key].name = "fool" // cannot assign to struct field persons[key].name in map
        persons[key].age = 1 // cannot assign to struct field persons[key].age in map
    }
}

他の言語の連想配列みたいにkeyで指定してメンバーにアクセスしようとするとエラーになってコンパイルが通りません。
下のように一旦コピーした構造体にセットしてmapに戻すという方法もありますが、

for key, p := range persons {
  p.name = "fool"
  p.age = 1
  persons[key] = p
}

操作するならそもそもポインターのmapのほうが間違えないし処理的にも負荷が少なくなります。

func main() {
    persons := map[int]*Person{
        1: &Person{},
        2: &Person{},
        3: &Person{},
    }
    setFunc(persons)
    fmt.Println(persons[1]) // &{fool 1}
    fmt.Println(persons[2]) // &{fool 2}
    fmt.Println(persons[3]) // &{fool 3}
}

func setFunc(persons map[int]*Person) {
    for k, p := range persons {
        p.name = "fool"
        p.age = k
    }
}

それならスライスのほうもポインターのスライスにしたほうがいいんじゃないかという気がします。
大きい構造体ならいちいちコピーしないで操作できたほうがいいですよね。

それではよいGoライフを!