こんにちは、かつコーチです。
Go言語を書き始めた人が最初にとまどうのが、エラー処理の書き方だと思います。
「なんでtry-catchがないんだろう」「if err != nilばっかりでコードが縦に伸びる」。
そう感じたことがある人は多いはずです。
この記事では、Goのエラーハンドリングの基本思想と書き方、そして実際につまずきやすいポイントを、私の実体験も交えて解説します。
読み終える頃には、if err != nilの意味とGo流の作法が理解できるはずです。
エラーハンドリングとは?
例外機構がないGoの設計思想
多くの言語には例外機構(プログラム実行中の異常を専用の仕組みで検知し、通常の処理フローから外れて対応するしくみ)があります。
JavaやPythonならtry-catchやtry-exceptを使い、エラーが起きた処理を外側でまとめて捕まえます。
ところがGoには、この例外機構がありません。
Goの設計では、エラーは戻り値(関数が呼び出し元に返す値)として明示的に返す、というスタイルを取っています。
つまりGoでは「エラーは特別な仕組みで飛んでくるもの」ではなく「関数が返す普通のデータの一種」として扱われます。
なぜ戻り値でエラーを返すのか
この設計には理由があります。
例外機構は便利な反面、どこでエラーが発生し、どこで捕まえられるのかがコードを読むだけでは分かりにくくなりがちです。
一方、Goのように戻り値でエラーを返すスタイルだと、関数を呼び出すたびに「このエラーは今すぐ処理すべきか」を強制的に意識させられます。
読みにくいと言われがちなif err != nilの連続は、裏を返せば「エラー処理を後回しにさせない」ためのGoらしい仕組みだと言えます。
基本の書き方
手順1: errorインターフェースを理解する
Goでエラーを表す型はerrorというインターフェース(実装方法を問わない「役割の契約書」のようなもの。中身がどんな型でも、契約書に書かれたメソッドさえ持っていればOKというルール)です。
errorインターフェースは、Error() stringというメソッドを1つ持つだけのシンプルな契約書です。
package main
import "fmt"
type error interface {
Error() string
}
func main() {
fmt.Println("errorはこういうインターフェースです")
}
実際には自分でこの定義を書く必要はなく、標準ライブラリに用意されています。
手順2: if err != nilの基本形
Goで関数がエラーを返す場合、多くは戻り値の最後にerror型を追加します。
呼び出し側は、その戻り値がnil(値が「何もない」ことを表す特別な値)かどうかをチェックします。
package main
import (
"fmt"
"strconv"
)
func main() {
// 文字列を数値に変換する
num, err := strconv.Atoi("123")
if err != nil {
fmt.Println("変換に失敗しました:", err)
return
}
fmt.Println("変換成功:", num)
// わざと失敗させる
num2, err2 := strconv.Atoi("abc")
if err2 != nil {
fmt.Println("変換に失敗しました:", err2)
return
}
fmt.Println("変換成功:", num2)
}
このように「エラーが返ってきたら、その場ですぐ処理する」のが基本の流儀です。
手順3: errors.Newとfmt.Errorfでエラーを作る
自分の関数でエラーを返したい場合、標準ライブラリのerrorsパッケージやfmtパッケージを使います。
errors.Newはシンプルな文字列からエラーを作る関数です。
package main
import (
"errors"
"fmt"
)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("0で割ることはできません")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("エラー:", err)
return
}
fmt.Println("結果:", result)
}
一方fmt.Errorfは、変数を埋め込んだメッセージを作りたいときに便利です。
package main
import (
"fmt"
)
func validateAge(age int) error {
if age < 0 {
return fmt.Errorf("年齢は0以上である必要があります(入力値: %d)", age)
}
return nil
}
func main() {
if err := validateAge(-5); err != nil {
fmt.Println(err)
}
}
%dのような書式指定子を使えば、エラーメッセージに具体的な値を埋め込めるので、デバッグ時にとても役立ちます。
つまずきやすい設定・注意点
エラーを返す関数を作るとき、正常時は必ずnilを返すことを忘れないようにしましょう。
error型の戻り値を用意したのに正常時も何かエラー値を返してしまうと、呼び出し側は常にエラーだと誤判定してしまいます。
また、複数のエラーチェックが連続する場合でも、1つずつ丁寧にif err != nilを書くのがGoの流儀です。
まとめて処理しようとせず、素直に書いた方が結果的に読みやすいコードになります。
よくあるつまずきポイント・エラー対処
nil pointer dereferenceで実際にハマった話
私が実際にGoを書いていて一番よく出会い、かつ一番焦ったのがnil pointer dereference(invalid memory address or nil pointer dereferenceというエラーメッセージで表される、存在しないメモリ番地を参照しようとしたときに起きるエラー)です。
これはGo初心者が最も高い確率で踏むエラーだと思います。
私が実際に踏んだのは、外部APIからJSONを取得してデコードする処理でした。
package main
import "fmt"
type User struct {
Name string
Age int
}
func findUser(id int) *User {
// 本来はDBやAPIを検索する処理だが、
// 見つからなかった場合は nil を返す
if id != 1 {
return nil
}
return &User{Name: "田中", Age: 30}
}
func main() {
user := findUser(2)
// ここでuserはnilなのに、いきなりフィールドにアクセスしてしまった
fmt.Println(user.Name)
}
このコードを実行すると、次のようなエラーで落ちます。
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x…]
私の場合、原因は完全に自分のチェック漏れでした。
「findUserは絶対に何か返してくるだろう」と思い込み、戻り値がnilかどうかの確認を省いてしまったのです。
さらに厄介だったのは、:=によって新しいスコープで変数が作られ、外側の変数を上書きしていないつもりが変数シャドーイング(同じ名前の変数を内側のスコープで新しく宣言してしまい、外側の変数とは別物になってしまうこと)を起こしていたことでした。
if文の中で:=を使ってエラーチェック用の変数を作ったつもりが、実は外側のuserとは別の変数になっていて、外側のuserはnilのまま参照され続けていたのです。
原因が分かるまで、ログを何度も仕込んで1時間近く格闘しました。
これを踏まえて、私が実践している対策は次の3つです。
❌ Before
user := findUser(2)
fmt.Println(user.Name) // userがnilかもしれないのに即アクセス
✅ After
user := findUser(2)
if user == nil {
fmt.Println("ユーザーが見つかりませんでした")
return
}
fmt.Println(user.Name)
ポインタを返す関数を呼んだら、フィールドにアクセスする前に必ずnilチェックを入れる。
これだけで、このエラーの大半は防げます。
エラーを握りつぶしてしまうミス
もう1つよくあるのが、エラーを受け取ったのに何もせず無視してしまうパターンです。
❌ Before
data, _ := readFile("config.json")
fmt.Println(string(data))
✅ After
data, err := readFile("config.json")
if err != nil {
fmt.Println("設定ファイルの読み込みに失敗しました:", err)
return
}
fmt.Println(string(data))
_でエラーを捨ててしまうと、問題が起きたときに原因を追う手がかりが消えてしまいます。
急いでいるときほどやりがちなので、私も気をつけている点です。
応用・一歩先の使い方
基本が身についたら、次はerrors.Isやerrors.Asを使ったエラーの種類判定に挑戦してみましょう。
Go1.13以降では、エラーをラップ(fmt.Errorfの%wを使って元のエラー情報を包んで返すこと)して、呼び出し元でその正体を調べられるようになっています。
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("データが見つかりません")
func findData(id int) error {
if id != 1 {
return fmt.Errorf("findData失敗: %w", ErrNotFound)
}
return nil
}
func main() {
err := findData(2)
if errors.Is(err, ErrNotFound) {
fmt.Println("これは「見つからない」エラーです")
}
}
これができるようになると、単に「エラーがあるかないか」だけでなく「どんな種類のエラーか」に応じた処理の出し分けができるようになります。
まとめ
この記事のポイント
- Goには例外機構がなく、エラーは戻り値として明示的に返す設計になっている
if err != nilは面倒に見えて、エラー処理を後回しにさせないGoらしい工夫- 自作のエラーは
errors.Newやfmt.Errorfで作れる - nil pointer dereferenceは、ポインタの
nilチェック漏れや変数シャドーイングが原因になりやすい - エラーを
_で握りつぶさず、必ず何らかの形で処理する
次に読むべき記事
エラー処理の基本を押さえたら、次はGoの並行処理の入口である「goroutineの基本:軽量スレッドで並行処理する」に進んでみてください。
エラーハンドリングと同じくらい、Go初心者がつまずきやすいポイントを解説しています。