RACCOON TECH BLOG

株式会社ラクーンホールディングスのエンジニア/デザイナーから技術情報をはじめ、世の中のためになることや社内のことなどを発信してます。

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])
}
「map[キーの型]値の型」という慣れていないと大変気持ちの悪い書き方に見えてしまいますが、任意の型を組み合わせたmap型を定義することができます。
また、下記のコードのように使い勝手の良い初期化リテラルや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)
    }
}
intなどの値型や構造体型はそのままで関数に引数で渡すとスタック上にコピーが発生して別の領域に格納されたデータとして扱われるため呼び出し側のデータそのものを参照させたい場合にはポインタ型を利用する必要がありました。
しかし、参照型については他の関数にわたしてもコピーされることはなく参照としてそのまま引き渡すことができます。

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])
}
上記のコードで出てくるlen()は配列またはsliceの要素数を取り出すための組込み関数です。また、cap()は配列構造を格納するために用意されているcapacityを取り出すための組込み関数です。
平易に「[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!
}
上記のコードはmainの最終行でruntime errorを表示して終了します。len=2であるsliceの3番目の要素に値を書き込もうとしたためです。
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))
}
当然のことながらappendによってキャパシティを自動的に増加させるのはそれ相応のコストがかかります。上記の小さなコード程度ではほとんどパフォーマンスに差は出ませんが、予め取り扱うデータ量が想定される場合はキャパシティを適切な値に設定することが重要になります。

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)
}
上記のコードはchannelを作成して、そのチャンネルに「<-」演算子を使って数値の1を書き込んでいます。また次に「<-」演算子を使ってchannelに書き込まれた値を受け取って最後にその値を表示するという流れになっています。
しかし、コンパイルは問題ないものの上記のコードは期待通りに動きません。

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
「全てのgoroutineが眠っている」=「デッドロックを検出」というエラーメッセージが表示されました。これは一体どういうことなのでしょうか?
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)
}
実験的にバッファサイズに1を設定したchannelに修正してみましょう。今度は意図通りに動作することが確認できるでしょう。
しかし、channelからデータを取り出す前のバッファが埋まった状態でさらにchannelにデータを入力すると前段のコードと同様deadlockによってプログラムは停止してしまいます。

また、channelへのデータの入力と同様にデータの受信時に受け取り可能なデータが存在しない場合はgoroutineが停止します。

package main
import "fmt"
func main() {
    ch := make(chan int)
    n := <- ch /* deadlock! */
    fmt.Printf("%d\n", n)
}
channelは多数の並列処理間で安全にデータを共有する仕組みであり、かつ各並列処理の実行や停止を調整するロック機能も提供している機能であると言えるでしょう。

goroutine

さてgoroutineについて見てみましょう。
goでは「goステートメント」を使って与えた関数を非同期で実行するgoroutineを作成します。

package main
import "fmt"
func main() {
    go fmt.Printf("Hello, Another World!\n")
    fmt.Printf("Hello, World!\n")
}
上記のコード例を実行するとわたしのテスト環境では「Hello, Another World!」の出力が出たり出なかったりとまちまちな動作をします。

手っ取り早く作成した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から受信して値は捨てる */
}
「goステートメント」には無名関数を与えることができます。
「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という挨拶を出力する関数を定義していますが関数名の前にMember型のselfという引数が定義されています。
ここでは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()
    }
}
メソッドgreetを備えるinterfaceとしてGreetableという型を定義しています。
せっかくなのでポリモルフィズムの参考として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!")
}
組込み関数panicにエラーメッセージを渡すとそこで関数の実行が停止します。
したがって上記のコードでは「Hello, World!」は出力されずにランタイムエラーを出力してプログラムが停止します。
package main
import "fmt"
func main() {
    defer fmt.Println("Hello, World!")
    panic("error!")
}
panicは原則として関数の実行を停止しますがdeferで定義された処理はしっかりと実行されますので、上記のコードであれば「Hello, World!」の出力後にランタイムエラーでプログラムが終了します。

panicによる関数の巻き戻しを停止させるには組込み関数recoverを使用します。原則としてrecoverはdeferによって遅延実行させなければ意味を持ちません。

package main
import "fmt"
func main() {
    defer func() {
        err := recover()
        fmt.Printf("%s\n", err)
    }()
    panic("error!")
}
deferにrecoverを使用してpanicによるエラーを補足しつつ関数の巻き戻しを停止させています。

package main
import "fmt"
func main() {
    err := recover() /* => nil */
    fmt.Println(err)
}
特に意味を持ちませんがrecoverはどこに書いても動作しますが、deferによるpanicが起きたコンテキスト以外では単にnilを返します。

とりあえず終わり

3回に渡ってgoを書く上でポイントになりそうなところをまとめてみました。初めはもう少し簡単に書けるかと思ったのですがけっこう大変でした。プログラミング言語を解説するって難しいですね。

ラクーンはメインでJava、PHP、最近Rubyを導入しつつといった環境でサービスを展開していますが、開発メンバーに対してどういう説明の仕方をすれば効率的に他言語を学ぶことができるだろうか?という試行錯誤の果てに初心者向けでもなければ上級者向けでも無い個人的には「職業プログラマ向け」というスタンスで書いた解説になっているつもりです。不正確なところや誤解を招きそうなところがありましたらご指摘いただければ幸いです。

あー、疲れた。
 

一緒にラクーンのサービスを作りませんか? 採用情報を詳しく見る

関連記事

運営会社:株式会社ラクーンホールディングス(c)2000 RACCOON HOLDINGS, Inc