
前回のおさらい、そしてこのシリーズもいよいよ最終回
前回は、ゴルーチンとチャネルによる並行処理について学びました。今回はこのシリーズの最終回として、
- プログラムがうまくいかなかったときの対処法(エラーハンドリング)
- 書いたプログラムが正しく動くことを確認する方法(テスト)
の2つを学びます。この2つは、実際に人に使ってもらうプログラムを作る上で、欠かせない知識です。
エラーってなに?
プログラムは、いつも思い通りに動くとは限りません。たとえば、
- 存在しないファイルを開こうとした
- 数字を0で割ろうとした
- インターネットに接続できなかった
このように、「実行してみたら、うまくいかなかった」ということは、実際のプログラムでは頻繁に起こります。こうした「うまくいかなかったこと」を表すデータのことを、エラー(error) と呼びます。
エラーが発生したときに、プログラムが何の対処もせずに動き続けてしまうと、おかしな結果を表示したり、最悪の場合は突然プログラムが落ちてしまったりします。そのため、「エラーが起きたときに、どう対処するか」をあらかじめプログラムに書いておくことが、とても重要です。これを**エラーハンドリング(エラー処理)**と呼びます。
Goのエラーハンドリングの考え方
多くのプログラミング言語では、エラーが起きると、プログラムの流れが強制的に中断されて、専用の仕組み(try-catchなど)でそれをキャッチする、という方法がとられます。
一方でGoは、少し違う考え方を採用しています。第3回で学んだ「関数は複数の値を返せる」という特徴を思い出してください。Goでは、「エラーが起きるかもしれない処理」は、本来欲しい結果と一緒に、エラーも返り値として返す という設計が基本になっています。
実際にコードで見てみましょう。「わり算をする関数」を作りますが、0で割ろうとした場合はエラーを返すようにします。
package main
import (
"errors"
"fmt"
)
func divide(a int, b int) (int, error) {
if b == 0 {
return 0, errors.New("0で割ることはできません")
}
return a / b, nil
}
func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("エラーが発生しました:", err)
return
}
fmt.Println("結果:", result)
result2, err2 := divide(10, 0)
if err2 != nil {
fmt.Println("エラーが発生しました:", err2)
return
}
fmt.Println("結果:", result2)
}
実行結果は次のようになります。
結果: 5
エラーが発生しました: 0で割ることはできません
順番に確認していきましょう。
func divide(a int, b int) (int, error) {
戻り値の型が (int, error) となっています。「わり算の結果(int)」と、「エラー(error)」の、2つの値を返す関数だという意味です。errorはGoに標準で用意されている型で、「エラーの内容を表す値」を扱うための型です。
if b == 0 {
return 0, errors.New("0で割ることはできません")
}
もしbが0だったら、「わり算の結果としては、とりあえず0を返し、errors.New(...)で作った、新しいエラー(エラーメッセージ付き)を返す」という処理です。errorsパッケージのNew関数は、「文字列からエラーを作る」ための機能です。
return a / b, nil
正常にわり算ができた場合は、「計算結果」と、「エラーはありません」という意味の nil(ニル)を返します。nilは、「何もない、空っぽ」を表す特別な値です。「エラーは発生しませんでした」ということを、nilを返すことで表現しています。
result, err := divide(10, 2)
if err != nil {
fmt.Println("エラーが発生しました:", err)
return
}
fmt.Println("結果:", result)
呼び出す側では、必ずresult(結果)とerr(エラー)の両方を受け取ります。そして、if err != nil { ... }という形で、「エラーが発生していないかどうか」を必ず確認する、というのがGoの決まったパターンです。「もしerrがnilではなかったら(=何かエラーが起きていたら)、エラー処理をして、そこで処理を終える(return)。そうでなければ、正常な結果を使って処理を続ける」という流れになります。
この「関数を呼んだら、必ずerrを確認する」という書き方は、Goのコードのいたるところで登場します。慣れないうちは「if err != nilばかり出てきて、コードが長く感じる」と思うかもしれませんが、これは「エラーの可能性を、その場その場できちんと確認する」という、Goが大切にしている考え方の表れです。
テストってなに?
続いて、テストについて学びます。
テストとは、ひとことで言うと 「書いたプログラムが、期待通りに動いているかどうかを、自動でチェックする仕組み」 です。
たとえば、先ほどのdivide関数について、「divide(10, 2)を実行したら、本当に5が返ってくるだろうか?」「divide(10, 0)を実行したら、本当にエラーが返ってくるだろうか?」ということを確認したいとします。
これを毎回、fmt.Printlnで表示して、自分の目で結果を確認する、というやり方でもできなくはありません。しかし、プログラムが大きくなって、確認したい項目が何十個、何百個にもなってくると、目視でのチェックはとても大変になり、ミスも起きやすくなります。
そこで、「こういう入力を渡したら、こういう結果が返ってくるはずだ」という確認作業自体を、プログラムとして書いておき、実行するたびに自動でチェックしてもらう、というのがテストの考え方です。学校のテストのように、「正解」があらかじめ決まっていて、それと実際の結果を照らし合わせる、とイメージしてください。
Goには、テストを書くための仕組みが標準で用意されています。追加のインストールなどをしなくても、すぐにテストを書き始められるのも、Goの特徴の1つです。
実際にテストを書いてみよう
先ほどのdivide関数のテストを書いてみましょう。Goのテストには、いくつかのお約束のルールがあります。
- ファイル名を
対象のファイル名_test.goにする(例:main.goのテストならmain_test.go) - テスト用の関数名は
Testから始める testingパッケージを使う
まず、main.goを次のようにしておきます(先ほどのdivide関数だけを残し、main関数はシンプルにします)。
package main
import (
"errors"
"fmt"
)
func divide(a int, b int) (int, error) {
if b == 0 {
return 0, errors.New("0で割ることはできません")
}
return a / b, nil
}
func main() {
fmt.Println("プログラムを実行しました")
}
同じフォルダに、main_test.go というファイルを新しく作り、次のように書きます。
package main
import "testing"
func TestDivide_正常なわり算(t *testing.T) {
result, err := divide(10, 2)
if err != nil {
t.Errorf("エラーが発生しないはずが、発生しました: %v", err)
}
if result != 5 {
t.Errorf("結果は5になるはずですが、%dでした", result)
}
}
func TestDivide_0で割るとエラーになる(t *testing.T) {
_, err := divide(10, 0)
if err == nil {
t.Errorf("エラーが発生するはずですが、発生しませんでした")
}
}
1つずつ確認しましょう。
func TestDivide_正常なわり算(t *testing.T) {
テスト用の関数は、名前が必ずTestから始まる必要があります。それ以降の名前は自由につけられるので、「何をテストしているか」がわかる名前をつけるのがおすすめです(日本語を使うこともできます)。引数の t *testing.T は、テストの結果を報告するための道具だと思っておいてください。
result, err := divide(10, 2)
if err != nil {
t.Errorf("エラーが発生しないはずが、発生しました: %v", err)
}
if result != 5 {
t.Errorf("結果は5になるはずですが、%dでした", result)
}
divide(10, 2)を実行して、「エラーが発生していないか」「結果が期待通り5になっているか」の、2つを確認しています。t.Errorf(...)は、「もし条件に合わなかったら、このメッセージとともに、テストを失敗として報告してください」という意味です。逆に言うと、t.Errorfが一度も呼ばれなければ、そのテストは「成功」として扱われます。
2つ目のTestDivide_0で割るとエラーになる関数では、divide(10, 0)を実行したときに、「きちんとエラーが返ってくるかどうか」を確認しています。_(アンダースコア)は、「この値は使わないので、受け取ったことにはするけれど、無視します」という意味の特別な書き方です。今回は結果の数値そのものには興味がなく、エラーが発生したかどうかだけを確認したいので、結果のほうを_にしています。
テストを実行してみよう
ターミナルで、main.goとmain_test.goがあるフォルダに移動して、次のコマンドを実行します。
go test
すべてのテストが成功すると、次のように表示されます。
PASS
ok hello-go 0.123s
PASS(パス)は「合格」という意味です。試しに、main.goのdivide関数の中身を、わざと間違えてみましょう(たとえばreturn a / b, nilをreturn a + b, nilに変えるなど)。もう一度go testを実行すると、今度はテストが失敗し、t.Errorfで書いたメッセージとともに、どこがおかしいのかが表示されるはずです。
このように、「プログラムを修正するたびに、go testを1回実行するだけで、これまで書いた確認項目がすべて正しいままかどうかを、瞬時にチェックできる」というのが、テストの大きなメリットです。プログラムが育っていくにつれて、この安心感はどんどん大きくなっていきます。
まとめ、そしてこのシリーズのまとめ
今回は、以下のことを学びました。
- エラーは「処理がうまくいかなかったこと」を表すデータ
- Goでは、エラーが起きうる関数は「結果」と「エラー(
error型)」の両方を返し、呼び出す側はif err != nilで必ず確認するのが基本パターン - テストは「プログラムが期待通りに動くかを、自動でチェックする仕組み」
- Goでは、
〇〇_test.goというファイルに、Testから始まる関数を書き、go testコマンドで実行する
これで、全6回のGo言語入門シリーズは完結です。振り返ってみると、
- Goとは何か、環境構築、Hello, World!
- 変数、型、if/for/switch
- 関数とパッケージ
- 構造体とインターフェース
- ゴルーチンとチャネル(並行処理)
- エラーハンドリングとテスト
という道のりを一緒に歩いてきました。ここまで読んで、実際に自分の手でコードを書いて動かしてみた方は、もうGoプログラミングの基礎はしっかり身についています。
ここから先は、簡単な電卓プログラムを作ってみる、Webサーバーを立ち上げてみる、といった「実際に何かを作ってみる」ステップに進んでいくのがおすすめです。手を動かしながら学んだ知識を使うことで、理解はさらに深まっていきます。
これまでお読みいただき、ありがとうございました。ぜひこれからも、Goでのプログラミングを楽しんでください。


