2019/05/16

Recent entries from same category

  1. Go で大文字小文字無視の文字列比較ベンチマーク
  2. Go で型がインタフェースを実装している事を保証するには
  3. 改訂2版 みんなのGo言語
  4. TensorFlow Lite の Go binding を書いた。
  5. Lint ツールを Language Server に対応させるコマンド efm-langserver 作った。

Go は最近のプログラミング言語にしては珍しくポインタを扱えるプログラミング言語。とはいってもC言語よりも簡単で、オブジェクトの初期化やメソッドの定義以外の場所ではおおよそポインタを使っている様には見えない。メソッドやフィールドへのアクセスも . で出来るし Duck Type によりインタフェースを満たしていれば実体であろうとポインタであろうとそれほど意識する必要はない。ところがこの便利さに乗っかってしまうと思わぬ所で足をすくわれてしまう。

package main

type foo struct {
    v int
}

func (f foo) add(v int) {
    f.v = v
}

func main() {
    var a foo

    a.add(3)

    println(a.v)
}

このコードは 0 が表示される。メソッドを呼び出す際にはレシーバのオブジェクトを得る必要があるが、foo は実体なのでコピーが生成される。例えばレシーバ f が引数であったと考えると理解しやすくなる。

package main

type foo struct {
    v int
}

func add(f foo, v int) {
    f.v += v
}

func main() {
    var a foo

    add(a, 3)

    println(a.v)
}

メソッドとレシーバの関係は、実は「単なる関数と第一引数」と考えると分かりやすい。特にC言語でオブジェクト指向をやる様な人達は訓練されているので、Go をやる上でもこの辺りの動作を意識せず扱えているのかもしれない。

実体のレシーバにメソッドを生やすメリットが無い訳ではない。例えばオブジェクトの値を明示的に変更させたくない場合がそれで、そういった設計にしたい場合は戻り値を使う。

package main

type foo struct {
    v int
}

func (f foo) add(v int) foo {
    f.v += v
    return f
}

func main() {
    var a foo

    a = a.add(3)

    println(a.v)
}

これとは別に、ポインタと実体をまぜて考えてしまうと失敗してしまう事もある。

この例のポイントは「for range の反復変数はループ毎に新しいコピーが作成されない」という事。つまり上書きになる。プログラマは一見このループが実行されると以下の様になると考えてしまう。

= append(b, c[0].f())
= append(b, c[1].f())

だが実際はこう。

var i a
= c[0]
= append(b, i.f())
= c[1]
= append(b, i.f())

The Go Playground

slice の b は一見、c の情報が詰め込まれている様に見えるが、実際は i の情報が詰め込まれている。なので2回目の i への代入時に「i が持っていたレシーバの情報 c[0]c[1] 上書きされてしてしまう事になり、結果 a, b ではなく b, b が表示される。Method values という仕組みは参考として見ておくと良い。i への代入時点でレシーバの情報が デリファレンス されコピーされた状態で代入されているのがポイント、f() の呼び出し時の話ではない。

package main

import (
    "fmt"
)

type a struct {
    N string
}

func (s *a) f() func() {
    return func() {
        fmt.Printf("%s\n", s.N)
    }
}

func main() {
    b := []func(){}
    c := []a{{"a"}, {"b"}}

    var i a

    i = c[0]
    b = append(b, i.f())

    b[0]()
    i = c[1]
    b[0]() // この時点で既に b になる
    b = append(b, i.f())
}

まとめ

混ぜるな危険

みんなのGo言語【現場で使える実践テクニック】 みんなのGo言語【現場で使える実践テクニック】
松木雅幸, mattn, 藤原俊一郎, 中島大一, 牧 大輔, 鈴木健太
技術評論社 / ¥ 5,140 (2016-09-09)
 
発送可能時間:

Posted at by | Edit


blog comments powered by Disqus