【Features編】ねこでもわかるGo言語


ねこでもわかる Go言語 の第2回目です。

タイトルにもあるように私はねこ好きなんですが, 住まいの都合でペットは飼えないので, 最近はねこ画像を集めて癒されています。
日課になりつつあるのでこれは自動化したい!ということで Google APIでつくりはじめました。双方向データバインドがしたいためだけに Angular.js を使って MVC の練習も兼ねます。

Go言語の特徴

Goには特徴的な機能が多くあります。一方でジェネリクスがないとか、クラスの継承がないとか、型宣言が逆とか現在の主流言語との相違点もあります。今回は興味を持った機能を紹介します。

  • 構造体とインターフェイス
  • Map
  • goroutine

構造体とインターフェイス

Goには クラス がありませんが, 構造体を使って似たような機能を持たす事ができます。
また, クラスの継承はありませんが似た機能として後述するインターフェイスがあります。これにより型で区別するメソッドを作ることができます。

例えば構造体は以下のように書きます。

var rect struct{
	length int 
	name string
}

rect.length = 10
rect.name = "四角"

println("長さ:", rect.length, "名前:", rect.name)

Goでは構造体に直接メソッドを書く事ができませんが, 構造体のレシーバを使うことでメソッドを定義できます。型にメソッドを持たせるイメージです。

func main() {
	Array := make([]Rect,5)

	for i := 0; i < len(Array); i++{
		Array[i].length = i+1
		Array[i].name = "四角"
		println(Array[i].intro())
	}
}

type Rect struct {
		length int 
		name string
}

func (figure Rect) intro()(str string){
	str = fmt.Sprintf("図形は%sで横の長さは%dMです", figure.name, figure.length)

	return str
}

構造体 Rect のメンバ length, name を参照するメソッドが intro() です。実行結果は以下です。

図形は四角で横の長さは1Mです
図形は四角で横の長さは2Mです
図形は四角で横の長さは3Mです
図形は四角で横の長さは4Mです
図形は四角で横の長さは5Mです

続いて, インターフェイスです。Goのオブジェクト指向はクラスでなくこのインターフェイスによるものです。インターフェイスは抽象クラスみたいな機能です。

インターフェイスを使ったプログラム例です。(冗長なコード)

func main() {
	var GetParam Interface
	var num numbers 
	var str strings

	num.num_1 = 2012
	str.str_1 = "I Like cat."

	GetParam = num
	println(GetParam.GetParam())

	GetParam = str
	println(GetParam.GetParam())
}

type Interface interface { // メソッドの宣言
		GetParam() string
}

type numbers struct {
	num_1 int
}

type strings struct {
	str_1 string
}


func (str strings) GetParam()(ret string) { // メソッドの実装(string)
	ret = fmt.Sprintf("文字列は %s です", str.str_1)

	return ret
}

func (num numbers) GetParam()(ret string) { // メソッドの実装(int)
	ret = fmt.Sprintf("数字は %d です:", num.num_1)

	return ret
}

上記例では構造体を2つ宣言し2つに対応したメソッド GetParam を実装しています。main内で構造体のメンバを呼び出しています。

数字は 2012 です
文字列は I Like cat. です

GetParam = num とするか GetParam = str にするかで結果が変化するのがわかると思います。

以上をまとめると,, レシーバを使って構造体にメソッドを持たせることができ, interfaceを使ってから構造体が持つメソッドのリストを作ることができる。

Map

Map は key-value 構造です。この辺はモダンな印象を受けます。まず map を宣言します。

var m map[string]int

続いて、make によってインスタンス化します。データを追加してみましょう。

m["a"] = 1

データを消すにはこんな感じで false を指定します。

m["a"] = 1,false

goroutineを使ってみる

Go の並行処理へのアプローチは CSP (Communicating Sequential Processes) というモデルを参考にされており, これはUNIXのパイプラインと同様です。詳しくはこちらを参考に。

同一アドレス空間内で他の goroutine を並行実行することができ, 軽量化の工夫として開始時のスタックサイズ (一時メモリ保持)は節約のため小さく取り、必要に応じてヒープ領域を割り当てています。

Goにおける並行処理は以下の2つの特徴があります。

メモリ共有

Goではチャネルを利用することでメモリ共有による同時書き込みの危険性を減らしています。
このチャネルはキュー構造をしており、makeを使って割り当てることができます。
また、ポインタ参照を禁止(非推奨)することでデータ共有をさけています。この方法をメッセージパッシングと呼びます。

マルチコア対応

自動でコンテキストを切り替えを行うことで、マルチコアで実行が可能です。
ここでいうコンテキストとは、制御フローの切り替えであり手動の場合、これをプログラムに記述する必要があります。

goroutine を使った簡単な例を示します。非同期処理したい関数の前に go を追加するだけです。

func main() {
	value_1 := ch_1();
	value_2 := ch_2();

	for i:= 0; i <= 10;i++ {
           println("ST1:",<- value_1,"\t","ST2:",<- value_2);
        };
}


func ch_1 () chan int {
	ch := make(chan int );

	go func(){
		for i := 0;i <= 10;i++{
		   ch <- i;
		}
	}();

	return ch;
}

func ch_2 () chan int {
	ch := make(chan int );

	go func(){
		for i := 10;i <= 10;i--{
		  ch <- i;
		}
	}();

	return ch;
}

上記のように チャネル を経由することでデータのやり取りができます。
ch <- i によって、 goroutine の無名関数内で channel にデータを送信しています。ch_1はchannelで受け取ったデータを返します。
それを main 関数内で変数 stream_1 に入れて表示した結果が以下です。

ST1: 0 	 ST2: 10
ST1: 1 	 ST2: 9
ST1: 2 	 ST2: 8
ST1: 3 	 ST2: 7
ST1: 4 	 ST2: 6
ST1: 5 	 ST2: 5
ST1: 6 	 ST2: 4
ST1: 7 	 ST2: 3
ST1: 8 	 ST2: 2
ST1: 9 	 ST2: 1

Go では関数の引数に関数を指定することもできます。

使う CPU Core数を変化させて, goroutine を使ったプログラムの実行時間を計測してみます。使うマシンは 4 Cores搭載の 1.3 GHz Intel Core i5 です。

まずは gorouthineを使わない場合です。forループ 100億回 を計算させます。

package main

import (
	"fmt"
	"runtime"
	"time"
)

func main() {
	cpus := runtime.NumCPU()
	fmt.Println("Your Machine Has", cpus, "cores")

	start := time.Now().UnixNano()

	x := 0
	for i := 0; i < 10000000000; i++ { // 100億
		x += 1
	}
	fmt.Println("sum :", x)

	end := time.Now().UnixNano()

	fmt.Println(float64(end-start)/float64(1000000), "ms")
}

4423 ms かかりました。

$ go run no-goroutine.go
Your machine has 4 cores
sum : 10000000000
4423.711072 ms

続いて, goroutineで forループ 100億回 を 10個の goroutineで並行処理させます。

package main

import (
	"fmt"
	"runtime"
	"time"
)

func worker() <-chan int {
	receiver := make(chan int)
	s := 10
	for i := 0; i < s; i++ {
		go func(i int) {
			x := 0
			for j := 0; j < 1000000000; j++ {
				x += 1
			}
			fmt.Println(i, ":", x)
			receiver <- x
		}(i)
	}
	return receiver
}

func main() {
	cpus := runtime.NumCPU()
	fmt.Println("Your Machine Has", cpus, "cores")

	start := time.Now().UnixNano()

	receiver := worker()
	y := 0
	for i := 0; i < 10; i++ {
		y += <-receiver
	}

	fmt.Println("sum :", y)
	end := time.Now().UnixNano()

	fmt.Println(float64(end-start)/float64(1000000), "ms")
}

4コア搭載のマシンなので, GOMAXPROCS環境変数 を 1~4の間で動かして プロセスを起動させます。

$ GOMAXPROCS=1 go run goroutine.go
4057.920576 ms
$ GOMAXPROCS=2 go run goroutine.go
2209.118826 ms
$ GOMAXPROCS=3 go run goroutine.go
2372.733596 ms
$ GOMAXPROCS=4 go run goroutine.go
2204.119871 ms

1 coreの場合は goroutine を使わない場合とほぼ同じ実行時間でした。2-4 coresの場合は 1 coreと比較すると 約6割 実行時間が短くなりました。
3 coresが若干遅いように見えますが, 何度か実行して平均を取れば他とも変わらないと思います。

おわりに

触ってみて2週間なので間違った情報を書いてしまっているかもしれませんが悪しからず。


[1] A Multi-threaded Go Raytracer
[2] effective_go