Go事始め (3)
開発の松尾です。
「毎年少なくとも一つの言語を学習する― 言語が異なると、同じ問題でも違った解決方法が採用されます。つまり、いくつかの異なったアプローチを学習することにより、幅広い思考ができるようになるわけです」 (達人プログラマー―システム開発の職人から名匠への道)
怠惰により「Language of the Year」といったペースは果たせていませんが、それでも複数の言語を学ぶ効力はそれなりに知っているつもりです。今年はKotlinあたりが楽しそうだなあと思いつつ。
「Go事始め 」の3回目です。
参照型
前回まででgoには値型と構造体型にそれぞれのポインタ型が用意されていることがわかりました。
goにはさらに「参照型」と呼ばれる特殊な組込型が用意されています。
不正確な表現になりますが単純化するとgoの参照型とは主として「make」を使って作成するデータ構造を参照する型になります。具体的には「map」と「slice」と「channel」という3種類の特殊なデータ構造が参照型として用意されています。
map
goのmapはいわゆる「ハッシュテーブルです」。JavaにおけるHashMap、RubyにおけるHashといったようにキーと値の組み合わせを複数格納するための便利なデータ構造ですがgoではmapという型でサポートしています。
package main
import "fmt"
func main() {
m := make(map[int]string)
m[1] = "あ"
m[2] = "い"
m[3] = "う"
fmt.Printf("%s,%s,%s\n", m[1], m[2], m[3])
}
また、下記のコードのように使い勝手の良い初期化リテラルやforを使用したイテレーションもサポートされています。
package main
import "fmt"
func main() {
/* mapの初期化リテラル */
m := map[string]int{
"A": 128,
"B": 256,
"C": 512,
}
/* mapのイテレーション */
for k, v := range m {
fmt.Printf("%s => %d\n", k, v)
}
}
しかし、参照型については他の関数にわたしてもコピーされることはなく参照としてそのまま引き渡すことができます。
package main
import "fmt"
func hoge(m map[string]int) {
m["d"] = 8
return
}
func main() {
m := map[string]int{
"a": 1,
"b": 2,
"c": 4,
}
hoge(m)
for k, v := range m {
fmt.Printf("%s => %d\n", k, v)
}
}
map型を定義する文法は少々奇妙な印象も受けますが、頻繁に使用されるデータ構造をできるだけシンプルに書きやすくなるように設計されていることがうかがえます。
slice
わたしがgoに触れた時に一番混乱したのは「配列型とsliceの違い」でした。便利な仕組みではあるもののgoの言語仕様でもっとも混乱を招きやすいのは相変わらずこのあたりだと思っています。
いくつかのsliceの例を見てみましょう。
package main
import "fmt"
func main() {
/* [...]で要素数を省略した配列型。[5]intと書いても同じ */
a := [...]int{1, 2, 3, 4, 5}
fmt.Printf("length=%d,capacity=%d,first=%d\n", len(a), cap(a), a[0])
/* [5]int全体を参照するslice */
s := []int{1, 2, 3, 4, 5}
fmt.Printf("length=%d,capacity=%d,first=%d\n", len(s), cap(s), s[0])
/* [5]intの2要素目から3要素目までの参照するslice */
p := a[1:3]
fmt.Printf("length=%d,capacity=%d,first=%d\n", len(p), cap(p), p[0])
/* capacity=100で先頭の1要素のみ0に初期化されたslice */
b := make([]int, 1, 100)
fmt.Printf("length=%d,capacity=%d,first=%d\n", len(b), cap(b), b[0])
}
平易に「[5]int{...}」といった配列型を定義すれば、len=5、cap=5と要素数もキャパシティも同じ数値を戻します。
しかし、上記のコード例の変数pのように配列aの一部分を参照するsliceは要素数、キャパシティともに異なることが確認できます。a[1:3]という書き方は配列型であるaのa[1]、a[2]に該当する範囲に限定されたsliceを作成します。従ってlen=2となります。さて、pのcap=4とは何を意味するのでしょうか?
package main
import "fmt"
func main() {
a := [...]int{1, 2, 3, 4, 5}
p := a[1:3]
fmt.Printf("length=%d,capacity=%d,first=%d\n", len(p), cap(p), p[0])
p[2] = 15 // runtime error!
}
goの配列型は型が厳密であり可変長配列のような要素数が変動するような操作はできません。しかし、sliceであれば組み込み関数appendを利用することで可変長配列のような振る舞いを実現できます。
package main
import "fmt"
func main() {
a := [...]int{1, 2, 3, 4, 5}
p := a[1:3]
fmt.Printf("length=%d,capacity=%d\n", len(p), cap(p))
p = append(p, 6)
fmt.Printf("length=%d,capacity=%d\n", len(p), cap(p))
p = append(p, 7, 8, 9)
fmt.Printf("length=%d,capacity=%d\n", len(p), cap(p))
p = append(p, 10, 11, 12)
fmt.Printf("length=%d,capacity=%d\n", len(p), cap(p))
}
上記のコードを実行した出力は下記のようになりました。
length=2,capacity=4
length=3,capacity=4
length=6,capacity=8
length=9,capacity=16
appendで要素を追加するにしたがってlenの返す値が増加するのは見ての通りなのですが、あるタイミングでcapの返す値が大きくなっているのがわかります。ここで要素数とキャパシティは異なるものであることが理解できます。配列のサイズを表す要素数と配列の末尾に効率的に要素を追加するために予め確保されたメモリ領域のサイズを表すキャパシティという2つの要素があるのです。
上記の実行例ではappendによってキャパシティを超える要素が配列に追加されるタイミングで元のキャパシティの倍の値に拡張されていることがわかります。(内部的にcap=4のメモリ領域から、cap=8のメモリ領域へコピーされていると想定できます)
goのsliceは特に効率を気にしなければガンガンとappendを使用してスクリプト言語の配列のように柔軟な使い方が可能になっています。
package main
import "fmt"
func main() {
/* len=0, cap=0のslice */
s1 := []int{}
for i := 0; i < 10000; i++ {
s1 = append(s1, i)
}
fmt.Printf("cap=%d\n", cap(s1))
/* len=0, cap=10000のslice */
s2 := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
s2 = append(s2, i)
}
fmt.Printf("cap=%d\n", cap(s2))
}
goにおける配列とsliceは全く違う構造を持っています。しかし、(これが最も混乱の元なのですが)文法上は比較的同じように操作できます。goでは自分が配列を扱っているのか、sliceを扱っているのかについてある程度意識的にコーディングする必要があります。
channel
channelは並列処理間でデータの受け渡しを行うためのデータ構造です。どういった「型」を受け渡すのかという情報と何個のデータをバッファリング可能かという「バッファサイズ」の2つの情報で構成されています。
goはgoroutineという並列処理のための機構を備えています。channelは複数のgoroutine間のデータ共有のための中心的な存在です。
まずはgoroutineは抜きにしてchannel単体で見てみましょう。channelはmapやsliceのようにmakeで作成します。
package main
import "fmt"
func main() {
ch := make(chan int) /* バッファサイズ0のchannel */
ch <- 1
n := <- ch
fmt.Printf("%d\n", n)
}
しかし、コンパイルは問題ないものの上記のコードは期待通りに動きません。
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/home/aiga/src/golang/test/ch1.go:7 +0x54
exit status 2
goはOSが提供するthreadとは異なるgoroutineを処理の単位として持ちます。特にgoroutineを生成しないこの記事のコード例のような単純なコードでもmainが動作している以上はひとつのgoroutineが動作していることになります。
上記のコードではバッファサイズ0のchannelに「ch <- 1」とデータを入力した時点でその動作主体であるgoroutineが待ち状態になります。結果的にプログラムの実行状態に唯一存在するgoroutineが停止したため処理を進行させるgoroutineが無くなったことによりデッドロックと判断されました。
package main
import "fmt"
func main() {
ch := make(chan int, 1) /* バッファサイズ1のchannel */
ch <- 1
n := <- ch
fmt.Printf("%d\n", n)
}
しかし、channelからデータを取り出す前のバッファが埋まった状態でさらにchannelにデータを入力すると前段のコードと同様deadlockによってプログラムは停止してしまいます。
また、channelへのデータの入力と同様にデータの受信時に受け取り可能なデータが存在しない場合はgoroutineが停止します。
package main
import "fmt"
func main() {
ch := make(chan int)
n := <- ch /* deadlock! */
fmt.Printf("%d\n", n)
}
goroutine
さてgoroutineについて見てみましょう。
goでは「goステートメント」を使って与えた関数を非同期で実行するgoroutineを作成します。
package main
import "fmt"
func main() {
go fmt.Printf("Hello, Another World!\n")
fmt.Printf("Hello, World!\n")
}
手っ取り早く作成したgoroutineの完了を待つにはどうすればいいでしょうか?
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
fmt.Printf("Hello, Another World!\n")
ch <- 1 /* channelに入力 */
}()
fmt.Printf("Hello, World!\n")
<-ch /* channelから受信して値は捨てる */
}
「chan int」であるchannelを作成しそれをgoroutineに与えて処理の完了の合図としてchannelに特に意味をなさない1という数値を入力しています。
main関数のラストでchannelからデータを受信する箇所が結果的に作成したgoroutineの完了を待つ処理として動作します。
マルチスレッド・プログラミングなどでよく見る同期処理とは趣が大きく異なりますが、goではchannelを駆使することで効率的な非同期処理を実現することができる仕掛けが施されていることがわかります。
ちょっとした応用ですが、goroutineとchannelを組み合わせることでPythonにおけるgeneratorのような処理も簡単に実現できます。
package main
import "fmt"
func nums(init int) chan int {
n := init
yield := make(chan int)
go func() {
for {
yield <- n
/* A */
n++
}
}()
return yield
}
func main() {
generator := nums(100)
fmt.Printf("num=%d\n", <-generator) // => 100
fmt.Printf("num=%d\n", <-generator) // => 101
fmt.Printf("num=%d\n", <-generator) // => 102
}
生成したgoroutineは関数numsの動作を開始してコード内の「A」の位置で停止します。以後「<-generator」のようにchannelから読み出しを行うたびに処理を再開して「A」の位置で停止を繰り返します。
このように協調して動作するコルーチン的な処理方法も簡単に実現できるところがgoの強みになっています。
interface
goでは型推論による型指定の省略によって大抵は変数の型を書かずに済む反面、関数の定義などでは厳密に引数の型を明示する必要があります。
ここで疑問が湧いてきます。fmt.Printfのような関数は第1引数にフォーマット文字列をとり、残りを可変長引数としてbool型、int型、string型、構造体型などなどあらゆる型を渡すことができます。
いったいこれはどういう仕組みで実現できているのでしょうか?
論より証拠、fmt.Printfの定義を見てみましょう。
func Printf(format string, a ...interface{}) (n int, errno os.Error)
第1引数のformatは特に問題ありません。「a ...」という書き方も可変長引数を配列として受け取るための書き方だと推定できます。しかし、可変長引数aの型指定のinterface{}とはどんな型なのでしょうか?
説明を前にざっくりとした答えをいえばinterface{}はgoのありとあらゆる型を表す汎用的な型指定です。
ここで話を戻します。Go言語はとりたてて「オブジェクト指向」な言語ではありません。JavaのClassのような機能は無くJavaScriptのprototypeのような機能もありません。
しかし、goには「interface」と「method」という機能は存在します。ただし「interface」といってもJavaのそれとは大きく異なります。
まずはgoのmethodとはどういうものでしょうか。
package main
import "fmt"
type Member struct {
name string
age int
message string
}
/* Member型に定義されたメソッド */
func (self Member) greet() {
fmt.Printf("%sです。%s\n", self.name, self.message)
}
func main() {
m := Member{"まつお", 37, "よろしくお願いします!"}
m.greet()
}
ここではMember型専用のメソッドgreetを定義していると読むことができます。
JavaやRubyのようにレシーバをthisなりselfなりで参照できる言語しか触ったことがない場合は少々分かりづらいかもしれませんが、PerlやPythonのオブジェクト指向機能のようにmethodの第1引数にレシーバをとるタイプの言語であれば馴染みやすい書き方ではないかと思います。
さてinterfaceの説明に戻ります。
上記でMember型へ定義したgreetメソッドをもつ型を表すinterfaceを作成してみましょう。
package main
import "fmt"
/* interfaceの定義 */
type Greetable interface {
greet()
}
type Member struct {
name string
age int
message string
}
type MemberEx struct {
name string
grade string
}
func (self Member) greet() {
fmt.Printf("%sです。%s\n", self.name, self.message)
}
func (self MemberEx) greet() {
fmt.Printf("グレード%sの%sだ。よろしくな!\n", self.grade, self.name)
}
func main() {
members := []Greetable {
Member{"まつお", 37, "よろしくお願いします!"},
MemberEx{"つちだ", "X"},
}
for _, m := range members {
m.greet()
}
}
せっかくなのでポリモルフィズムの参考としてMember型とは別の型としてMemberEx型を定義してみました。また、それぞれの型が内容の異なるgreetを実装しています。
Javaのinterfaceと異なり各々の型が明示的にinterfaceをimplementsする必要はありません。interfaceで定義されたmethod定義を満たしている型であればinterface型へのダウンキャストが可能です。(interfaceの定義を満たしていない場合はコンパイル時にエラーになります)
サンプルコードではGreetable型のsliceにMemberとMemberExの2つを並べていることで異なる型を共通するinterfaceでまとめることができることを示しています。
このようにgoではinterfaceとmethodを使用してポリモルフィズムを実現することができます。interface{}という書き方が「特にメソッド指定のない空のinterfaceを表す」ことから結果として「全ての型を表す」という流れが見えてくるのではないでしょうか。
例外処理
goは他の言語で良く見るtry〜catch型の例外処理機構は備えていませんが、独特な大域脱出のための仕組みを持ちます。
package main
import "fmt"
func main() {
panic("error!")
fmt.Println("Hello, World!")
}
したがって上記のコードでは「Hello, World!」は出力されずにランタイムエラーを出力してプログラムが停止します。
package main
import "fmt"
func main() {
defer fmt.Println("Hello, World!")
panic("error!")
}
panicによる関数の巻き戻しを停止させるには組込み関数recoverを使用します。原則としてrecoverはdeferによって遅延実行させなければ意味を持ちません。
package main
import "fmt"
func main() {
defer func() {
err := recover()
fmt.Printf("%s\n", err)
}()
panic("error!")
}
package main
import "fmt"
func main() {
err := recover() /* => nil */
fmt.Println(err)
}
とりあえず終わり
3回に渡ってgoを書く上でポイントになりそうなところをまとめてみました。初めはもう少し簡単に書けるかと思ったのですがけっこう大変でした。プログラミング言語を解説するって難しいですね。
ラクーンはメインでJava、PHP、最近Rubyを導入しつつといった環境でサービスを展開していますが、開発メンバーに対してどういう説明の仕方をすれば効率的に他言語を学ぶことができるだろうか?という試行錯誤の果てに初心者向けでもなければ上級者向けでも無い個人的には「職業プログラマ向け」というスタンスで書いた解説になっているつもりです。不正確なところや誤解を招きそうなところがありましたらご指摘いただければ幸いです。
あー、疲れた。