Raccoon Tech Blog [株式会社ラクーン 技術戦略部ブログ]

株式会社ラクーン 技術戦略部より、tipsやノウハウなど技術的な話題を発信いたします。

fluentd社内勉強会

fluentdの社内勉強会を技術戦略部の柚木を講師として開催しました。

ログの集約やさまざまな監視などに今や不可欠と言っても過言ではないfluentdについての知識を広く共有することを目的にしています。

写真 1
写真 2

次回の社内勉強会はAWSのワークショップを予定しています。

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

あー、疲れた。
 

Go事始め (2)

開発の松尾です。
前回は環境面について解説しましたが、今回はGo言語の文法と特徴について解説してみたいと思います。

Go言語のシンプルさと特殊さ

Goは言語の文法という側面から見ると実に「シンプル」な言語です。型推論による型指定の省略、ループのための構文は"for"のみ、GCを備えメモリ管理が不要・・・・・・などなどプログラムを最低限のコード量で記述できるように設計されています。
また、一方でGoは他言語経験者の視線から眺めると少々変わった分かりにくい要素をいくつか備えています。「ポインタ型はあるがポインタ演算は無い」、「channelやmapやsliceといった特殊な組み込み参照型」、「変数の値がstackとheapのどちらに配置されるかはコンパイラが適切に決めてくれる」などなどと私自身もGoの言語仕様を理解する途上でいくつか躓くポイントがありました。

このようにGoには「シンプルで分かりやすい」部分と「Go独特の言語仕様で分かりにくい」箇所が混在しています。このような性質を踏まえつつ言語仕様を見ていくことにしましょう。

func main()

C言語で書かれたプログラムがmain関数より実行が開始されるように、またJavaの場合は指定したclass内に含まれるstaticなmainメソッドより実行が開始されるのと似て、Goはmainパッケージに定義されたmain関数より実行が開始されます。
したがって下記のようなmain関数を含まないソースコードは実行できません。
/* no_main.go */
package main import "fmt" func no_main() { fmt.Println("hogehoge") }
$ go run no_main.go
# command-line-arguments
runtime.main: undefined: main.main
mainパッケージのmain関数が定義されているファイルに対してのみ"go run"で実行することができ、"go build"で実行可能ファイルを作成することができます。

定数

定数はconstを使って定義することができます。
package main
import "fmt"
const (
	OUTER = "外側のconst"
)
func main() {
	const inner = "内側のconst"
	fmt.Printf("%s %s \n", OUTER, inner)
}
パッケージ下および関数内で定義することが可能です。
ここでは文字列を値とする整数を定義していますが、整数、浮動小数点など様々な型を定数として定義することができます。
ちなみに上記のコードでは日本語文字列を使用していますがGoが対応するマルチバイト文字列のリテラル表現はUTF-8エンコーディングに対応しています。(それ以外のエンコーディングである場合はコンパイル時のエラーとなります。)

また、Goにはenum(列挙型)に該当する文法はありませんが、iotaを使うことでenumに似た定数定義を行うことができます。
package main
import "fmt"
const (
	A = iota
	B
	C
	_  // 3をスキップ
	D
	E
)
func main() {
	const X = iota
	const Y = iota

	fmt.Printf("%d\n", A) // 0
	fmt.Printf("%d\n", B) // 1
	fmt.Printf("%d\n", C) // 2
	fmt.Printf("%d\n", D) // 4
	fmt.Printf("%d\n", E) // 5

	fmt.Printf("%d\n", X) // 0
	fmt.Printf("%d\n", Y) // 0
}
iotaの挙動はちょっと分かりづらいのですが「コンテキストが異なるiotaの値は0に戻る」(ソース内のX、Yの挙動)、「同じコンテキスト内では0から1ずつ増加する」、「iotaを使った式がそれ以下の定数に対して自動的に適用される」(ソース内のA-Eの挙動)あたりがポイントになります。

reflectパッケージ

ここで突然reflectパッケージについて説明します。
reflectパッケージは主としてGoのリフレクション機能を提供します。といってもその機能は多岐に渡ってしまうのでここではreflectパッケージを利用することで変数の型情報を得ることができる利便性について解説します。
Goの型システムには少々分かりづらいところがあるため、具体的に実行時の型がどのようになっているのかを得る手段を知ることが大変有用だと思われるためです。

さっそく使ってみましょう。
package main

import(
	"fmt"
	"reflect"
)

func main() {
	b := true
	fmt.Println(reflect.TypeOf(b))
	ui8 := uint8(100)
	fmt.Println(reflect.TypeOf(ui8))
	n := 100
	fmt.Println(reflect.TypeOf(n))
	c := 'A'
	fmt.Println(reflect.TypeOf(c))
	r := 'あ'
	fmt.Println(reflect.TypeOf(r))
	u := '\U00101234'
	fmt.Println(reflect.TypeOf(u))
	s := "文字列"
	fmt.Println(reflect.TypeOf(s))
	a := [3]int{1, 2, 3}
	fmt.Println(reflect.TypeOf(a))
}
このプログラムの実行結果は下記のようになります。
bool
uint8
int
int32
int32
int32
string
[3]int
この結果で分かることは、
  • 組込みでbool型をもつ(true or false)
  • 型指定の無い数値の定数はint型となる
  • シングルクォーテーションで囲んだ「ルーンリテラル」はUnicodeの1文字を表す書き方でint32型となる
  • 組込みでstring型をもつ
  • 配列型は[3]intのように要素数も含めてひとつの型を表す
上記のようなポイントを読み取ることができます。

goでは「:=」の構文を使うことで型指定の省略を行うことができます。しかし、何らかのコードを読んでいて変数の実際の型を知りたいなどの場合にはreflectパッケージの機能が有効に利用できます。

もうひとつ気をつけるべきポイントがあります。goでは組込み型としてuint32(符号なし32bit)、int8(符号あり8bit)などの厳密な数値型が用意されているのですが、数値定数がデフォルトでとるint型ではプラットフォーム次第でその内容が異なることがあります。
具体例を示すと、下記のコードは64bit環境であれば問題なく動作しますが、32bit環境ではコンパイルエラーとなります。
package main

import(
	"fmt"
	"reflect"
)

func main() {
	n := (1<<63) - 1 /* 64bit環境におけるintの最大値 */
	fmt.Println(reflect.TypeOf(n))
}
ここまでは明示的にreflectパッケージの機能を使用してみましたが、goではfmt.Printfの書式に「%T」を指定することで同様の出力を得ることができます。
package main

import (
	"fmt"
)

func main() {
	fmt.Printf("%T\n", true)
	fmt.Printf("%T\n", 1024)
	fmt.Printf("%T\n", "string")
	fmt.Printf("%T\n", [3]int{1, 2, 3})
}
変数の型情報を見るには上記の書き方で十分でしょうが、goでは実行時に変数の型情報を取り出すことができるところがポイントです。Javaで言えばinstanceof演算子のような振る舞いを実現するのに利用することができます。

変数

 先に型推論を利用した変数定義とreflect.TypeOfについて解説しましたが型指定も含めた変数の定義は下記のように書きます。
package main

import (
	"fmt"
)

func main() {
	var b bool = false
	fmt.Println(b)
	var c int32 = 'あ'
	fmt.Println(c)
	var s string = "あいうえお"
	fmt.Println(s)
	var a [5]int = [5]int{1, 2, 3, 4, 5}
	fmt.Println(a)
}
CやJavaの経験者であれば変数名の後に型指定があるところや一見奇妙に見える配列型の定義箇所が少々気味が悪く感じるかもしれません。これがgoの構文ですので慣れましょう(笑)

 また、goの慣例として定義する変数名を「短く簡潔に」定義する傾向があります。Javaなどのように「長く具体的な」命名の慣習を持つ文化に馴染みが深いと違和感が大きいとは思いますが、郷に入っては郷に従え、できるだけ学ぼうとする言語の慣習で書くべきでしょう。それによってコードを読む際にも大いに役に立ちます。

ポインタ型

goにはCに良く似たポインタ型が用意されています。
package main

import (
	"fmt"
	"reflect"
)

func main() {
	n  := 100
	np := &n
	fmt.Println(reflect.TypeOf(np))
	fmt.Println(np)
	fmt.Println(*np)
}
「&変数名」でポインタの取り出し、「*変数名」で値の取り出しとCと大きく違うところはありません。
goのポインタで一番重要なのは「ポインタ型はあるがポインタ演算が無い」という特性です。言語仕様からポインタ演算を取り除くことで得られる安全性を重視した結果とも言えるでしょう。

ポインタについての説明は本気でやると際限なく長くなりそうなのでこの程度にとどめておきます。

構造体(Struct)

goにはCによく似た構造体の定義を行うことができます。
package main

import (
	"fmt"
)

type Member struct {
	name string
	age  int
}

func main() {
	var m Member = Member{"まつお", 37}
	fmt.Printf("name = %s\n", m.name)
	fmt.Printf("age  = %d\n", m.age)
}
Cの構造体と異なってフィールドの初期化に便利なリテラルが利用できます。上記ではname、ageといったフィールドに対して変数の定義と同時に初期化を行っています。

goの構造体の作成には「new([構造体型])」という書き方もあります。
package main

import (
	"fmt"
)

type Member struct {
	name string
	age  int
}

func main() {
	m := new(Member)
	fmt.Printf("name = %s\n", m.name)
	fmt.Printf("age  = %d\n", m.age)
}
newによる初期化は構造体の領域のみを作成して「構造体のポインタ型」を返すことに注意してください。上記のコードを実行するとMember型のフィールドnameは空文字列、ageは数値の0になっていることが確認できます。

なぜこのような文法が用意されているのでしょうか?
私見ですがプログラムにおいて構造体で定義されるデータ構造は比較的「長生き」する性質があります。Cにおいてヒープに作成された構造体のポインタを持ち回り各所で処理を行っていくなどの流れは極めて基本的な手順です。この「構造体を作成してそのポインタを取り出す」といった頻出するパターンを簡略化したのがこのnewによる構造体の作成の手法なのだと思われます。

ちなみにgoでは実行時にオプションを指定してどの変数がヒープに格納されるかを確認する手段が用意されています。
/* newを使用して構造体のポインタを返すCreateMember */
package main

import (
	"fmt"
)

type Member struct {
	name string
	age  int
}

func CreateMember(name string, age int) *Member {
	m := new(Member)
	m.name = name
	m.age = age
	return m
}

func main() {
	m := CreateMember("まつお", 37)
	fmt.Printf("name = %s\n", m.name)
	fmt.Printf("age  = %d\n", m.age)
}
/* 構造体をstack上に作成して構造体のコピーを発生させるパターン */
package main

import (
	"fmt"
)

type Member struct {
	name string
	age  int
}

func CreateMember(name string, age int) Member {
	var m Member
	m.name = name
	m.age = age
	return m
}

func main() {
	m := CreateMember("まつお", 37)
	fmt.Printf("name = %s\n", m.name)
	fmt.Printf("age  = %d\n", m.age)
}
上記2パターンを下記のオプションを付加して実行してみてください。異なる結果が得られます。
下記はnewを使用したバージョンの実行例です。
$ go run -gcflags -m test1.go
# command-line-arguments
./test1.go:12: can inline CreateMember
./test1.go:20: inlining call to CreateMember
./test1.go:12: leaking param: name
./test1.go:13: new(Member) escapes to heap
./test1.go:20: CreateMember new(Member) does not escape
./test1.go:21: main ... argument does not escape
./test1.go:22: main ... argument does not escape
name = まつお
age  = 37
「new(Member) escapes to heap」の箇所からヒープに割り当てられたことが分かりました。

goのプログラム内の変数はその参照範囲を解析することでスタックに配置されるかヒープに配置されるかが自動的に判別されてコンパイルされます。非常に便利な機能なのですが反面、知らずに書くと非効率的なコードを書いてしまう危険性をはらみます。
最低限、データがスタックに置かれるかヒープに確保されるかについて多少なりとも意識的にコーディングする必要があります。

ついでにもうひとつ。構造体の出力についてはfmt.Printfに便利な書式が用意されています。
package main

import (
	"fmt"
)

type Member struct {
	name string
	age  int
}

func main() {
	var m Member = Member{"まつお", 37}
	fmt.Printf("%T\n", m)
	fmt.Printf("%+v\n", m)
	fmt.Printf("%#v\n", m)
}
main.Member
{name:まつお age:37}
main.Member{name:"まつお", age:37}
構造体のフィールドとデータを一括して表示させたい場合に役に立つでしょう。

関数

goの関数定義についてはそれほど変わったところはありません。
package main

import (
	"fmt"
)

func power(n int) int {
	return n * n
}

func main() {
	fmt.Println(power(10))
}
あまり他の言語に見られない文法としては関数が「多値」を返すように定義できるところでしょうか。
package main

import (
	"fmt"
)

func oneTwoThree() (int, int, int) {
	return 1, 2, 3
}

func main() {
	a, _, c := oneTwoThree()
	fmt.Printf("%d %d\n", a, c)
}
ちょっと無理矢理ですがint型の数値を3つ戻す関数の定義です。また、関数の呼び出し側の代入部分で分かるように_(アンダースコア)で変数名を省略して戻り値を無視することができます。

goはC++やJavaのような例外機構は備えていません。そのためgoのプログラムにおいては「失敗する可能性のある処理」については処理結果とエラー情報 を多値で返すという下記のようなパターンが頻出します。
package main

import (
	"fmt"
	"os"
)

func main() {
	file, err := os.Open("input.txt")
	if err != nil {
		os.Exit(1)
	}
	fmt.Printf("%#v\n", file)
}
このようにgoのエラー処理はCにおけるエラー処理の不便さとJavaなどの例外処理のようなコストの高い仕組みの中間ぐらいを狙って設計されていることが分かります。

for

goにおけるループのための構文はforのみと大変シンプルです。
package main

import (
	"fmt"
)

func main() {
	/* C言語的なfor */
	for i := 0; i < 5; i++ {
		fmt.Printf("i=%d\n", i)
	}
	/* C言語タイプのwhile */
	c := 0
	for c < 5 {
		fmt.Printf("c=%d\n", c)
		c++
	}
	/* 配列の走査にはrangeを使う */
	a := [3]int{1, 10, 100}
	for i, v := range a {
		fmt.Printf("[%d]=%d\n", i, v)
	}
	/* stringを文字単位で処理する */
	for pos, r := range "日本語" {
		fmt.Printf("[%d]=%c\n", pos, r)
	}
	/* 無限ループ */
	for {
		break
	}
}
forにいくつかの書き方パターンがあるところが独特ですね。

defer

goの特徴的な構文にdeferがあります。
deferで定義された関数呼び出しは実行されている関数の「脱出直前に呼び出される」という動作をします。
package main

import (
	"fmt"
)

func main() {
	fmt.Println("1")
	defer fmt.Println("2")
	defer fmt.Println("3")
	defer fmt.Println("4")
	fmt.Println("5")
}
Javaなどのtry-catch-finallyといった文法のfinallyに近い動作イメージです。
オープンしたファイルを関数の完了時に確実にクローズするなどのリソース処理などを簡潔に記述するのに役に立ちます。

To be continued

長くなってきたので第2回めはここでいったん終わります。
次回は少し分かりづらいgoの「参照型」や非同期処理のためのgoroutineという仕組みについて解説してみようと思います。

記事検索