Menu

Go言語のエラー処理:if err != nil、ラップ、エラーの判定

Goはエラーを関数から返される普通の値として扱います。errorインターフェース、if err != nil、errors.Newとfmt.Errorf、文脈を付けてエラーを返す方法、errors.Isとerrors.Asでのチェック、そして各エラーを一度だけ処理することを解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

エラーは値

失敗しうるGoの関数は、最後の戻り値として error を返します。呼び出し側はすぐにそれをチェックします。

出力:

parsed: 42
could not parse: strconv.Atoi: parsing "forty-two": invalid syntax

仕組みはこれがすべてです。例外も、trycatch も、隠れた制御の流れもありません。エラーはコードが渡した場所にしか伝わりません。代償は if err != nil の目に見える繰り返しです。その見返りとして、失敗しうる場所がすべてページ上に見え、それぞれで何が起きるかを自分で決められます。

error型

error は、メソッドを1つ持つ組み込みのインターフェースです。

type error interface {
	Error() string
}

Error() string メソッドを持つ型はどれもエラーです。nil のエラーは成功を意味します。fmt.Println(err)%v でエラーを表示すると Error() が呼ばれます。

エラーを作る

ほとんどの場合、2つの関数で足ります。

errors.New は固定のテキストを持つエラーを作ります。fmt.ErrorfPrintf と同じ書式指定子でエラーを整形します。エラーの文字列は慣習として小文字で始め、末尾に句読点を付けません。load config: open app.yaml: no such file or directory のように、たいてい長いメッセージの中に埋め込まれるからです。

if err != nilのパターン

Goらしい形は、呼んで、チェックして、早めに返すことです。成功の経路は左端に残り、それぞれの失敗は起きた時点ですぐに抜けます。

func loadUser(id int) (*User, error) {
	row, err := db.Query(id)
	if err != nil {
		return nil, err
	}
	u, err := parseUser(row)
	if err != nil {
		return nil, err
	}
	if err := u.Validate(); err != nil {
		return nil, err
	}
	return u, nil
}

注目すべき慣習が2つあります。

  • エラーのときは、他の戻り値にゼロ値(nil0"")を返します。err != nil のとき、呼び出し側はそれらを使ってはいけません。
  • 関数がエラーだけを返すなら、if err := f(); err != nilerr のスコープを if の中に収められます。外側のスコープをきれいに保てます。

エラーでreturnした後の else は避けましょう。if err != nil { return err } else { ... } は、正常系を意味もなくインデントするだけです。

エラーを返すときに文脈を加える

そのまま上に渡されたエラーは、どこから来たのかという経緯を失います。open config.yaml: no such file or directory だけでは、起動のどの段階で失敗したのかわかりません。fmt.Errorf%w で文脈を加えます。

出力:

start server: read config: open /etc/myapp/config.yaml: no such file or directory
true

それぞれの層が自分のしていたことを加え、最終的なメッセージは呼び出しの一番上から原因までの足跡のように読めます。良い文脈は操作と入力を名指しします:parse line 12fetch user 42。どの層でも「error」や「failed」を付け足すのはやめましょう。メッセージはすでにエラーです。

%w はラップします。元のエラーを新しいエラーの中に残すので、errors.Iserrors.As で見つけられます。%v はテキストをコピーするだけです。実装の詳細を呼び出し側から意図的に隠したいとき、たとえばデータベースドライバのエラー型に依存させたくないときに %v を使います。

特定のエラーをチェックする:errors.Isとerrors.As

呼び出し側が特定の種類の失敗に対応する必要がある場合があります。ファイルがないなら「デフォルトを使う」、タイムアウトなら「リトライする」といった具合です。これに答える関数が2つあり、どちらもラップのすべての層を調べます。

経験則は次のとおりです。

  • 定義済みのエラー値(io.EOFos.ErrNotExistsql.ErrNoRows のような番兵)との比較には、== ではなく errors.Is を使います。== はエラーがラップされると失敗します。
  • 型付きのエラーを取り出すには、同じ理由で型アサーションではなく errors.As を使います。errors.As は対象の型の変数へのポインタを受け取ります。
  • err.Error() のテキストでマッチさせてはいけません。メッセージはバージョン間で変わり、変わったときにテキストのマッチは何も言わずに壊れます。

独自の番兵エラーやエラー型の定義、errors.Join による複数のエラーの結合は、カスタムエラーのページで扱っています。

エラーは一度だけ処理する

エラーはちょうど一度だけ処理するべきです。処理とは、返す(たいていはラップして)、ログに出して続行する、リトライする、ユーザーへのレスポンスに変える、のいずれかです。このうち2つをしてしまうのが、Goのコードで最もよくあるエラーのバグです。

// Wrong: logged here, and returned, so it is logged again by every caller.
if err != nil {
	log.Printf("could not fetch user: %v", err)
	return err
}

// Right: add context and return. The top of the program logs once.
if err != nil {
	return fmt.Errorf("fetch user %d: %w", id, err)
}

ログに出して返すと、同じ失敗がログに何度も現れ、しかもどれも最終的なメッセージより文脈が少なくなります。エラーは何をすべきか判断できる場所(HTTPハンドラ、main、ワーカーのループ)まで流し、そこでログに出します。

エラーの行き着く先

プログラムの一番上では、何かがエラーに対して行動しなければなりません。main では、たいていエラーを表示して0以外のステータスで終了します。

引数なしで実行すると、標準エラー出力に error: usage: app <name> と表示し、ステータス1で終了します(もう一方の経路を見るには、Argsパネルに名前を入力してください)。main をこの形に保ち、実際の処理を run に置くと、プログラムがテストしやすくなり、os.Exit はdeferした呼び出しを飛ばすものの、run の中の defer 文はちゃんと実行されます。

HTTPサーバーでは一番上はハンドラです。エラーをステータスコードとクライアント向けの安全なメッセージに対応させ、詳細なメッセージは自分のためにログに出します。

無視してよいエラーといけないエラー

エラーを無視するのが正しいこともありますが、判断の結果だと読む人にわかるよう、_ で明示します。

_ = conn.SetDeadline(t) // best effort

実際には失敗しえない呼び出しもあります(strings.Builder.WriteStringbytes.Buffer.Write)。一見無害でもそうでないものもあります。書き込んだファイルの Close はデータがディスクに届かなかったことを報告しうるし、json.Marshal はチャネルや関数で失敗します。迷ったらチェックしましょう。

errcheck リンター(golangci-lint に含まれています)はチェックされていないエラーを報告します。go vet 単体ではそれを指摘しません。

エラーとpanic

Goには panic もありますが、例外の仕組みではありません。不正な入力、存在しないファイル、ネットワークの障害など、通常の動作で起こりうることにはすべてエラーを使います。panicは、バグ(ありえない状態、壊れた不変条件)と、続行しても意味のない起動時の失敗のためにとっておきます。ライブラリがAPIをまたいでpanicすることは、ほぼあってはなりません。panicとrecoverのページを参照してください。

繰り返しを減らす

if err != nil は冗長で、そのための新しい構文を追加する提案は何度も却下されてきました。Goチームは2025年に、エラー処理のための構文変更をもう追求しないと発表しています。言語の範囲内で雑音を減らすパターンもあります。

  • 早めに返し、関数を小さく保つ。 繰り返しの大半は、多くの手順をこなす長い関数から生まれます。
  • スティッキーなエラー。 一連の書き込みでは、最初のエラーを構造体のフィールドに保持し、それが設定されたら後の呼び出しを何もしないようにします。bufio.Writer はこの仕組みで、Flush の後にエラーを一度チェックすれば済みます。
  • 関数ごとに一度だけラップする。 名前付き戻り値を捕捉するdeferしたクロージャで、関数が返すすべてのエラーに同じ文脈を加えられます(deferを参照)。

よくある間違い

  • errがnilでないのに値を使う。 先にチェックし、それから使います。
  • ログに出して返す。 どちらか1つを選びます。
  • ラップした後に == で比較する。 errors.Is を使います。
  • %v で原因を失う。 隠すことが目的でない限り %w を使います。
  • 型付きのnilポインタを error として返す。 var e *MyErr; return e は呼び出し側にとってnilではありません。リテラルの nil を返します。
  • 大文字や句読点付きのメッセージ。 errors.New("Failed to connect.") はラップされると読みにくくなります。connect to db: ... と書きます。

よくある質問

Goのエラー処理はどういう仕組みですか?

失敗しうる関数は最後の戻り値として error を返します。呼び出し側はすぐにそれをチェックします:v, err := f(); if err != nil { return err }errorError() string という1つのメソッドを持つ普通のインターフェースの値で、nil は成功を意味します。例外はありません。

Goにtry/catchはありますか?

ありません。Goには例外もtry/catchもありません。想定される失敗は error の値として返され、if err != nil でチェックされます。panicrecover はありますが、プログラミングのバグや回復できない状態のためのもので、普通のエラーの流れのためのものではありません。

Goでエラーを返すには?

最後の戻り値として error を宣言し、成功時は nil を返します。固定のテキストなら errors.New("message") で、受け取ったエラーに文脈を加えるなら fmt.Errorf("reading %s: %w", name, err) でエラーを作ります。失敗時は、他の戻り値にはゼロ値を返します。

fmt.Errorfの%wと%vの違いは何ですか?

どちらも元のエラーのメッセージを新しいエラーに入れます。%w はさらにそれをラップするので、errors.Iserrors.As で元のエラーを見つけられます。%v はテキストだけを持つ新しいエラーを作ります。呼び出し側が原因をチェックする必要がありそうなら %w、隠したいなら %v を使います。

Goで返されたエラーの種類を確認するには?

io.EOFos.ErrNotExist のような番兵エラーとの比較には errors.Is(err, target) を、*fs.PathError のような特定のエラー型の取り出しには errors.As(err, &target) を使います。どちらもラップされたエラーをたどります。err.Error() の文字列を比較するのは避けましょう。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める