Goを書き始めるにあたって、基本構文を自分用にまとめてみました。
対象は、Ruby などの他の言語の経験があり、Goはこれから触るという人です。
変数宣言やメソッドの定義から、スライスの長さと容量、ポインタとポインタレシーバの使い分け、error を返すGoらしいエラー処理までを扱います。
エラーのラップ(%w、errors.Is、errors.As)は Go 1.13 以降の機能のため、Go 1.13 以降を前提としています。
Goの基本構文について触れていきます
var a int var b string // or(関数の中でのみ使える短縮形) a := 123 // 型推論で型はintになる
:= は関数の中でしか使えません。また、:= は「新しい変数の宣言」なので、同じスコープですでに var a int と宣言している場合に a := 123 と書くとコンパイルエラー(no new variables on left side of :=)になります。
Goにはクラスがありません。 その代わりに、型にメソッドを定義できます。
下記のコードはHuman型にBmiとRunのメソッドを定義しています。
func (レシーバ) メソッド名(引数) 戻り値 と言う定義順になっています。
型にメソッドを定義した場合のみfuncと関数名の間にレシーバという自身の引数リストを定義します。
レシーバを伴うメソッドの宣言は、レシーバ型が同じパッケージにある必要があります。 他のパッケージに定義している型に対して、レシーバを伴うメソッドを宣言できません (組み込みの int などの型も同様です)。
package main import ( "fmt" ) type Human struct { height float64 weight float64 } // BMI値は「体重(kg)÷ 身長(m)の2乗」の計算式で算出 func (h Human) Bmi() float64 { height_m := h.height /100 return h.weight / (height_m * height_m) } func (h Human) Run() { fmt.Println("走っています") } func main() { h := Human{160, 65} fmt.Println(h.Bmi()) h.Run() } // 実行結果 25.390624999999996 走っています
スライスの操作についてまとめます。
配列とスライスの違いは
配列の定義
// 1. 要素数のみを指定して初期化(中身はすべてゼロ値の 0 になる) var a [6]int // 2. 要素の値を指定して初期化 a := [6]int{1, 2, 3, 4, 5, 6} // 3. 要素数をコンパイラに自動で数えさせる初期化(これも固定長の配列になります) a := [...]int{1, 2, 3, 4, 5, 6}
スライスの定義
// 1. リテラルで初期化(型に数字を書かないとスライスになります) s := []int{1, 2, 3, 4, 5, 6} // 2. make関数を使って初期化(長さ 5、容量 5 のスライスを作成) s := make([]int, 5) // 3. 長さと容量(Capacity)を別々に指定して初期化(長さ 5、容量 10) s := make([]int, 5, 10)
| 項目 | 意味 | 取得する関数 |
|---|---|---|
| 長さ (Length) | スライスに現在入っている要素の数 | len(s) |
| 容量 (Capacity) | スライスが再確保なしで格納できる最大の要素数 | cap(s) |
make([]int, 3, 5) の場合)例えば、s := make([]int, 3, 5) と宣言したとき、メモリ上は次のような状態になります。
s[0] s[1] s[2] [ 0, 0, 0, _, _ ] (裏側にある長さ5の配列) |<-- 長さ(3) -->| |<------- 容量(5) ------->|
s[0], s[1], s[2] までです。s[3] = 10 のようにアクセスしようとすると、容量内であっても**パニック(インデックス範囲外エラー)**になります。append で要素を追加できます。スライスは、要素が増えて容量を超えると、自動的に裏側でより大きな配列を確保し、古いデータを丸ごとコピーするという処理(リサイズ)を行います。
しかし、配列の再確保とデータコピーは、メモリやCPUへの処理負荷(コスト)が高いです。
あらかじめ要素数がたくさん増えることが分かっている場合、make([]int, 0, 100) のように最初から広い容量を確保しておくことで、裏でのコピー処理を減らし、プログラムを高速に動かすことができるようになります。
appendを使います。
package main import "fmt" func main() { var s []int fmt.Println(s) s = append(s, 0) fmt.Println(s) s = append(s, 10) fmt.Println(s) s = append(s, 100, 1000) fmt.Println(s) } // 実行結果 [] [0] [0 10] [0 10 100 1000]
append は、要素を追加した新しい長さのスライスを返します。容量に余裕がある場合でも長さは変わるため、戻り値を受け取らないと元の変数には反映されません。さらに容量が足りない場合は新しい配列が確保されます。そのため、s = append(s, ...) のように必ず戻り値を受け取る必要があります。
⚠️ 裏側の配列の共有に注意
t := s[:2]のように切り出したスライスは、元のスライスと同じ裏側の配列を共有しています。容量に余裕がある状態でt = append(t, 99)を行うと、新しい配列は確保されず、元のs[2]が 99 に書き換わります。切り出したスライスに要素を追加する場合は、この挙動に注意が必要です。
if文の書き方は以下のようになります。
package main import "fmt" func main() { score := 85 if score >= 90 { fmt.Println("優秀です") } else if score >= 70 { fmt.Println("合格です") // 出力:合格です } else { fmt.Println("不合格です") } }
実務では、関数の実行とエラーチェックを同時に行う際によく使われます。
// 戻り値とエラーを受け取り、その場でエラーチェックを行う if err := process(); err != nil { fmt.Println("エラーが発生しました:", err) return }
簡易文で宣言した変数(上記の err など)の有効範囲は、その if-else ブロックの中だけ
for文の書き方をまとめます。
カウンタ変数を使って、指定回数だけ繰り返す最も標準的な方法です。
for i := 0; i < 5; i++ { fmt.Println(i) // 0 から 4 まで出力 }
() は不要です。{} は必須です。初期化文と後処理文を省略すると、他言語の while 文と同じ挙動になります。
n := 1 for n < 100 { n *= 2 } fmt.Println(n) // 128
条件式もすべて省略すると、無限ループになります。処理を抜けるには break を使用します。
for { // ずっと繰り返す処理 if 終了条件 { break // ループを抜ける } }
配列、スライス、マップ(連想配列)などの要素を順番に処理する際に最もよく使われる構文です。
for インデックス, 値 := range スライス の順で取得できます。
s := []string{"apple", "banana", "cherry"} for i, v := range s { fmt.Printf("インデックス: %d, 値: %s\n", i, v) }
for キー, 値 := range マップ の順で取得できます。
m := map[string]int{"apple": 100, "banana": 200} for k, v := range m { fmt.Printf("キー: %s, 値: %d\n", k, v) }
マップを range で回したときの順序は保証されません(実行するたびに変わる可能性があります)。順序が必要な場合は、キーをスライスに取り出してソートしてから回します。
Go言語では、関数内で宣言したローカル変数を使わないとコンパイルエラーになります。そのため、片方の値しか使わない場合は以下のように記述します。
_ で捨てる
for _, v := range s { fmt.Println(v) // 値だけを使用 }
for i := range s { fmt.Println(i) // インデックスだけを使用 }
break: 現在のループを完全に終了し、外に抜けます。continue: 現在の回の処理をスキップし、次のループの評価(または次の要素)に進みます。for i := 1; i <= 5; i++ { if i == 2 { continue // 2 のときは出力せず次のループへ } if i == 4 { break // 4 になったらループ自体を終了 } fmt.Println(i) // 1 と 3 だけが出力される }
ポインタとは、値そのものではなく、「値がメモリ上のどこに保存されているか」という住所(メモリアドレス)を指し示す変数のことです。
& と *)ポインタを扱う上で、覚えるべき記号は &(アドレス演算子) と *(デリファレンス演算子) の2つだけです。
& (&):変数から**「アドレス(住所)」**を取り出す。* (*):アドレスから**「実際の実体(値)」**を取り出す。(*int のように型の前に付けた場合は「intのポインタ型」という意味になります)package main import "fmt" func main() { var x int = 42 // 1. &x で x のメモリアドレスを取得し、p に代入 // p の型は「intのポインタ型」を表す *int になる var p *int = &x fmt.Println(x) // 出力:42 (実体) fmt.Println(p) // 出力:0xc0000140a8 などのメモリアドレス(実行環境・実行ごとに変わる) // 2. *p で、pが指し示すアドレスにある「実体の値」にアクセス fmt.Println(*p) // 出力:42 // 3. ポインタ経由で元の変数の値を書き換える *p = 100 fmt.Println(x) // 出力:100(xの値そのものが変わる) } // 実行結果 42 0xc0000140a8 42 100
Go言語でポインタを使う代表的な理由は以下の2つです。
Go言語は**値渡し(引数のデータをコピーして関数に渡す)**が基本です。そのため、ポインタを使わないと関数内で値を書き換えても呼び出し元には反映されません。
// ❌ ポインタを使わない場合(値のコピーが渡る) func updateValue(n int) { n = 100 // コピーされた n を変えただけなので、呼び出し元は変わらない } // ⭕ ポインタを使う場合(アドレスが渡る) func updatePointer(n *int) { *n = 100 // アドレスの先にある実体を書き換える }
巨大な構造体(Struct)などをそのまま関数に渡すと、メモリ上でその巨大なデータを丸ごとコピーすることになります。 ポインタ(アドレス情報)だけを渡せば、データのサイズに関わらずコピーするのはアドレス分だけで済むため、大きな構造体ではコピーのコストを減らせます。
ただし、ポインタにすると値がヒープに確保されてGC(ガベージコレクション)の負荷が増える場合もあるため、小さな構造体ではポインタにしても速くなるとは限りません。
この2つ以外にも、「値がない状態」を nil で表したい場合や、複数の場所から同じデータを共有したい場合、sync.Mutex を含む構造体をコピーしないようにしたい場合などにもポインタを使います。
ポインタ型の変数を初期化せずに宣言すると、値は nil(どこも指していない状態) になります。
nil の状態のまま *p = 10 のように実体にアクセスしようとすると、プログラムが強制終了(パニック)するため注意が必要です。
var p *int // 初期値は nil if p != nil { fmt.Println(*p) } else { fmt.Println("pは何も指していません") }
メソッドを定義するとき、構造体をそのまま受け取る 「値レシーバ」 と、ポインタとして受け取る 「ポインタレシーバ」 の2種類があります。
メソッド名の前のカッコ内に (u *User) のように * を付けて定義するものがポインタレシーバです。
package main import "fmt" type User struct { Name string Age int } // ⭕ ポインタレシーバ: 構造体のフィールドを「書き換える」ことができる func (u *User) CelebrateBirthday() { u.Age++ // レシーバがポインタなので、呼び出し元の実体が書き換わる } // ❌ 値レシーバ: 構造体が「コピー」されて渡るため、書き換えても呼び出し元に影響しない func (u User) UpdateNameDummy(newName string) { u.Name = newName // コピーされたデータが変わるだけ } func main() { user := User{Name: "Taro", Age: 20} // 1. ポインタレシーバの呼び出し user.CelebrateBirthday() fmt.Println(user.Age) // 出力:21(年齢が増えている!) // 2. 値レシーバの呼び出し user.UpdateNameDummy("Jiro") fmt.Println(user.Name) // 出力:Taro(名前は変わっていない) } // 実行結果 21 Taro
💡 自動でアドレスを取ってくれる メソッドを呼び出す際、
userがポインタではなく実体であっても、アドレスを取れる変数であれば、Goコンパイラが自動的に(&user).CelebrateBirthday()のように解釈してくれます。そのため、呼び出し側はどちらのレシーバであってもuser.Method()という同じ構文で呼び出せます。 ただし、マップの要素(m["taro"].CelebrateBirthday())や関数の戻り値のようにアドレスを取れない値に対しては、ポインタレシーバのメソッドを直接呼ぶとコンパイルエラーになります。
どちらを採用すべきかの目安は以下の通りです。
| 基準項目 | 値レシーバ (u User) | ポインタレシーバ (u *User) |
|---|---|---|
| データの変更 | 変更しない(読み取り専用) | 変更する(状態を更新したい) |
| データサイズ | 構造体が小さい、基本型、スライス | 構造体の中に多くのフィールドがある(コピーのコスト削減) |
| 一貫性 | - | ポインタレシーバが必要なメソッドが1つでもあるなら、他のメソッドもポインタに揃える |
Go Wiki の Go Code Review Comments では、「迷ったらポインタレシーバを使う(when in doubt, use a pointer receiver)」とされています。 一方で、Go公式のFAQ では、基本型やスライス、小さな構造体であれば、メソッドの意味上ポインタが必要でない限り値レシーバが効率的で分かりやすいとされています。
そのため、小さく変更しない値型なら値レシーバ、それ以外で判断に迷った場合はポインタレシーバを選ぶ、というのが無難だと思います。
「このメソッドは値を変更しないから値レシーバ」「このメソッドは変更するからポインタレシーバ」というように、1つの型に対して両方を混在させるのは避けるべきとされています。
混在させると、その型がインターフェース(interface)を満たしているかどうかの判定(メソッドセットのルール)で混乱を生む原因になります。
メソッドセットのルールは次の通りです。
*User(ポインタ型)のメソッドセットには、値レシーバとポインタレシーバの両方のメソッドが含まれるUser(値型)のメソッドセットには、値レシーバのメソッドだけが含まれるつまり、インターフェースのメソッドをポインタレシーバで実装した場合、*User はそのインターフェースを満たしますが、User の値は満たしません。
Goでは、例外(Exception)を投げる代わりに、関数の戻り値として error 型のオブジェクトを返します。呼び出し側は、そのエラーが nil かどうかをチェックして処理を進めます。
Goで最も頻繁に目にする、関数の戻り値と if 文を組み合わせた標準的なパターンです。
package main import ( "errors" "fmt" ) // 割り算を行う関数。0で割ろうとした場合はエラーを返す。 func divide(a, b float64) (float64, error) { if b == 0 { // errors.New() でシンプルなエラーを作成 return 0, errors.New("0で割ることはできません") } return a / b, nil // 正常終了時は error に nil を返す } func main() { // 結果とエラーを受け取り、先にエラーをチェックして早期リターンする result, err := divide(10, 0) if err != nil { fmt.Println("エラーが発生しました:", err) // 出力:エラーが発生しました: 0で割ることはできません return } fmt.Println("結果:", result) } // 実行結果 エラーが発生しました: 0で割ることはできません
エラー時の処理を先に書いて return し、正常系は else に入れずにインデントを浅く保つのがGoの慣習です(Go Code Review Comments - Indent Error Flow)。
fmt.Errorf)エラーメッセージに変数や数値を埋め込みたい(動的に作成したい)場合は、fmt.Errorf を使用します。
id := 999 if id > 500 { // 文字列のフォーマットと同じ感覚でエラーを作れる return fmt.Errorf("ユーザーID %d は存在しません", id) }
実務では、下層の関数から上がってきたエラーに「どこで起きたか」の文脈(コンテキスト)を追加して上に引き渡す 「エラーのラップ(Wrapping)」 を行います。
%w)fmt.Errorf の中で %w という特別なフォーマット指定子を使うことで、元のエラーを包み込むことができます。
func processData() error { err := readDatabase() if err != nil { // 元のエラー(err)を %w でラップして、新しいメッセージを付与する return fmt.Errorf("データの処理に失敗しました: %w", err) } return nil }
errors.Is)エラーが特定の原因(特定のエラーオブジェクト)かどうかを判定するには、== ではなく errors.Is を使います。これにより、エラーが何重にラップされていても、中身にそのエラーが含まれているかを自動で探してくれます。
// 判定したい特定のベースエラー var ErrNotFound = errors.New("データが見つかりません") func main() { err := processData() // 内部で ErrNotFound がラップされて返ってきたとする // ❌ == での比較は、ラップされていると一致しなくなるためNG // if err == ErrNotFound { ... } // ⭕ errors.Is を使えば、ラップされていても正しく判定できる if errors.Is(err, ErrNotFound) { fmt.Println("404エラーの画面を表示します") } }
エラーメッセージだけでなく、エラーコードや詳細なステータスを持たせたい場合は、**error インターフェース(Error() string メソッドを持つ型)**を満たす独自の構造体を作ります。
// 1. 独自のエラー構造体を定義 type MyCustomError struct { Code int Message string } // 2. error インターフェースを実装(Error() メソッドを定義) func (e *MyCustomError) Error() string { return fmt.Sprintf("[%d] %s", e.Code, e.Message) } func doSomething() error { return &MyCustomError{Code: 403, Message: "アクセス権限がありません"} }
errors.As)返ってきた error 型の変数から、独自の構造体(上記例なら MyCustomError)の実体や中のフィールドを取り出したいときは、errors.As を使います。errors.As はラップされたエラーを順にたどり、指定した型に一致するエラーが見つかればその値を代入してくれます。
func main() { err := doSomething() var customErr *MyCustomError // err(またはラップされた中身)が *MyCustomError 型なら、customErr に代入する if errors.As(err, &customErr) { fmt.Println("エラーコード:", customErr.Code) // 出力:エラーコード: 403 fmt.Println("メッセージ:", customErr.Message) // 出力:メッセージ: アクセス権限がありません } }
ポインタ型のカスタムエラーでは、次のように書くとエラーがないのに err != nil が true になるという落とし穴があります。
func doSomething() error { var e *MyCustomError // nil のポインタ // ... エラーが起きなかった場合 return e // ❌ error 型としては nil にならない } func main() { err := doSomething() fmt.Println(err != nil) // 出力:true }
インターフェース(error)は「型」と「値」の組で表現されており、*MyCustomError 型の nil を入れると「型は *MyCustomError、値は nil」という状態になるため、nil と比較しても一致しません(Go FAQ - Why is my nil error value not equal to nil?)。
エラーがない場合は、return nil と明示的に nil を返すようにします。
Goの基本構文についてまとめました。
var または :=(関数内のみ)で宣言し、クラスの代わりに型へメソッドを定義するmake で容量を確保しておくとコピーを減らせる%w でラップして errors.Is / errors.As で判定する