本章では、Goの基本的な文法について解説します。
序盤は1章で実行したhello worldのプログラムを振り返ってみましょう。
mainパッケージ
Goのコードはパッケージの宣言から始まります。
プログラムをコンパイルして実行すると、まずmainパッケージの中にあるmain()
関数が実行されるため、ここに処理を記述しています。関数の詳細は後述します。
インポート
importは、プログラム内にほかのパッケージを取り込むために使用します。
インポートしたパッケージ内へは、パッケージ名にドットを付けてアクセスできます。たとえば上記では、出力にfmtパッケージのPrintln()
を使用しています。
複数パッケージの取り込み
複数のパッケージを取り込む場合は次のように縦に並べて記述します。
Goの標準パッケージはインストールしたGoの中に含まれているため自動でパスが解決され、それ以外のパッケージは1章で解説したようにGOPATH
環境変数からパスを解決します。
オプションの指定
インポートするパッケージ名の前には、いくつかのオプションが指定できます。
任意の名前(上記ではf
)を指定した場合は、プログラム内でのパッケージ名を変えることができます。
_
を付けた場合は、インポートしたパッケージを使用しないことをコンパイラに伝えます。第1章で解説したように、Goはインポートして使用しないパッケージがあるとコンパイルエラーになります。上記の例ではgithub.com/wdpress/gosampleパッケージはインポートして使っていなかったので、_が付いていなかったらコンパイルエラーになります。
.
を付けた場合は、使用時にパッケージ名が省略可能になります。
組込み型
さて、hello worldプログラムではhello world
という文字列を出力しましたが、この文字列はGoではstringという組込み型として扱われます。ほかにもGoには表1の組込み型があります。
表1 Goの組込み型
型 | 説明 |
uint8 | 8ビット符号なし整数 |
uint16 | 16ビット符号なし整数 |
uint32 | 32ビット符号なし整数 |
uint64 | 64ビット符号なし整数 |
int8 | 8ビット符号あり整数 |
int16 | 16ビット符号あり整数 |
int32 | 32ビット符号あり整数 |
int64 | 64ビット符号あり整数 |
float32 | 32ビット浮動小数 |
float64 | 64ビット浮動小数 |
complex64 | 64ビット複素数 |
complex128 | 128ビット複素数 |
byte | uint8のエイリアス |
rune | Unicodeのコードポイント |
uint | 32か64ビットの符号なし整数 |
int | 32か64ビットの符号あり整数 |
uintptr | ポインタ値用符号なし整数 |
error | エラーを表わすインタフェース |
runeは、Unicodeのコードポイントを指します。実際は、ほかの言語で言うcharのように1文字を表すことに使用します。stringがダブルクオートでくくられるのに対し、runeはシングルクオートでくくられます。またバッククオートで囲むことで、複数行に渡るstring(ヒアドキュメント)を記述できます。
変数
次は変数について説明します。例としてhello worldプログラムの出力メッセージを、message
というstringの変数に代入してみます。
変数の宣言は、var
で始まり、次に変数名、最後が型です。型と変数名の順番を珍しく思うかもしれませんが、これがGoの特徴の一つです。
一度に複数の宣言と初期化
一度に複数宣言する場合は次のように記述することもできます。
一度に多くの変数を同じ型で宣言する場合は、var
と2つ目以降の型を省略して、次のように書くこともできます。
関数内部での宣言と初期化
変数宣言と初期化を関数の内部で行う場合は、var
と型宣言を省略し、:=
という記号を用いることができます。
この書き方をした場合、変数の型は代入する値からコンパイラによって推論されます。今回は文字列を代入していることから、変数message
の型がstringであると推論されます。
定数
変数宣言のvar
をconst
に変えると定数になります。
定数宣言できる型は、組込み型のうちerror以外の型です。定数に対する再代入はコンパイルエラーになるため、実行前にミスを発見できます。
ゼロ値
変数を宣言し、明示的に値を初期化しなかった場合、変数はゼロ値というデフォルト値で初期化されます。ゼロ値は型ごとに決まっており、たとえばintのゼロ値は0
であるため、次のコードは0
を出力します。
型ごとのゼロ値は表2のようになります。
表2 型ごとのゼロ値
型 | ゼロ値 |
整数型 | 0 |
浮動小数点型 | 0.0 |
bool | false |
string | "" |
配列 | 各要素がゼロ値の配列 |
構造体 | 各フィールドがゼロ値の構造体 |
そのほかの型 | nil(※) |
※ nilは値がないことを表す値。ほかの言語におけるnullなどに相当する
if
Goでは、if文の条件部に丸括弧は必要ありません。
なお、if文の処理が1行の場合に波括弧を省略可能な言語もありますが、Goではそうした省略はコンパイルエラーになります[1]。
また、三項演算子はないため次のような書き方はできません。if/else文を使用します。
Goではこうした多様な書き方を認めないことで、言語の仕様を小さく保つ方針を採っています。
for
Goのfor文では、CやJavaでは必要な条件部の丸括弧が必要ありません。
Goで繰り返しを表現する方法はfor文以外になく、ほかの言語におけるwhile文やdo/while文、loop文といったような表現は、すべてfor文を用いて行います。
whileもforで
たとえばCのwhile文は、for文で次のように表現します。
無限ループ
Cの場合、for文を使って次のように無限ループを表現します。
Goでは、for文の条件部を省略することで同様の表現ができます。
break、continue
繰り返し制御にはCやJavaと同様に、ループを終了するbreak
、ループの最初から処理を再開するcontinue
を使用できます。
switch
if/else文が繰り返す場合は、switch文を用いたほうがスッキリ書ける場合があります。Goのswitch文は非常に柔軟であり、値の比較だけでなく条件分岐にも使用できます。
値での分岐
まず、値を用いたswitch文は次のようになります。
switch 文に指定した値に一致するcase
が実行され、どのcase
にも一致しなかった場合はdefault
が実行されます。case
には単一の値だけでなく、カンマで区切った複数の値も指定できます。
fallthrough
CやJavaなどのswitch文は、1つのcase
が実行されるとその次のcase
に処理が移るため、単一のcase
の実行で終わらせたい場合に、case
ごとにbreak
を書く必要がありました。しかしGoのswitch文では、case
が1つ実行されると次のcase
に実行が移ることなくswitch文が終了するため、break
をいちいち書く必要はありません。
ただ、case
の処理が終わったあとに、次のcase
に処理を進めたい場合もあります。そうした場合はcase
内にfallthrough
を書くことで、明示的に次のcase
に処理を進めることができます。
式での分岐
Goのswitch文では、case
に値だけでなく式も指定できます。
たとえば上記のようにcase
に式を指定すれば、評価結果がtrue
になるcase
が実行でき、if/else 文の代わりに使用できます。
また、値、式以外にtype
(型)を用いたswitch文もありますが、これについては3章で解説します。
関数
関数はfunc
で始まります。引数も戻り値もない場合は次のように宣言します。
引数がある場合は変数と型を指定します。複数の同じ型が続く場合は、型の宣言は最後の1つにまとめることができます。
戻り値がある場合は引数の次に指定します。
関数は複数の値を返せる
Goの大きな特徴の一つとして、関数は複数の値を返すことができます。戻り値が複数の場合は、型をカンマで区切って指定し丸括弧でくくります。return
はそれに対応した型の値を、同じくカンマで区切って返します。
関数の実行時には、戻り値を格納する変数を必要な数だけ用意する必要があります。関数が返す値の数と、受け取る変数の数が合わないとコンパイルエラーになります。ただし、無視したい戻り値がある場合は_で明示的に無視することで、戻り値を受け取らなくてもコンパイル、実行ともに可能です。
エラーを返す関数
Goでは、関数が多値を返せることを利用して、内部で発生したエラーを戻り値で表現します。関数の処理に成功した場合はエラーはnil
にし、異常があった場合はエラーだけに値が入り、他方はゼロ値になります。
たとえばファイルを開くos.Open()
は、1つ目の戻り値に*os.File
、2つ目にerror
を返します。
自作のエラーは、errorsパッケージを用いて作ることができます。
複数の値を返す場合もエラーを最後にする慣習があるため、自分でAPIを設計する場合もエラーを最後にするほうがよいでしょう。
異常を戻り値で表現できない場合については、後述のパニックとリカバで解説します。
名前付き戻り値
Goでは、戻り値にあらかじめ名前を付けることができます。先ほどの関数の戻り値に、次のように名前を付けてみます。
名前付き戻り値は、関数内ではゼロ値で初期化された変数として扱うことができます。また、変数に名前を付けている場合は、return
のあとに返す値を明示する必要がなく、return
された時点での名前付き戻り値の値が自動的に返されることになります。
これを用いると、先の関数は次のように書くことができます。
名前付き戻り値を用いることで、関数の宣言から戻り値の意味が読み取りやすくなると同時に、戻り値のための変数の初期化が不要になり、同じ型の戻り値が多かった場合のreturn
の書き間違えなどを防ぐこともできます。
ただし、戻り値に名前を付けても、return
のあとに戻す値を明示することは可能です。プログラムのわかりやすさを重視して使い分けるとよいでしょう。
関数リテラル
関数リテラルを用いると、無名関数を作ることができます。即時に実行する関数は次のように記述できます。
Goにおける関数は第一級オブジェクトであるため、関数を変数に代入したり関数の引数に渡すことができます。たとえば、上記の関数を代入する変数の型は次のようになります。
配列
Goの配列は固定長です。可変長配列は後述するスライスがそれにあたります。たとえば長さが4で要素の型がstringである配列は、次のように宣言します。
配列は、ほかの言語同様に添字でアクセスします。
宣言と同時に初期化することも可能で、その場合は[...]
を用いることで、必要な配列の長さを暗黙的に指定できます。
配列の型は長さも情報として含むため、次のarr1
とarr2
は、要素の型は同じstringですが、長さが違うため配列としては別の型です。関数fn
は[4]string
型を引数にとるため、型の合わないarr2
を渡すとコンパイルエラーになります。
また、関数に配列を渡す場合は値渡しとなり、配列のコピーが渡されます。次のfn()
の中で配列に対して行った変更は、main()
側には反映されません。
スライス
スライスは、可変長配列として扱うことができます。配列を直接使うのは、シビアなメモリ管理が必要な一部のプログラムだけなので、同じ性質のデータを束ねて扱うという用途であれば、基本的にはスライスを用います。
なお、スライスの詳細な内部構造については筆者のブログ記事を参照してください。
スライスの宣言
stringのスライスは次のように宣言します。
このように、スライスの型には配列のように長さの情報はありません。
初期化を同時に行う場合は、配列と同じように書くことができます。またスライスも、配列同様に添字でアクセスできます。
append()
スライスの末尾に値を追加する場合はappend()
を使用します。append()
は、スライスの末尾に値を追加し、その結果を返す組込み関数です。複数の値を追加することもできます。
次のように指定すれば、スライスに別のスライスの中身を展開して追加することもできます。
range
配列やスライスに格納された値を、先頭から順番に処理するような場合は、添字によるアクセスの代わりにrange
を使用できます。
for文の中でrange
を用いると、添字と値の両方が取得できます。
}
実行結果は次のようになります。
range
は配列やスライスのほかに、string、マップ、チャネルに対しても使用できます。マップについては本章で、チャネルについては5章で解説します。
値の切り出し
string、配列、スライスから、値を部分的に切り出すことができます。次のように始点と終点をコロンで挟んで指定すると、その範囲の値を切り出すことができます。始点、終点を省略した場合、それぞれ先頭、末尾になります。
可変長引数
関数において引数を次のように指定すると、可変長引数として、任意の数の引数をその型のスライスとして受け取ることができます。
マップ
マップは、値をKey-Valueの対応で保存するデータ構造です。
宣言と初期化
たとえばintのキーにstringの値を格納するマップは次のように宣言します。
次のようにキーを指定して値を保存します。
宣言と初期化を一緒に行う場合は次のように書きます。
マップの操作
マップから値を取り出す場合は、次のようにキーを指定し、戻り値として受け取ります。
このとき2つ目の戻り値も受け取ると、指定したキーがこのマップに格納されているかをbool
で返します。マップ内のキーの存在を調べるような場合には、値を無視して次のようにします。
マップからデータを消す場合は組込み関数のdelete()
を使用します。
スライス同様、range
を用いるとfor文でKey-Valueをそれぞれ受け取りながら処理を進めることができます。ただし、マップの場合は取り出される順番は保証されない点に注意してください。
ポインタ
Goはポインタを扱うことができます。ポインタ型の変数は、型の前に*
を付けます。アドレスは変数の前に&
を付けて取得できるため、Cと似たような形で表現できます。
しかし、Cなどと違い、Goはポインタ演算を認めていません。ポインタをデータサイズ分ずつずらして、メモリ上からデータを読み込むといったことは基本的にはできません。
defer
ファイル操作などを行う場合、使用後のファイルは必ず閉じる必要があります。次の例では関数の最後にファイルのクローズ処理を記述していますが、その前に関数を抜ける処理があったり、後述するパニックが起こってしまうと、Close()
まで到達しない場合が発生してしまいます。
こうした処理はdefer
を用いて記述できます。先の例ではfile.Close()
の関数呼び出しをdefer
の後ろに記述すると、この処理がmain()
を抜ける直前に必ず実行されるようになります。
ファイルのClose()
などは、defer
を用いて記述するほうが安全です。
パニック
エラーは戻り値によって表現するのが基本ですが、そうではない場合もあります。たとえば配列やスライスの範囲外にアクセスした場合や、ゼロ除算をしてしまった場合などです。こうした処理はエラーを返すことができないため、代わりにパニックという方法でエラーが発生します。
このパニックで発生したエラーはrecover()
という組込み関数で取得し、そこでエラー処理を実施できます。recover()
をdefer
の中に書くことで、パニックで発生したエラーの処理を実施してから、関数を抜けることができます。
panic()
パニックは組込み関数panic()
を用いて自分で発生させることもできます。先ほどの例を自分でパニックにする場合は次のように書けます。
ただしパニックを用いるのは、エラーを戻り値として表現できない場合や、回復が不可能なシステムエラー、やむを得ず大域脱出が必要な場合などであり、基本的にエラーは関数の戻り値として呼び出し側に返すようにしましょう。
まとめ
この章ではGoの基本的な文法を解説しました。これらの知識は次章以降にも使用しますので、しっかり把握しておきましょう。