はじめてのGo―シンプルな言語仕様、型システム、並行処理

第2章基本文法―覚えやすいコンパクトな言語仕様

本章では、Goの基本的な文法について解説します。

序盤は1章で実行したhello worldのプログラムを振り返ってみましょう。

package main

import (
    "fmt"
)

func main() {
    fmt.Println("hello world")
}

mainパッケージ

Goのコードはパッケージの宣言から始まります。

package main

プログラムをコンパイルして実行すると、まずmainパッケージの中にあるmain()関数が実行されるため、ここに処理を記述しています。関数の詳細は後述します。

func main() {
}

インポート

importは、プログラム内にほかのパッケージを取り込むために使用します。

import (
    "fmt"
)

func main() {
    fmt.Println("hello world")
}

インポートしたパッケージ内へは、パッケージ名にドットを付けてアクセスできます。たとえば上記では、出力にfmtパッケージのPrintln()を使用しています。

複数パッケージの取り込み

複数のパッケージを取り込む場合は次のように縦に並べて記述します。

import (
    "fmt"
    "github.com/wdpress/gosample"
    "strings"
)

Goの標準パッケージはインストールしたGoの中に含まれているため自動でパスが解決され、それ以外のパッケージは1章で解説したようにGOPATH環境変数からパスを解決します。

オプションの指定

インポートするパッケージ名の前には、いくつかのオプションが指定できます。

package main

import (
    f "fmt"
    _ "github.com/wdpress/gosample"
    . "strings"
)

func main() {
    // fmt.Println()がf.Println()になり
    // strings.ToUpper()がToUpper()なっている
    f.Println(ToUpper("hello world"))
}

任意の名前(上記ではfを指定した場合は、プログラム内でのパッケージ名を変えることができます。

_を付けた場合は、インポートしたパッケージを使用しないことをコンパイラに伝えます。第1章で解説したように、Goはインポートして使用しないパッケージがあるとコンパイルエラーになります。上記の例ではgithub.com/wdpress/gosampleパッケージはインポートして使っていなかったので、_が付いていなかったらコンパイルエラーになります。

.を付けた場合は、使用時にパッケージ名が省略可能になります。

組込み型

さて、hello worldプログラムではhello worldという文字列を出力しましたが、この文字列はGoではstringという組込み型として扱われます。ほかにもGoには表1の組込み型があります。

表1 Goの組込み型
説明
uint88ビット符号なし整数
uint1616ビット符号なし整数
uint3232ビット符号なし整数
uint6464ビット符号なし整数
int88ビット符号あり整数
int1616ビット符号あり整数
int3232ビット符号あり整数
int6464ビット符号あり整数
float3232ビット浮動小数
float6464ビット浮動小数
complex6464ビット複素数
complex128128ビット複素数
byteuint8のエイリアス
runeUnicodeのコードポイント
uint32か64ビットの符号なし整数
int32か64ビットの符号あり整数
uintptrポインタ値用符号なし整数
errorエラーを表わすインタフェース

runeは、Unicodeのコードポイントを指します。実際は、ほかの言語で言うcharのように1文字を表すことに使用します。stringがダブルクオートでくくられるのに対し、runeはシングルクオートでくくられます。またバッククオートで囲むことで、複数行に渡るstring(ヒアドキュメント)を記述できます。

変数

次は変数について説明します。例としてhello worldプログラムの出力メッセージを、messageというstringの変数に代入してみます。

var message string = "hello world"

func main() {
    fmt.Println(message)
}

変数の宣言は、varで始まり、次に変数名、最後が型です。型と変数名の順番を珍しく思うかもしれませんが、これがGoの特徴の一つです。

一度に複数の宣言と初期化

一度に複数宣言する場合は次のように記述することもできます。

var foo, bar, buz string = "foo", "bar", "buz"

一度に多くの変数を同じ型で宣言する場合は、varと2つ目以降の型を省略して、次のように書くこともできます。

var (
a string = "aaa"
b = "bbb"
c = "ccc"
d = "ddd"
e = "eee"
)

関数内部での宣言と初期化

変数宣言と初期化を関数の内部で行う場合は、varと型宣言を省略し、:=という記号を用いることができます。

func main() {
    // どちらの書き方も同じ意味
    // var message string = "hello world"
    message := "hello world"
    fmt.Println(message)
}

この書き方をした場合、変数の型は代入する値からコンパイラによって推論されます。今回は文字列を代入していることから、変数messageの型がstringであると推論されます。

定数

変数宣言のvarconstに変えると定数になります。

func main() {
    const Hello string = "hello"
    Hello = "bye" // cannot assign to Hello
}

定数宣言できる型は、組込み型のうちerror以外の型です。定数に対する再代入はコンパイルエラーになるため、実行前にミスを発見できます。

ゼロ値

変数を宣言し、明示的に値を初期化しなかった場合、変数はゼロ値というデフォルト値で初期化されます。ゼロ値は型ごとに決まっており、たとえばintのゼロ値は0であるため、次のコードは0を出力します。

func main() {
    var i int // iはゼロ値で初期化
    fmt.Println(i) // 0
}

型ごとのゼロ値は表2のようになります。

表2 型ごとのゼロ値
ゼロ値
整数型0
浮動小数点型0.0
boolfalse
string""
配列各要素がゼロ値の配列
構造体各フィールドがゼロ値の構造体
そのほかの型nil

nilは値がないことを表す値。ほかの言語におけるnullなどに相当する

if

Goでは、if文の条件部に丸括弧は必要ありません。

func main() {
    a, b := 10, 100
    if a > b {
        fmt.Println("a is larger than b")
    } else if a < b {
        fmt.Println("a is smaller than b")
    } else {
        fmt.Println("a equals b")
    }
}

なお、if文の処理が1行の場合に波括弧を省略可能な言語もありますが、Goではそうした省略はコンパイルエラーになります[1]⁠。

if n == 10
    fmt.Println(n)
    // syntax error: missing { after if clause

また、三項演算子はないため次のような書き方はできません。if/else文を使用します。

n == 10 ? fmt.Println(n): fmt.Println(0)
// unexpected name, expecting semicolon or newline or }

Goではこうした多様な書き方を認めないことで、言語の仕様を小さく保つ方針を採っています。

for

Goのfor文では、CやJavaでは必要な条件部の丸括弧が必要ありません。

func main() {
    for i := 0; i < 10; i++ {
        fmt.Println(i)
    }
}

Goで繰り返しを表現する方法はfor文以外になく、ほかの言語におけるwhile文やdo/while文、loop文といったような表現は、すべてfor文を用いて行います。

whileもforで

たとえばCのwhile文は、for文で次のように表現します。

// C
int n = 0;
while (n < 10) {
    printf("n = %d\n", n);
    n++;
}

// Go
n := 0
for n < 10 {
    fmt.Printf("n = %d\n", n)
    n++
}

無限ループ

Cの場合、for文を使って次のように無限ループを表現します。

// C
for (;;) {
    doSomething();
}

Goでは、for文の条件部を省略することで同様の表現ができます。

// Go
for {
    doSomething()
}

break、continue

繰り返し制御にはCやJavaと同様に、ループを終了するbreakループの最初から処理を再開するcontinueを使用できます。

func main() {
    n := 0
    for {
        n++
        if n > 10 {
            break // ループを抜ける
        }
        if n%2 == 0 {
            continue // 偶数なら次の繰り返しに移る
        }
        fmt.Println(n) // 奇数のみ表示
    }
}

switch

if/else文が繰り返す場合は、switch文を用いたほうがスッキリ書ける場合があります。Goのswitch文は非常に柔軟であり、値の比較だけでなく条件分岐にも使用できます。

値での分岐

まず、値を用いたswitch文は次のようになります。

func main() {
    n := 10
    switch n {
    case 15:
        fmt.Println("FizzBuzz")
    case 5, 10:
        fmt.Println("Buzz")
    case 3, 6, 9:
        fmt.Println("Fizz")
    default:
        fmt.Println(n)
    }
}

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に処理を進めることができます。

func main() {
    n := 3
    switch n {
    case 3:
        n = n - 1
        fallthrough
    case 2:
        n = n - 1
        fallthrough
    case 1:
        n = n - 1
        fmt.Println(n) // 0
    }
}

式での分岐

Goのswitch文では、caseに値だけでなく式も指定できます。

func main() {
    n := 10
    switch {
    case n%15 == 0:
        fmt.Println("FizzBuzz")
    case n%5 == 0:
        fmt.Println("Buzz")
    case n%3 == 0:
        fmt.Println("Fizz")
    default:
        fmt.Println(n)
    }
}

たとえば上記のようにcaseに式を指定すれば、評価結果がtrueになるcaseが実行でき、if/else 文の代わりに使用できます。

また、値、式以外にtype(型)を用いたswitch文もありますが、これについては3章で解説します。

関数

関数はfuncで始まります。引数も戻り値もない場合は次のように宣言します。

func hello() {
    fmt.Println("hello")
}

func main() {
    hello() // hello
}

引数がある場合は変数と型を指定します。複数の同じ型が続く場合は、型の宣言は最後の1つにまとめることができます。

func sum(i, j int) { // func sum(i int, j int) と同じ
    fmt.Println(i + j)
}

func main() {
    sum(1, 2) // 3
}

戻り値がある場合は引数の次に指定します。

func sum(i, j int) int {
    return i + j
}

func main() {
    n := sum(1, 2)
    fmt.Println(n) // 3
}

関数は複数の値を返せる

Goの大きな特徴の一つとして、関数は複数の値を返すことができます。戻り値が複数の場合は、型をカンマで区切って指定し丸括弧でくくります。returnはそれに対応した型の値を、同じくカンマで区切って返します。

func swap(i, j int) (int, int) {
    return j, i
}

func main() {
    x, y := 3, 4
    x, y = swap(x, y)
    fmt.Println(x, y) // 4, 3

    x = swap(x, y) // コンパイルエラー

    x, _ = swap(x, y) // 第二戻り値を無視
    fmt.Println(x) // 3

    swap(x, y) // コンパイル、実行ともに可能
}

関数の実行時には、戻り値を格納する変数を必要な数だけ用意する必要があります。関数が返す値の数と、受け取る変数の数が合わないとコンパイルエラーになります。ただし、無視したい戻り値がある場合は_で明示的に無視することで、戻り値を受け取らなくてもコンパイル、実行ともに可能です。

エラーを返す関数

Goでは、関数が多値を返せることを利用して、内部で発生したエラーを戻り値で表現します。関数の処理に成功した場合はエラーはnilにし、異常があった場合はエラーだけに値が入り、他方はゼロ値になります。

たとえばファイルを開くos.Open()は、1つ目の戻り値に*os.File2つ目にerrorを返します。

func main() {
    file, err := os.Open("hello.go")
    if err != nil {
        // エラー処理
        // returnなどで処理を別の場所に抜ける
    }
    // fileを用いた処理
}

自作のエラーは、errorsパッケージを用いて作ることができます。

package main

import (
    "errors"
    "fmt"
    "log"
)

func div(i, j int) (int, error) {
    if j == 0 {
        // 自作のエラーを返す
        return 0, errors.New("divied by zero")
    }
    return i / j, nil
}

func main() {
    n, err := div(10, 0)
    if err != nil {
        // エラーを出力しプログラムを終了する
        log.Fatal(err)
    }
    fmt.Println(n)
}

複数の値を返す場合もエラーを最後にする慣習があるため、自分でAPIを設計する場合もエラーを最後にするほうがよいでしょう。

異常を戻り値で表現できない場合については、後述のパニックとリカバで解説します。

名前付き戻り値

Goでは、戻り値にあらかじめ名前を付けることができます。先ほどの関数の戻り値に、次のように名前を付けてみます。

func div(i, j int) (result int, err error)

名前付き戻り値は、関数内ではゼロ値で初期化された変数として扱うことができます。また、変数に名前を付けている場合は、returnのあとに返す値を明示する必要がなく、returnされた時点での名前付き戻り値の値が自動的に返されることになります。

これを用いると、先の関数は次のように書くことができます。

func div(i, j int) (result int, err error) {
    if j == 0 {
        err = errors.New("divied by zero")
        return // return 0, errと同じ
    }
    result = i / j
    return // return result, nilと同じ
}

名前付き戻り値を用いることで、関数の宣言から戻り値の意味が読み取りやすくなると同時に、戻り値のための変数の初期化が不要になり、同じ型の戻り値が多かった場合のreturnの書き間違えなどを防ぐこともできます。

ただし、戻り値に名前を付けても、returnのあとに戻す値を明示することは可能です。プログラムのわかりやすさを重視して使い分けるとよいでしょう。

関数リテラル

関数リテラルを用いると、無名関数を作ることができます。即時に実行する関数は次のように記述できます。

func main() {
    func(i, j int) {
        fmt.Println(i + j)
    }(2, 4)
}

Goにおける関数は第一級オブジェクトであるため、関数を変数に代入したり関数の引数に渡すことができます。たとえば、上記の関数を代入する変数の型は次のようになります。

var sum func(i, j int) = func(i, j int) {
    fmt.Println(i + j)
}

func main() {
    sum(2, 4)
}

配列

Goの配列は固定長です。可変長配列は後述するスライスがそれにあたります。たとえば長さが4で要素の型がstringである配列は、次のように宣言します。

var arr1 [4]string

配列は、ほかの言語同様に添字でアクセスします。

var arr [4]string

arr[0] = "a"
arr[1] = "b"
arr[2] = "c"
arr[3] = "d"
fmt.Println(arr[0]) // a

宣言と同時に初期化することも可能で、その場合は[...]を用いることで、必要な配列の長さを暗黙的に指定できます。

// どちらも同じ型
arr := [4]string{"a", "b", "c", "d"}
arr := [...]string{"a", "b", "c", "d"}

配列の型は長さも情報として含むため、次のarr1arr2は、要素の型は同じstringですが、長さが違うため配列としては別の型です。関数fn[4]string型を引数にとるため、型の合わないarr2を渡すとコンパイルエラーになります。

func fn(arr [4]string) {
    fmt.Println(arr)
}

func main() {
    var arr1 [4]string
    var arr2 [5]string

    fn(arr1) // ok
    fn(arr2) // コンパイルエラー
}

また、関数に配列を渡す場合は値渡しとなり、配列のコピーが渡されます。次のfn()の中で配列に対して行った変更は、main()側には反映されません。

func fn(arr [4]string) {
    arr[0] = "x"
    fmt.Println(arr) // [x b c d]
}

func main() {
    arr := [4]string{"a", "b", "c", "d"}
    fn(arr)
    fmt.Println(arr) // [a b c d]
}

スライス

スライスは、可変長配列として扱うことができます。配列を直接使うのは、シビアなメモリ管理が必要な一部のプログラムだけなので、同じ性質のデータを束ねて扱うという用途であれば、基本的にはスライスを用います。

なお、スライスの詳細な内部構造については筆者のブログ記事を参照してください。

スライスの宣言

stringのスライスは次のように宣言します。

var s []string

このように、スライスの型には配列のように長さの情報はありません。

初期化を同時に行う場合は、配列と同じように書くことができます。またスライスも、配列同様に添字でアクセスできます。

s := []string{"a", "b", "c", "d"}
fmt.Println(s[0]) // "a"

append()

スライスの末尾に値を追加する場合はappend()を使用します。append()は、スライスの末尾に値を追加し、その結果を返す組込み関数です。複数の値を追加することもできます。

var s []string
s = append(s, "a") // 追加した結果を返す
s = append(s, "b")
s = append(s, "c", "d")
fmt.Println(s) // [a b c d]

次のように指定すれば、スライスに別のスライスの中身を展開して追加することもできます。

s1 := []string{"a", "b"}
s2 := []string{"c", "d"}
s1 = append(s1, s2...) // s1にs2を追加
fmt.Println(s1)        // [a b c d]

range

配列やスライスに格納された値を、先頭から順番に処理するような場合は、添字によるアクセスの代わりにrangeを使用できます。

for文の中でrangeを用いると、添字と値の両方が取得できます。

var arr [4]string

arr[0] = "a"
arr[1] = "b"
arr[2] = "c"
arr[3] = "d"

for i, s := range arr {
    // i = 添字, s = 値
    fmt.Println(i, s)
}

実行結果は次のようになります。

$ go run range.go
0 a
1 b
2 c
3 d

rangeは配列やスライスのほかに、string、マップ、チャネルに対しても使用できます。マップについては本章で、チャネルについては5章で解説します。

値の切り出し

string、配列、スライスから、値を部分的に切り出すことができます。次のように始点と終点をコロンで挟んで指定すると、その範囲の値を切り出すことができます。始点、終点を省略した場合、それぞれ先頭、末尾になります。

s := []int{0, 1, 2, 3, 4, 5}
fmt.Println(s[2:4])      // [2 3]
fmt.Println(s[0:len(s)]) // [0 1 2 3 4 5]
fmt.Println(s[:3])       // [0 1 2]
fmt.Println(s[3:])       // [3 4 5]
fmt.Println(s[:])        // [0 1 2 3 4 5]

可変長引数

関数において引数を次のように指定すると、可変長引数として、任意の数の引数をその型のスライスとして受け取ることができます。

func sum(nums ...int) (result int) {
    // numsは[]int型
    for _, n := range nums {
        result += n
    }
    return
}

func main() {
    fmt.Println(sum(1, 2, 3, 4))  // 10
}

マップ

マップは、値をKey-Valueの対応で保存するデータ構造です。

宣言と初期化

たとえばintのキーにstringの値を格納するマップは次のように宣言します。

var month map[int]string = map[int]string{}

次のようにキーを指定して値を保存します。

month[1] = "January"
month[2] = "February"
fmt.Println(month) // map[1:January 2:February]

宣言と初期化を一緒に行う場合は次のように書きます。

month := map[int]string{
    1: "January",
    2: "February",
}
fmt.Println(month) // map[1:January 2:February]

マップの操作

マップから値を取り出す場合は、次のようにキーを指定し、戻り値として受け取ります。

jan := month[1]
fmt.Println(jan) // January

このとき2つ目の戻り値も受け取ると、指定したキーがこのマップに格納されているかをboolで返します。マップ内のキーの存在を調べるような場合には、値を無視して次のようにします。

_, ok := month[1]
if ok {
    // データがあった場合
}

マップからデータを消す場合は組込み関数のdelete()を使用します。

delete(month, 1)
fmt.Println(month) // map[1:January]

スライス同様、rangeを用いるとfor文でKey-Valueをそれぞれ受け取りながら処理を進めることができます。ただし、マップの場合は取り出される順番は保証されない点に注意してください。

for key, value := range month {
    fmt.Printf("%d %s\n", key, value)
}

ポインタ

Goはポインタを扱うことができます。ポインタ型の変数は、型の前に*を付けます。アドレスは変数の前に&を付けて取得できるため、Cと似たような形で表現できます。

func callByValue(i int) {
    i = 20 // 値を上書きする
}

func callByRef(i *int) {
    *i = 20 // 参照先を上書きする
}

func main() {
    var i int = 10
    callByValue(i) // 値を渡す
    fmt.Println(i) // 10
    callByRef(&i) // アドレスを渡す
    fmt.Println(i) // 20
}

しかし、Cなどと違い、Goはポインタ演算を認めていません。ポインタをデータサイズ分ずつずらして、メモリ上からデータを読み込むといったことは基本的にはできません。

defer

ファイル操作などを行う場合、使用後のファイルは必ず閉じる必要があります。次の例では関数の最後にファイルのクローズ処理を記述していますが、その前に関数を抜ける処理があったり、後述するパニックが起こってしまうと、Close()まで到達しない場合が発生してしまいます。

func main() {
    file, err := os.Open("./error.go")
    if err != nil {
        // エラー処理
    }
    // 正常処理
    file.Close()
}

こうした処理はdeferを用いて記述できます。先の例ではfile.Close()の関数呼び出しをdeferの後ろに記述すると、この処理がmain()を抜ける直前に必ず実行されるようになります。

func main() {
    file, err := os.Open("./error.go")
    if err != nil {
        // エラー処理
    }
    // 関数を抜ける前に必ず実行される
    defer file.Close()
    // 正常処理
}

ファイルのClose()などは、deferを用いて記述するほうが安全です。

パニック

エラーは戻り値によって表現するのが基本ですが、そうではない場合もあります。たとえば配列やスライスの範囲外にアクセスした場合や、ゼロ除算をしてしまった場合などです。こうした処理はエラーを返すことができないため、代わりにパニックという方法でエラーが発生します。

このパニックで発生したエラーはrecover()という組込み関数で取得し、そこでエラー処理を実施できます。recover()deferの中に書くことで、パニックで発生したエラーの処理を実施してから、関数を抜けることができます。

func main() {
    defer func() {
        err := recover()
        if err != nil {
            // runtime error: index out of range
            log.Fatal(err)
        }
    }()

    a := []int{1, 2, 3}
    fmt.Println(a[10]) // パニックが発生
}

panic()

パニックは組込み関数panic()を用いて自分で発生させることもできます。先ほどの例を自分でパニックにする場合は次のように書けます。

a := []int{1, 2, 3}
for i := 0; i < 10; i++ {
    if i >= len(a) {
        panic(errors.New("index out of range"))
    }
    fmt.Println(a[i])
}

ただしパニックを用いるのは、エラーを戻り値として表現できない場合や、回復が不可能なシステムエラー、やむを得ず大域脱出が必要な場合などであり、基本的にエラーは関数の戻り値として呼び出し側に返すようにしましょう。

まとめ

この章ではGoの基本的な文法を解説しました。これらの知識は次章以降にも使用しますので、しっかり把握しておきましょう。

おすすめ記事

記事・ニュース一覧