blog.bouzuya.net

2015-11-07 『ちょっとさわってみる Go 言語ハンズオン』にいった

ちょっとさわってみる Go 言語ハンズオン

関西オープンフォーラム 2015 で開催されていた GDG Kobe の『ちょっとさわってみる Go 言語ハンズオン』に参加した。A Tour of Go を前から流す。途中までは指示どおりに進めていたのだけど、Pointer や Array と Slice が気になってそこから進まなくなった。

まず Pointer 。正確には「参照渡し」という表現でつまずいた。普段はこんなことに噛みつかないんだけど、知らない言語なので噛みつく。「参照渡し」と「参照の値渡し」とでは随分と違う。結論としては golang.jp の FAQ 『関数パラメータは値渡しか?』 (原文 golang.or) にある通りすべて「値渡し」なので「参照の値渡し」が正解。ちなみにぼくが参照渡しを必要とする場面に遭遇したことはほとんどない。

次に Array 。Array を受け取る関数を試しに書こうとしてその型に驚いた。Golang では Array は長さ (length) を含めて「型」である。これはぼくの感覚からすると異常事態だ。[3]int[4]int とは別の型であり、長さ 3 の配列を受け取る関数は長さ 4 の配列を受け取ることはできない。配列の長さが固定という点について異論はないが、長さを型にするのはどうなんだ……。ちなみに変数として宣言すると以下のようになる。

var arr [3]int = [3]int{2, 3, 5}

A Tour of Go では := を常用しているので分かりにくいが、型宣言すると異常さが伝わってくる。

Array リテラルについては ... と書くことで長さを省略できる。左辺はもちろん省略できない、省略したいなら := を使う。以下の例では長さ 3 の配列を 2 つ用意している。

var arr1 [3]int = [...]int{2, 3, 5}
arr2 := [...]int{2, 3, 5}

最後が Slice 。チュートリアルでは自然と出てくるけど、おそらく相当数が Array と Slice とを正しく理解しないままに使いはじめることになるだろう。まずその構文を見てほしい。

var slice1 []int = []int{2, 3, 5} // Slice
slice2 := []int{2, 3, 5} // Slice
arr1 := [...]int{2, 3, 5} // Array

最後に意図的に Array を混ぜたのだけど、たぶん、他の言語から入った人だと Slice こそ Golang の Array だと思うだろう。あるいは Array リテラルの ... を省略可能だと思い Slice は Array の一種かのように思うかもしれない。しかし、このよく似た構文の Array と Slice とはまったく別ものだ。

Array は値であり、Slice は参照型だ。Golang は値渡しなので、呼び出し先で引数の Array の要素を変更しても呼び出し元の Array は変更されない。一方で呼び出し先で引数の Slice の要素を変更すると呼び出し元の Slice が変更される。ぼくにはなぜポインタではダメなのかが理解できない。ポインタ演算を避けたかったのだろうか。実行時にオーバーランは防げるだろうし、ホント何なんだ……。もう説明は要らないと思うが、参照型とポインタともまた別ものだ。参照型は値をポインタで操作するかのように参照を受け渡すが、ポインタではない。ぼくには不快だ。これで「言語仕様がシンプル」と主張する gopher を理解できない。

サンプルコードを示す。

package main

import "fmt"

func f(a [3]int) {
  a[0] = 100
}

func g(s []int) {
  s[0] = 100
}

func main() {
  var a1 [3]int = [3]int{2, 3, 5}
  f(a1)
  fmt.Println(a1) // [2 3 5]

  var s1 []int = []int{2, 3, 5}
  g(s1)
  fmt.Println(s1) // [100 3 5]
}

ちなみに Slice には闇がある。Slice は Array の参照と長さとを保持しており、それらを何も考えずに値コピーしている。なので GoのSliceもヤバイ - Qiita にあるようなパッと見て謎の挙動を見ることができる。

func main() {
  a := make([]int, 1, 2)
  b := a
  a2 := append(a, 1)
  b2 := append(b, 2)
  fmt.Println("a", a)   // [0]
  fmt.Println("b", b)   // [0]
  fmt.Println("a2", a2) // [0 2] ???
  fmt.Println("b2", b2) // [0 2]
}

もうこれだけでお腹いっぱいだ。またすこしずつ触る。構文的にもまだまだ闇がありそう。