このドキュメントは、http://golang.org/doc/go_tutorial.htmlの翻訳です。
はじめに
このドキュメントはCまたはC++プログラマ向けに書かれたGo言語のチュートリアルです。Go言語の全般的なガイドではありません。現在のところ全般的なガイドに一番近い資料はGo言語仕様です。
このチュートリアルを読み終わったら実践Go言語を読んでみてください。こちらにはもっと詳しい言語の活用方法が記載されています。またGo言語の3日間学習コースのスライドも合わせてご覧ください。(Day 1, Day 2, Day 3)
この資料では、一連のサンプルプログラムを通してGo言語の特色を説明します。サンプルプログラムは記述された単位でコンパイル・実行可能です。またこれらサンプルプログラムはリポジトリの/doc/progs/ディレクトリに格納されています。
サンプルプログラムは細切れになっています。行番号はオリジナルファイルの行番号が付けられているため番号が飛んでいますが、空の行は空のままにしておいてください。
Hello, World
おなじみのやつからはじめてみましょう。
05 package main
07 import fmt "fmt" // 入出力フォーマットを実装したパッケージ
09 func main() {
10 fmt.Printf("Hello, world; or Καλημέρα κόσμε; or こんにちは 世界\n");
11 }
packageステートメントを使ってこのGo言語ソースファイルがどのパッケージに属しているかを必ず宣言します。また別のパッケージに含まれているものを使用するためにはimportステートメントを使います。このプログラムではfmtパッケージをインポートし、fmt.Printfのようにパッケージ名を指定し、関数名の先頭を大文字にして呼び出しています。
関数を記述するときは関数名の前にfuncキーワードを書きます。プログラムのmainパッケージ内のmain関数が、まず最初(初期化処理後)に実行されます。
文字定数にはUTF-8エンコードのUnicodeを使うことができます。(Go言語のソースファイルはUTF-8エンコードと規定されています)
コメントの書き方はC++言語と同じです。
/* ... */
// ...
出力についてはもっと後で説明します。
セミコロン
すでに気づかれたかと思いますがプログラム内にはセミコロンがありません。Go言語のコードでは通常、forループの各節を区切るぐらいにしかセミコロンは使いません。各ステートメントの終端にセミコロンは不要です。
セミコロンは実際にはCやJavaといった開発言語と同じ扱いですが、ステートメントの終わりとみなせる全ての行末にセミコロンが自動的に挿入されるため、セミコロンを入力する必要はありません。
これがどのように行われるか詳細はGo言語仕様を見ていただきたいのですが、いま知っておかなくてはならないことは行末にセミコロンを書く必要がないことだけです。(一行に複数のステートメントを書きたいときはセミコロンを挿入することもできます。) 補足になりますが、波括弧{}直前のセミコロンも不要です。
この仕組みは、セミコロンのないすっきりしたコードを書くのに役立っています。ひとつ気をつけなければならない重要なこととして、ifのような本体部を持つステートメントでは、本体部の開始波括弧{をifと同じ行に書かなくてはならないことです。こうしないとコンパイルに失敗するか思いがけない結果が生じます。若干ですが言語の仕様によって波括弧の書き方が強制されています。
コンパイル
Go言語はコンパイル言語です。現在2種類のコンパイラがあります。GccgoはGCCをバックエンドとするGo言語コンパイラです。
もう一方のコンパイラ群はアーキテクチャごとに名前が付けられていて、6gは64ビットx86用、8gは32ビットx86用といった具合です。後者のほうはかなりコンパイル速度が速いのですが、吐き出されたコードはgccgoには劣ります。これを書いている今時点(2009年の終り)では後者のほうが実行できるシステムが多いのですが、gccgoもこれに追随しています。
下に示すのがコンパイルとプログラム実行の手順です。これは6gを使用した例です。
$ 6g helloworld.go # コンパイル; 作成されるオブジェクトは helloworld.6
$ 6l helloworld.6 # リンク; 出力は 6.out
$ 6.out
Hello, world; or Καλημέρα κόσμε; or こんにちは 世界
$
gccgoを使った場合のほうが従来のやり方に近いです。
$ gccgo helloworld.go
$ a.out
Hello, world; or Καλημέρα κόσμε; or こんにちは 世界
$
Echoコマンドの実装例
次は、Unixのecho(1)コマンドを実装した例です。
echoはコマンドラインパラメータから渡された文字列を標準出力に書き出すユーティリティです。
05 package main
07 import (
08 "os"
09 "flag" // コマンドラインオプションのパーサー
10 )
12 var omitNewline = flag.Bool("n", false, "don't print final newline")
14 const (
15 Space = " "
16 Newline = "\n"
17 )
19 func main() {
20 flag.Parse() // パラメータリストを調べてflagに設定
21 var s string = ""
22 for i := 0; i < flag.NArg(); i++ {
23 if i > 0 {
24 s += Space
25 }
26 s += flag.Arg(i)
27 }
28 if !*omitNewline {
29 s += Newline
30 }
31 os.Stdout.WriteString(s)
32 }
短いプログラムですが、目新しいことがいくつか含まれています。前のhello worldプログラムで関数の前にfuncキーワードを使ったようにvar、const、type(これはまだ使っていません)キーワードもimport同様に宣言を開始するために使用します。
7~10行目、14~17行目のように同じ種類の宣言を丸括弧()内の各行に記述してひとまとめにできることを覚えておいてください。ただし必ずこうすべきという訳ではありません。
const Space = " " const Newline = "\n"
このechoプログラムでは*os.File型のStdout変数にアクセスするために"os"パッケージをインポートしています。このimportステートメントも実際は宣言のひとつです。前の”hello world”で記述したimportの使い方(import fmt "fmt")で説明すると、2つのfmtのうち最初のfmtはプログラムコードがパッケージメンバーにアクセスする際に使われる識別子です。後ろの"fmt"はパッケージをカレントディレクトリもしくは所定の場所から読み込むためのファイル名として使われます。
echoプログラムではimportのときに明示的に名前をつけませんでした。名前をつけないときはパッケージのファイル名がコードからアクセスするときに使用する名前となります。すなわち”hello world”プログラムのほうは、単にimport "fmt"とも書き表せます。
インポートの際、明示的に名前を付けるかどうかは自由です。本当に名前を付けなければならないケースはパッケージ名が競合したときだけです。
osパッケージをインポートしたので、os.Stdout内の文字列を出力するWriteStringメソッドが使えるようになりました。
12行目で インポートしたflagパッケージを使ってechoコマンドの-nフラグを格納するためのグローバル変数を宣言しています。変数omitNewlineは*bool型(=bool型のポインタ)です。
main.main関数の20行目でコマンドラインパラメータの解析(parse)を行い、その次の行で出力する文字列を格納するローカル変数を宣言しています。
変数の宣言は下の形式です。
var s string = ""
var キーワードに続いて、変数名、変数の型、イコール記号、初期値を記述します。
Go言語はシンプルさを目指しており、この宣言文を短くすることができました。文字定数はstring型なので、わざわざそれをコンパイラに伝える必要はありません。次のように書き表せます。
var s = ""
また、さらに短い書き方が出来ます。
s := ""
この:= 演算子は初期値を宣言する際にGo言語で多用されます。 その次の行のfor文でも使われています。
22 for i := 0; i < flag.NArg(); i++ {
flagパッケージはコマンドラインパラメータを解析した際、フラグ以外のパラメータをリスト内に残したままにします。リストからは内部要素を繰り返して取り出す(イテレートする)ことが可能です。
Go言語のforステートメントはC言語と比べると若干違いがあります。まずループ処理を記述できるのはforだけで、whileやdoはありません。次にfor節は丸括弧()で括る必要がありません。ただしループの本体部分は波括弧{}でくくる必要があります。ifやswitchステートメントもこれと同様です。チュートリアルの後ろのほうでは、for文のこれと違う書き方がでてきます。
ループ本体では、+=記号を使って引数と区切りのスペースを文字列sに結合しています。ループを抜けた後は、-nフラグがセットされていなければ改行コードを文字列に加え、最後に作成した文字列を標準出力に書き出しています。
main.mainはパラメータ・戻り値を持たない関数なので、main.main関数の最後まで実行されれば処理が成功したことになります。エラーを返したいときは、下の呼び出しをします。
os.Exit(1)
このosパッケージには、ほかにもスライスのos.Args(flagパッケージがコマンドラインパラメータにアクセスするために使用しています)ど開始時に役立つものが含まれています。
Goの型について
Go言語も他の言語と同じようにintやfloat型といったシステムによってサイズが異なる型を持ってます。他にもサイズを明示したint8やfloat64型もあり、加えてuint、uint32といった符号なし整数型もあります。これらはすべて異なる型であり、たとえint型が32ビットのシステム上であっても、intとint32は同じ型ではありません。一方でbyte型はuint8型の同義語であり、string型の要素です。
string型も同様に言語に組み込まれている型のひとつで、これについて説明します。string型は単なるbyte型の配列ではなく不変であり、一度string型の値を作成すると値は変更できません。あたりまえですが代入によりstring型の値を入れ替えることは可能です。strings.goプログラムから抜き出した下のコードは正しく動作します。
11 s := "hello"
12 if s[1] != 'e' { os.Exit(1) }
13 s = "good bye"
14 var p *string = &s
15 *p = "ciao"
しかし下のコードはstring型の値を変更しようとしているので不正なコードです。
s[0] = 'x'
(*p)[1] = 'y'
Go言語のstring型はC++言語の文字列定数(const string)のようなもので、string型のポインタはconst stringの参照とほぼ同じです。
はい、ここでポインタという言葉が出てきました。Go言語ではポインタの扱いが若干シンプルとなっています。では続きをどうぞ。
配列はこのように宣言します。
var arrayOfInt [10]int
配列はstring型と似ていますが、要素の値は変更可能です。C言語では上の例のarrayOfInt変数をint型のポインタとしても扱えますがGo言語では扱えません。Go言語の配列は値であり、配列のポインタはとても重要(かつ有益)であるため追って説明します。
配列は自分自身でサイズ情報を持っており、また配列からスライス変数をつくることができます。スライスとは同型の配列のポインタを割り当てることができる変数です。より一般的に説明すると、a[low : heigh]形式のスライス式で配列aのlow~high-1までを参照する部分配列が作成されます。スライスは配列とそっくりですが、サイズを明示的に持たず([]と[10]の違い)、作成元である通常の配列の一部分だけを参照しています。配列だけではデータを共有はできませんが、複数のスライスが同一の配列を参照することでデータ共有が可能になります。
スライスは通常の配列より柔軟で、実体への参照であり、かつ効果的であるためGo言語のプログラム内で多用されます。その一方、スライスでは通常の配列のようには細かいメモリ割り当ての制御はできないため、たとえば100要素の配列を構造体内に持たせるようなときは通常の配列を使ってください。
関数に配列を受け渡すときは、たいていはパラメータをスライスとして宣言します。関数を呼び出す側でスライス式を使って配列から必要とする部分だけの参照を作成して(効率的に)関数を呼び出してください。
スライスを使用した関数の例です。(sum.goから)
09 func sum(a []int) int { // intを返す
10 s := 0
11 for i := 0; i < len(a); i++ {
12 s += a[i]
13 }
14 return s
15 }
このsum関数を呼び出すにはこのようにします。
19 s := sum(&[3]int{1,2,3}) // 配列のスライスをsumに渡す
sum関数の戻り値の型はintです。戻り値の型は関数のパラメータリストの直後に記述します。
“[3]int{1,2,3}“の箇所では、角括弧[]式と型を記述して配列を作成しています。この例だとint型3要素を持つ配列が作成されます。さらに&記号を前に書くことでこの配列のインスタンスのアドレスが得られます。このようにsum()関数に配列のポインタを渡すと暗黙的にスライスを渡したことになります。
配列を作成するときに要素数を指定せずコンパイラ側に数えてもらうには、配列のサイズに...を指定します。
s := sum(&[...]int{1,2,3})
実際のところ、データ構造内のメモリ割り当てについて気にとめなければ、角括弧[]を空にして&をつけずにスライスを作成するだけで事足ります。
s := sum([]int{1,2,3})
マップも同様に、次のように初期化できます。
m := map[string]int{"one":1 , "two":2}
sum関数内で初めて出てきた関数len()は組み込み関数で、要素数を返します。このlen関数は文字列、配列、スライス、マップ、チャネルで利用可能です。
話は変わりますが、同様に文字列、配列、スライス、マップ、チャネルに対して機能するrangeについて説明します。これはforループ節で使用します。説明するより見てもらったほうが早いと思います。
for i := 0; i < len(a); i++ { ... }
これはスライス(またはマップなど)の要素数ループを繰り返します。これを下のように書き換えることが出来ます。
for i, v := range a { ... }
この例ではi変数にループのインデックス、v変数にrangeで指定したaの要素が順に代入されます。rangeの使用例は 実践Go言語 をご覧ください。
メモリの割り当てについて
Go言語のほとんどの型は値で、int型、構造体、配列へ代入することはオブジェクトの内容をコピーすることです。新しく変数を割り当てるにはnew()を使用します。new()は割り当てたメモリへのポインタを返します。
type T struct { a, b int }
var t *T = new(T)
より慣用的な書き方をすると
t := new(T)
一方マップ、スライス、チャネル(後で説明)といった型は、他のオブジェクトへの参照です。このスライスやマップの内容を変更すると、その参照先のデータをおなじく参照している別の変数にも影響を及ぼします。この3つの型を使うときは、ビルトイン関数のmake()を使います。
m := make(map[string]int)
上のステートメントは新しいマップを初期化して要素が格納できる状態にします。マップの宣言だけを行うには次のようにします。
var m map[string]int
これは何も要素を持てないnil参照を作成します。マップを使用するには最初にmake()を使用して参照を初期化するか、またはすでに作成済みのマップを代入しなければなりません。
new(T)は*Tを返しますが、make(T)はTを返すので注意してください。new()で参照オブジェクトを(誤って)割り当ててしまうとnil参照へのポインタが返ります。これは初期化していない変数を作り、そのアドレスを取得したことと同じです。
定数について
Go言語の整数(int)型には多くのサイズが用意されていますが、整数の定数は一種類だけです。0LLや0x0ULといった定数はありません。その代わりに整数の定数は大きな桁数を持っているとして扱われ、その値より小さい精度の変数へ代入しようとしたときだけオーバーフローを起こします。
const hardEight = (1 << 100) >> 97 // これは正しいです
Go言語仕様を参照してもらうのが一番ですが、ここにも実例が少しあります。
var a uint64 = 0 // aはuint64型で値は0
a := uint64(0) // 上と等価、変換(conversion)を使用
i := 0x1234 // iはデフォルトのint型
var j int = 1e6 // 正しい(1000000 はintで表現可能)
x := 1.5 // float型
i3div2 := 3/2 // 整数の割り算。値は1
f3div2 := 3./2. // 浮動小数点の割り算。値は1.5
型名と丸括弧()を使った変換は、整数の符号とサイズ、整数と浮動小数点間といった単純なケースにだけ使用できます。定数を変数に代入するときに具体的なサイズと型を持つようになることを除いて、数値の自動変換はGo言語では一切行われません。
I/O パッケージ
次は、open, close, read, writeといったI/O関数を持つシンプルなパッケージを扱ってみましょう。ここからfile.goファイルが始まります。
05 package file
07 import (
08 "os"
09 "syscall"
10 )
12 type File struct {
13 fd int // ファイルデスクプリタ番号
14 name string // Open時のファイル名
15 }
最初の数行は、fileパッケージの宣言と2つのパッケージのインポートです。osパッケージはオペレーションシステム間の様々な差を吸収し、ファイルへの一貫したアクセスを提供します。そのosパッケージのエラーハンドリング機能を使ってファイルI/Oの基本を実装してみましょう。
もうひとつのsyscallパッケージは、オペレーティングシステムを呼び出す低レベルなシステムコールインタフェースを提供します。
次の行では新しく型を定義しています。型の定義はtypeキーワードで始まり、このケースではFileという名前の構造体(struct)を定義しています。少し趣向を凝らし、このFile構造体内にファイルデスクリプタ(fd)と、それの参照先ファイルのファイル名(name)のフィールドを定義しました。
Fileという型の名前が大文字から始まるため、この型はパッケージの外部(パッケージの利用者)から利用することができます。Go言語の情報の可視性についてのルールは単純です。名前(トップレベルの型名、関数名、メソッド名、定数名、変数名、構造体のフィールドおよびメソッド名)の先頭一文字が大文字になっていれば、パッケージの利用者側から参照可能となります。すなわち大文字にしなければ、それが定義されているパッケージ内からしか参照できません。このルールはコンパイラによって実施されるため、絶対的なルールとなっています。この外部パッケージから可視状態であることをGo言語の用語で「エクスポートされた(exported)」と言います。
このFile型の場合、フィールドがすべて小文字なのでパッケージの使用者側からは不可視ですが、後ほど大文字で始まるエクスポートされたメソッドをいくつか追加していきます。
最初はFile型オブジェクトを作成するファクトリ関数です。
17 func newFile(fd int, name string) *File {
18 if fd < 0 {
19 return nil
20 }
21 return &File{fd, name}
22 }
この関数の戻り値は新しく作られたFile構造体のポインタで内部フィールドにファイルデスクリプタとファイル名が格納されます。このコードはマップや配列を作成するときに用いられる書き方に似てますが、これはGo言語の”複合リテラル(composite literal)”という書き方で、これを使ってオブジェクトに新しいヒープ領域を割り当てています。次のような書き方もできます。
n := new(File)
n.fd = fd
n.name = name
return n
File型のような単純な構造体であれば、21行目のように複合リテラルを使ったほうが簡単です。このファクトリ関数を使用して、エクスポートされた*File型の変数を作成します。この変数名はおなじみですね。
24 var ( 25 Stdin = newFile(0, "/dev/stdin") 26 Stdout = newFile(1, "/dev/stdout") 27 Stderr = newFile(2, "/dev/stderr") 28 )
newFileは内部関数なのでエクスポートされません。エクスポートすべきは次のOpen関数です。
30 func Open(name string, mode int, perm int) (file *File, err os.Error) {
31 r, e := syscall.Open(name, mode, perm)
32 if e != 0 {
33 err = os.Errno(e)
34 }
35 return newFile(r, name), err
36 }
このたった数行ですが、いくつかの新しいことが現れました。最初に目に付くのは、Open関数がfileとerrという複数の戻り値をもっていることです。(エラーについてはあとで説明します)丸括弧()で括った宣言リストで複数の戻り値を宣言します。構文的には引数リストが2個あるように見えます。31行目のsyscall.Open関数も複数の戻り値を返すので、変数を複数宣言して戻り値を受け取っています。戻り値を受け取る変数rとeはともにint型です。(syscallパッケージを要確認) 最後に35行目で、Fileのポインタとエラーの2つの値を返しています。syscall.Openがエラーとなったときは、ファイルデスクリプタrの値はマイナス値となり、newFile関数はnilを返します。
エラーについて説明します。osパッケージライブラリは包括的なエラーの概念を含んでいます。この例のように独自のインタフェースにも共通のエラーハンドリング方法を取り入れるのはコードの一貫性という意味でよい方法です。Openメソッドでは変換を利用してUnixのerrno値を整数型のos.Errnoに変換し、os.Error型を得ています。
File型オブジェクトが作成できるようになったので、つづいてメソッドを追加していきます。型にメソッドを宣言するには、定義する関数名の直前の括弧内に、レシーバを型を明示して記述します。下に記述する*Fileの各メソッドではそれぞれfileレシーバ変数を宣言しています。
38 func (file *File) Close() os.Error {
39 if file == nil {
40 return os.EINVAL
41 }
42 e := syscall.Close(file.fd)
43 file.fd = -1 // 再度クローズされないように
44 if e != 0 {
45 return os.Errno(e)
46 }
47 return nil
48 }
50 func (file *File) Read(b []byte) (ret int, err os.Error) {
51 if file == nil {
52 return -1, os.EINVAL
53 }
54 r, e := syscall.Read(file.fd, b)
55 if e != 0 {
56 err = os.Errno(e)
57 }
58 return int(r), err
59 }
61 func (file *File) Write(b []byte) (ret int, err os.Error) {
62 if file == nil {
63 return -1, os.EINVAL
64 }
65 r, e := syscall.Write(file.fd, b)
66 if e != 0 {
67 err = os.Errno(e)
68 }
69 return int(r), err
70 }
72 func (file *File) String() string {
73 return file.name
74 }
暗黙的なthisはなく、構造体のメンバにアクセスするためにはレシーバ変数を使用しなければなりません。メソッドは構造体内で宣言するのではありません。構造体の宣言内で定義するのはデータメンバだけです。実はメソッドは構造体にだけではなく整数または配列などほぼすべての型に作成することができます。配列を使った例は後で記述します。
Stringメソッドは文字出力でよく使用されるメソッドで、後ほど解説します。
これらのメソッドはUnixのエラーコードEINVAL(これをos.Errorに変換した値)を返すためパブリック変数os.EINVALを使用しています。osパッケージライブラリにはこのような標準的なエラー値が定義されています。
今回は新しいパッケージを使います。
05 package main
07 import (
08 "./file"
09 "fmt"
10 "os"
11 )
13 func main() {
14 hello := []byte("hello, world\n")
15 file.Stdout.Write(hello)
16 file, err := file.Open("/does/not/exist", 0, 0)
17 if file == nil {
18 fmt.Printf("can't open file; err=%s\n", err.String())
19 os.Exit(1)
20 }
21 }
インポートの”./file“の”./“の部分は、コンパイラに対しパッケージがインストールされているディレクトリからインポートするのではなく、このmainパッケージ自身のディレクトリからインポートするように指示しています。
最後にプログラムを実行してみましょう。
% helloworld3
hello, world
can't open file; err=No such file or directory
%
catコマンドの実装例
前回作成したfileパッケージを基にして、Unixユーティリティのcat(1)の簡易バージョンを作成します。このファイルはprogs/cat.goにあります。
05 package main
07 import (
08 "./file"
09 "flag"
10 "fmt"
11 "os"
12 )
14 func cat(f *file.File) {
15 const NBUF = 512
16 var buf [NBUF]byte
17 for {
18 switch nr, er := f.Read(&buf); true {
19 case nr < 0:
20 fmt.Fprintf(os.Stderr, "cat: error reading from %s: %s\n", f.String(), er.String())
21 os.Exit(1)
22 case nr == 0: // EOF
23 return
24 case nr > 0:
25 if nw, ew := file.Stdout.Write(buf[0:nr]); nw != nr {
26 fmt.Fprintf(os.Stderr, "cat: error writing from %s: %s\n", f.String(), ew.String())
27 }
28 }
29 }
30 }
32 func main() {
33 flag.Parse() // パラメータリストを調べてflagに設定
34 if flag.NArg() == 0 {
35 cat(file.Stdin)
36 }
37 for i := 0; i < flag.NArg(); i++ {
38 f, err := file.Open(flag.Arg(i), 0, 0)
39 if f == nil {
40 fmt.Fprintf(os.Stderr, "cat: can't open %s: error %s\n", flag.Arg(i), err)
41 os.Exit(1)
42 }
43 cat(f)
44 f.Close()
45 }
46 }
すでにこのコードを理解できるようになっていると思いますが、switchステートメントには説明すべき箇所があります。forループと同じく、ifやswitchには初期化ステートメントを含めることができます。18行目のswitchではf.Read()の戻り値をnrとer変数に、一度に代入しています。(25行目のifも同様です) swtichでは通常、値と一致するまでcaseを先頭からひとつずつ順に評価していきますが、caseに記述する式はswitchで指定した値と同じ型でありさえすれば定数や数値である必要はありません。
このswitchの値にはtrueが指定されています。このように値がtrueのときだけは値の記述は省略可能です。(forステートメントと同じです) 値が記述されていないときはtrueとして扱われます。このようなswitchステートメントは、if - elseステートメントを連ねたのと同じです。またswtichステートメントのcaseには暗黙的なbreakがあることをここに明記しておきます。
25行目では、入力バッファをスライスしてWrite()を呼び出しています。Go言語でI/Oバッファを取り扱うときは通常スライスを使用します。
これから入力をrot13変換するオプション機能をもったcatの亜種を作成してみましょう。rot13とはアルファベットを一文字毎に13文字後のアルファベットに置き換えてつくる簡単な暗号です。(参考文献:Wikipedia) これはバイト処理で行えば簡単ですが、すこし回り道してGo言語のインタフェース(interface)を理解していきましょう。
cat()サブルーチンではf.Read()とString()の2つのメソッドしか使いませんでした。まずはそれらの2つのメソッドとまったく同じメソッド持つインタフェースを定義することから始めましょう。このコードは、progs/cat_rot13.goファイルにあります。
26 type reader interface {
27 Read(b []byte) (ret int, err os.Error)
28 String() string
29 }
readerインタフェースのこの2つのメソッドを持つ型であれば(たとえ他のメソッドを有していても)このインタフェースを実装していることになります。すなわちfile.Fileはこの2つのメソッドをすでに実装しているので、readerインタフェースを実装していることになります。catサブルーチンを少し変更し、引数を*file.Fileの代わりにreaderを受け取るようにするだけでもちゃんと動作しますが、せっかくなのでreaderを実装する別の型を記述してみましょう。新しく作成する型では別のreaderをラップし、そのreaderから取り出したデータにrot13変換を行います。型を定義しreaderインタフェースの2つのメソッドだけ記述すると、readerインタフェースの2個目の実装ができあがります。
31 type rotate13 struct {
32 source reader
33 }
35 func newRotate13(source reader) *rotate13 {
36 return &rotate13{source}
37 }
39 func (r13 *rotate13) Read(b []byte) (ret int, err os.Error) {
40 r, e := r13.source.Read(b)
41 for i := 0; i < r; i++ {
42 b[i] = rot13(b[i])
43 }
44 return r, e
45 }
47 func (r13 *rotate13) String() string {
48 return r13.source.String()
49 }
50 // rotate13の実装はここまで
(42行目で呼び出しているrot13関数は、取るに足らないので省略しています)
新しく追加した機能を利用するためにflagを定義します。
14 var rot13Flag = flag.Bool("rot13", false, "rot13 the input")
さらに、これを使うためにcat()関数を少しだけ変更します。
52 func cat(r reader) {
53 const NBUF = 512
54 var buf [NBUF]byte
56 if *rot13Flag {
57 r = newRotate13(r)
58 }
59 for {
60 switch nr, er := r.Read(&buf); {
61 case nr < 0:
62 fmt.Fprintf(os.Stderr, "cat: error reading from %s: %s\n", r.String(), er.String())
63 os.Exit(1)
64 case nr == 0: // EOF
65 return
66 case nr > 0:
67 nw, ew := file.Stdout.Write(buf[0:nr])
68 if nw != nr {
69 fmt.Fprintf(os.Stderr, "cat: error writing from %s: %s\n", r.String(), ew.String())
70 }
71 }
72 }
73 }
56~58行目でrot13フラグがtrueのときは引数で受け取ったreaderをrotate13内にラップさせ入れ替えています。このラッピングさせる処理をmain関数内で行えば、cat関数は引数の型以外は変更しなくてもよかったのですがこれも学習と思ってください。
ここで気をつける点は、インタフェース変数はポインタではなく値であることです。内部的に構造体のポインタを持っていますが、受け取る引数の型はreaderであり*readerではありません。
実行してみます。
% echo abcdefghijklmnopqrstuvwxyz | ./cat
abcdefghijklmnopqrstuvwxyz
% echo abcdefghijklmnopqrstuvwxyz | ./cat --rot13
nopqrstuvwxyzabcdefghijklm
%
このようにインタフェースを使ってファイルデスクリプタの実装が入れ替えられるので、依存性注入(dependency injection)も簡単に行えます。
このインタフェースは他の言語にはないGo言語の特徴です。インタフェース内に定義されている全てのメソッドを実装した型は自動的にそのインタフェースを実装したことになります。すなわち型は複数の異なるインタフェースを実装可能ということです。Go言語の型には階層はありませんが、その代わりインタフェースを使用することでrot13で行ったようにプログラムのアドホックな変更が容易に行えます。file.File型はreaderインタフェースを実装していますが、writerインタフェース、その他のインタフェースを必要に応じて実装可能です。
空インタフェースについて考えてみましょう。
type Empty interface {}
すべての型は空インタフェース(interface {})を実装しています。そのため、空インタフェースはコンテナの類を作るときに役立ちます。
ソート
Go言語のインタフェースはポリモーフィズムを提供します。ポリモーフィズムにより、オブジェクトのふるまいと実際の処理を完全に分離できるので、同一インタフェースを実装していても型が異なるオブジェクトを、ひとつの変数を使って(代入しなおす必要はありますが)アクセスすることができます。
下の簡単なソートアルゴリズムの例をみてください。このファイルはprogs/sort.goにあります。
13 func Sort(data Interface) {
14 for i := 1; i < data.Len(); i++ {
15 for j := i; j > 0 && data.Less(j, j-1); j-- {
16 data.Swap(j, j-1)
17 }
18 }
19 }
SortのInterfaceに必要なのは3つのメソッドだけです。
07 type Interface interface {
08 Len() int
09 Less(i, j int) bool
10 Swap(i, j int)
11 }
Sort関数には、このLen、Less、Swapメソッドをさえ実装していればどんな型でも渡すことができます。sortパッケージには、整数値や文字列の配列をソートするのに必要なメソッドが用意されています。下は、int型の配列をソートするコードです。
33 type IntArray []int
35 func (p IntArray) Len() int { return len(p) }
36 func (p IntArray) Less(i, j int) bool { return p[i] < p[j] }
37 func (p IntArray) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
構造体ではない型に対してメソッドを定義していることがお分かりいただけるでしょうか。どんな型であっても、それが定義されたパッケージ内であればメソッドを定義することができます。
これをテストするためのルーチンです。これはprogs/sortmain.goファイルにあります。ここではコードを省略するためsortパッケージのIsSortedメソッドを使って実際にソートされているか確認しています。
12 func ints() {
13 data := []int{74, 59, 238, -784, 9845, 959, 905, 0, 0, 42, 7586, -5467984, 7586}
14 a := sort.IntArray(data)
15 sort.Sort(a)
16 if !sort.IsSorted(a) {
17 panic("fail")
18 }
19 }
新しい型をソート可能にするには、このようにその型に3つのメソッドを実装するだけです。
30 type day struct {
31 num int
32 shortName string
33 longName string
34 }
36 type dayArray struct {
37 data []*day
38 }
40 func (p *dayArray) Len() int { return len(p.data) }
41 func (p *dayArray) Less(i, j int) bool { return p.data[i].num < p.data[j].num }
42 func (p *dayArray) Swap(i, j int) { p.data[i], p.data[j] = p.data[j], p.data[i] }
文字出力
これまでは文字列のフォーマット出力は、あまり使ってきませんでした。このセクションではGo言語の文字列フォーマットI/Oがどのように動いているか解説します。
いままでのサンプルコードの中では簡単な使い方しかしていなかったfmtパッケージですが、このパッケージにはPrintf, Fprintfなどの関数が実装されています。fmtパッケージ内ではPrintfが次のシグネチャーで宣言されています。
Printf(format string, v ...interface{}) (n int, errno os.Error)
トークン...は、可変長変数リストの指定に使います。C言語では、stdarg.hマクロを使って扱います。Go言語では、可変引数関数は、指定した型の引数のスライスとして関数に渡されます。Printfでは、...interface{}と宣言されているので、受け取る型は、空インタフェースの値のスライス([]interface{})です。Printfでは、スライスをイテレートすることで、引数を調べることができ、各要素に対し、型スイッチやリフレクションライブラリを使って、値を解釈します。話がそれますが、このような実行時の解析処理は、Go言語のPrintfの優位性を説明するのに一役買っています。Printfには、引数の型を動的に調査する能力があるためです。
たとえば、C言語ではフォーマットと受け取ったパラメータとの型が一致している必要がありますが、Go言語ではほとんどの場合もっと簡単です。フォーマット%lludの代わりに%dとだけ記述すれば、Printfは整数のサイズ・符号を判断して一番適したフォーマットで出力します。
下は簡単な例です。
10 var u64 uint64 = 1<<64-1
11 fmt.Printf("%d %d\n", u64, int64(u64))
その出力です。
18446744073709551615 -1
もしフォーマットについて考えるのが面倒なら%vを使ってください。どんな値(配列や構造体でさえ)であってもそれに最適な書式で出力します。次はその例です。
14 type T struct {
15 a int
16 b string
17 }
18 t := T{77, "Sunset Strip"}
19 a := []int{1, 2, 3, 4}
20 fmt.Printf("%v %v %v\n", u64, t, a)
その出力です。
18446744073709551615 {77 Sunset Strip} [1 2 3 4]
もしPrintfの代わりにPrintまたはPrintlnを使用するのであれば、フォーマットの指定は全く不要です。これらのルーチンは完全自動でフォーマットを行います。Print関数はパラメータで指定された値を%vフォーマットを使ったときと同じように出力します。一方Println関数はパラメータの間にスペースを、そして最後に改行コードを挿入します。下の2行の出力内容は、前回のPrintfの呼び出しと同じ結果になります。
21 fmt.Print(u64, " ", t, " ", a, "\n") 22 fmt.Println(u64, t, a)
自分で作った型をPrintfまたはPrintでフォーマットしたいときは、文字列を返すString()メソッドを実装してください。printルーチンは、出力しようとする値がString()メソッドを実装しているか調べ、実装していればそれを使います。ここに分かりやすい例があります。
09 type testType struct {
10 a int
11 b string
12 }
14 func (t *testType) String() string {
15 return fmt.Sprint(t.a) + " " + t.b
16 }
18 func main() {
19 t := &testType{77, "Sunset Strip"}
20 fmt.Println(t)
21 }
*testTypeはString()メソッドを持っているので、この型のデフォルトのフォーマット処理としてString()メソッドが出力時に使われます。
77 Sunset Strip
このString()メソッド内でフォーマットのためにSprint関数を呼び出しているように、自作のフォーマット処理から他のフォーマット処理を呼び出すこともできます。
Printfのもう一つの特徴として、フォーマット%Tは値そのものではなく型情報を文字列に変換します。これはポリモーフィックなコードをデバッグするときに役立ちます。
フラグや精度情報などを持った完全自作のフォーマット処理を記述することは可能ですが、このチュートリアルの範囲からはずれているためここでは説明しません。
ある型がString()メソッドを実装しているかどうかをPrintfがどのように判断しているかというと、値が『String()メソッドを実装したインタフェース』に変換可能かを調べることで判定しています。
仮に変数vがあったとして、次のように判断しています。
type Stringer interface {
String() string
}
s, ok := v.(Stringer) // vが"String()"を実装しているか調べる
if ok {
result = s.String()
} else {
result = defaultOutput(v)
}
このコードでは、型アサーション(type assertion)を使用し、v変数の値がStringerインタフェースを実装しているか調べています。v.(Stringer)の箇所が型アサーションです。型アサーションに成功すると、指定したインタフェース型に変換されたインスタンスがs変数に設定され、ok変数にはtrueが設定されます。その後、変換されたs変数を使用してメソッドにアクセスします。(「カンマ+ok」という書き方は、型変換、マップ更新、通信処理において、処理が成功したかどうかを確認するときによく使われるGo言語のイディオムですが、チュートリアル内で使用しているのはここだけです) 値がインタフェースを実装していないときは、ok変数にはfalseが設定されます。
このStringerのコードのように、インタフェースに名前をつける際、メソッド名にer( またはr)をつけるというのがGo言語の習わしです。
最後の話題です。Printf系、Sprintf系がでてきましたが、当然Fprintf系もあります。C言語と異なりFprintfの第1パラメータはファイルではなく、ioパッケージで定義されているio.Writerインタフェースです。
type Writer interface {
Write(p []byte) (n int, err os.Error)
}
このインタフェースは前出の習わしに従った名前です。WriteメソッドからWriterインタフェースと名づけられています。io.Reader、io.ReadWriterの名前も同じルールで名づけられています。このように、ファイルだけでなく、ネットワークチャネル、バッファなど標準Writeメソッドを実装していればどんな型を使用してもFprintfを呼び出すとができます。
素数
やっと、プロセスと平行通信プログラミングのところまで到達しました。これは重要なセクションであり、このトピックにはある程度の知識が必要です。
「素数の篩(ふるい)」と呼ばれる古くから存在する素数を取得するプログラムがあります。(エラトステネスの篩はここで扱うアルゴリズムより効率的ではありますが、このセクションでは並列性を重要視しています) このアルゴリズムでは、まず2以上の自然数を順番に並べて数列を作成します。そこから素数をひとつづ取り出し、その素数の倍数すべてを数列から除外することを繰り返します。残った数列の先頭から取り出した数が次の素数であり、そこからまた倍数をすべて除外して次の数列を作成します。
下が、これのフロー図です。それぞれの箱は数列の先頭から取り出した素数であり、この箱で数列をフィルタリングして次の数列を作成しています。 
整数の数列を作成するためにチャネルと呼ばれるGo言語の機能を使います。チャネルの基となったものはCSPであり、チャネルとは2つの並列動作している処理をつなぐ通信チャネルです。Go言語のチャネル変数は”通信を調整するランタイムオブジェクト”への参照です。新しいチャネルをつくるには、マップやスライスと同様にmakeを使用してください。
下はprogs/sieve.go内の関数です。
09 // チャネル'ch'に、2, 3, 4, ... という連番を送信
10 func generate(ch chan int) {
11 for i := 2; ; i++ {
12 ch <- i // 'i'をチャネル'ch'に送信
13 }
14 }
このgenerate関数は、2,3,4,5,…という一連の値をパラメータchに送信します。送信には二項演算子<-を使用します。チャネルの送信ではch変数を受信する側がいなければブロックされ、誰かが受信状態になるまで送信を保留します。
次のfilter関数では3つのパラメータ、入力チャネル・出力チャネル・素数を受け取り、入力チャネルの値を出力チャネルにコピーします。そのとき素数で割り切れた値は除外します。受信に使用する単項演算子<-は、チャネル上から次の値を取り出します。
16 // チャネル'in'からチャネル'out'に値をコピー
17 // そのとき'prime'で割り切れる値を取り除く
18 func filter(in, out chan int, prime int) {
19 for {
20 i := <-in // 新しい変数'i'に'in'から値を受信する
21 if i % prime != 0 {
22 out <- i // 'i'をチャネル'out'に送信する
23 }
24 }
25 }
generator関数とfilters関数は平行動作します。Go言語は独自のプロセス/スレッド/軽量プロセス/コルーチンモデルを持っていますが、表記上の混乱を避けるためGo言語の平行実行処理をゴルーチン(goroutines)と呼ぶことにしています。ゴルーチンを開始するためには、キーワードgoを前に付けて関数を呼び出します。こうすると呼び出した関数はカレントの処理と同じアドレス空間のまま平行動作を開始します。
go sum(hugeArray) // sumの計算をバックグラウンドで行う
計算が終了したことを検知したいときは、つぎのようにして処理終了を通知するチャネルを関数に渡してください。
ch := make(chan int)
go sum(hugeArray, ch)
// ... しばらくの間、何かを行う
result := <-ch // 結果を受信するまで待つ
素数の篩に話を戻しましょう。下のようにして篩パイプラインを組み合わせます。
28 func main() {
29 ch := make(chan int) // 新しいチャネルの作成
30 go generate(ch) // generate()をゴルーチンとして開始
31 for {
32 prime := <-ch
33 fmt.Println(prime)
34 ch1 := make(chan int)
35 go filter(ch, ch1, prime)
36 ch = ch1
37 }
38 }
29行目でgenerate関数に渡すチャネルを作成、初期化しています。チャネルから素数をひとつ取り出し、新たにfilterをパイプラインに加えて、そこからの出力でch変数を置き換えます。
この篩プログラムは、共通プログラミングパターンを使って書き換えられます。下のコードはgenerate関数を変更したバージョンです。このファイルはprogs/sieve1.goにあります。
10 func generate() chan int {
11 ch := make(chan int)
12 go func(){
13 for i := 2; ; i++ {
14 ch <- i
15 }
16 }()
17 return ch
18 }
このバージョンでは、関数内ですべてのセットアップを行っています。まず出力チャネルをつくり、次にリテラル関数をゴルーチンとして起動して呼出元にチャネルを返します。これはすなわちゴルーチンを起動しその接続を返すという、並列処理のためのファクトリです。
リテラル関数表記(12-16行目)によって、匿名の関数をつくり即座に呼び出しています。注意していただきたいのは、ローカル変数chはリテラル関数からもアクセス可能で、generate関数から抜けたあとも残り続けることです。同じ変更をfilter関数にも適用します。
21 func filter(in chan int, prime int) chan int {
22 out := make(chan int)
23 go func() {
24 for {
25 if i := <-in; i % prime != 0 {
26 out <- i
27 }
28 }
29 }()
30 return out
31 }
その結果、sieve関数内のメインループはよりシンプルで処理が明瞭になりました。これも同様にファクトリに変更します。
33 func sieve() chan int {
34 out := make(chan int)
35 go func() {
36 ch := generate()
37 for {
38 prime := <-ch
39 out <- prime
40 ch = filter(ch, prime)
41 }
42 }()
43 return out
44 }
その結果、メイン関数とsieve関数との受け渡しはprimesチャネルだけになりました。
46 func main() {
47 primes := sieve()
48 for {
49 fmt.Println(<-primes)
50 }
51 }
多重化
チャネルを使うことで多重化処理のコードをほとんど書くことなく、複数の独立しているゴルーチンに対してデータ配信することができます。今回のポイントとしては、サーバーへ送信するメッセージ内にあらかじめ返信用チャネルを含めておき、それを使ってクライアントに返信を行います。実際のクライアント/サーバプログラムは多くのコードからできていますが、ここでは分かりやすいように単純化した例で説明します。はじめにrequest型を定義し、そこに返信に使用するチャネルを埋め込みます。
09 type request struct {
10 a, b int
11 replyc chan int
12 }
サーバ側は大した処理は行いません。整数の二項演算をするだけです。下のコードは演算を実行し、requestに返信します。
14 type binOp func(a, b int) int
16 func run(op binOp, req *request) {
17 reply := op(req.a, req.b)
18 req.replyc <- reply
19 }
14行目では関数の「型」としてbinOpという名前を定義しています。この関数はパラメータに2つの整数を取り、整数を返します。
server関数は無限ループしています。リクエストを受け取るたびに処理を行いますが、処理に時間がかかるとループ内でブロックしてしまうため、ゴルーチンを起動して処理を行わせます。
21 func server(op binOp, service chan *request) {
22 for {
23 req := <-service
24 go run(op, req) // 待たない
25 }
26 }
おなじみの方法でserverを作成・起動後、それと接続しているチャネルを返します。
28 func startServer(op binOp) chan *request {
29 req := make(chan *request)
30 go server(op, req)
31 return req
32 }
次は簡単なテストコードです。「加算処理を行う関数」をfuncステートメントで作成し、それをパラメータにしてsartServerを呼び出してサーバを起動します。つづいてN個のリクエストを連続して送信します。すべてのリクエストを送信し終えてから応答を確認します。
34 func main() {
35 adder := startServer(func(a, b int) int { return a + b })
36 const N = 100
37 var reqs [N]request
38 for i := 0; i < N; i++ {
39 req := &reqs[i]
40 req.a = i
41 req.b = i + N
42 req.replyc = make(chan int)
43 adder <- req
44 }
45 for i := N-1; i >= 0; i-- { // 順序は関係ありません
46 if <-reqs[i].replyc != N + 2*i {
47 fmt.Println("fail at", i)
48 }
49 }
50 fmt.Println("done")
51 }
このプログラムの問題点はサーバがちゃんとシャットダウンしないということです。mainからリターンするときに処理に時間がかかってしまったゴルーチンがあると通信をブロックしてしまいます。これを解決するために別のチャネルquitをserver関数に渡します。
32 func startServer(op binOp) (service chan *request, quit chan bool) {
33 service = make(chan *request)
34 quit = make(chan bool)
35 go server(op, service, quit)
36 return service, quit
37 }
server関数にquitチャネルをを渡します。server関数では受け取ったチャネルを次のようにして使います。
21 func server(op binOp, service chan *request, quit chan bool) {
22 for {
23 select {
24 case req := <-service:
25 go run(op, req) // 待たない
26 case <-quit:
27 return
28 }
29 }
30 }
server内のselectステートメントは、caseでリストされた複数の通信のうち処理が続行可能な方を選択します。すべてがブロックされていれば、どれかが続行可能になるまで待機します。どちらも続行可能ならどちらかがランダムに選択されます。この場合、selectはquitメッセージを受信しリターンするまでの間、requestメッセージを受信し続けます。
あとはmain関数の最後にquitチャネルにデータを送信するよう変更します。
40 adder, quit := startServer(func(a, b int) int { return a + b })
…
55 quit <- true
まだまだGo言語のプログラミングおよび同期処理について語るところはありますが、このチュートリアルは基本の説明だけにとどめておきましょう。
Trackback URL
Comments