本章では、
序盤は1章で実行したhello worldのプログラムを振り返ってみましょう。
package main
import (
"fmt"
)
func main() {
fmt.Println("hello world")
}
mainパッケージ
Goのコードはパッケージの宣言から始まります。
package main
プログラムをコンパイルして実行すると、main()
関数が実行されるため、
func main() {
}
インポート
importは、
import (
"fmt"
)
func main() {
fmt.Println("hello world")
}
インポートしたパッケージ内へは、Println()
を使用しています。
複数パッケージの取り込み
複数のパッケージを取り込む場合は次のように縦に並べて記述します。
import (
"fmt"
"github.com/wdpress/gosample"
"strings"
)
Goの標準パッケージはインストールしたGoの中に含まれているため自動でパスが解決され、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
)
_
を付けた場合は、
.
を付けた場合は、
組込み型
さて、hello world
という文字列を出力しましたが、
型 | 説明 |
---|---|
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は、
変数
次は変数について説明します。例としてhello worldプログラムの出力メッセージを、message
というstringの変数に代入してみます。
var message string = "hello world"
func main() {
fmt.Println(message)
}
変数の宣言は、var
で始まり、
一度に複数の宣言と初期化
一度に複数宣言する場合は次のように記述することもできます。
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であると推論されます。
定数
変数宣言のvar
をconst
に変えると定数になります。
func main() {
const Hello string = "hello"
Hello = "bye" // cannot assign to Hello
}
定数宣言できる型は、
ゼロ値
変数を宣言し、0
であるため、0
を出力します。
func main() {
var i int // iはゼロ値で初期化
fmt.Println(i) // 0
}
型ごとのゼロ値は表2のようになります。
型 | ゼロ値 |
---|---|
整数型 | 0 |
浮動小数点型 | 0. |
bool | false |
string | "" |
配列 | 各要素がゼロ値の配列 |
構造体 | 各フィールドがゼロ値の構造体 |
そのほかの型 | nil |
※ nilは値がないことを表す値。ほかの言語におけるnullなどに相当する
if
Goでは、
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 n == 10
fmt.Println(n)
// syntax error: missing { after if clause
また、
n == 10 ? fmt.Println(n): fmt.Println(0)
// unexpected name, expecting semicolon or newline or }
Goではこうした多様な書き方を認めないことで、
for
Goのfor文では、
func main() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}
Goで繰り返しを表現する方法はfor文以外になく、
whileもforで
たとえばCのwhile文は、
// 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の場合、
// C
for (;;) {
doSomething();
}
Goでは、
// 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/
値での分岐
まず、
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文は、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
が実行でき、
また、type
関数
関数はfunc
で始まります。引数も戻り値もない場合は次のように宣言します。
func hello() {
fmt.Println("hello")
}
func main() {
hello() // hello
}
引数がある場合は変数と型を指定します。複数の同じ型が続く場合は、
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.
は、*os.
、error
を返します。
func main() {
file, err := os.Open("hello.go")
if err != nil {
// エラー処理
// returnなどで処理を別の場所に抜ける
}
// fileを用いた処理
}
自作のエラーは、
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)
}
複数の値を返す場合もエラーを最後にする慣習があるため、
異常を戻り値で表現できない場合については、
名前付き戻り値
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"}
配列の型は長さも情報として含むため、arr1
とarr2
は、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、
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
}
マップ
マップは、
宣言と初期化
たとえば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はポインタを扱うことができます。ポインタ型の変数は、*
を付けます。アドレスは変数の前に&
を付けて取得できるため、
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
}
しかし、
defer
ファイル操作などを行う場合、Close()
まで到達しない場合が発生してしまいます。
func main() {
file, err := os.Open("./error.go")
if err != nil {
// エラー処理
}
// 正常処理
file.Close()
}
こうした処理はdefer
を用いて記述できます。先の例ではfile.
の関数呼び出しを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の基本的な文法を解説しました。これらの知識は次章以降にも使用しますので、