Go言語のエラー処理の仕組み① - 基本編
Go言語のエラー処理は、他言語と違って例外処理(try~catch等)がなく、エラー変数に対して判定を行うことでエラー処理を行います。
エラー変数に対して判定するだけですので簡単に思えます。しかしGo言語では、組み込みのエラーインターフェースを使用するため、エラー処理の実装によっては複雑な構造になっている例もあり、簡単ではありません。
この複雑なエラー処理を理解するために、まずは基本的な所から解説していきます。
最初に標準パッケージで提供されているエラー変数を作成して、Go言語のエラー処理の仕組みを学んでいきましょう。
標準パッケージのエラー変数を作成してみる
まずは標準パッケージのエラー変数を作成してみましょう。
標準パッケージのエラー変数はerrors.New関数で作成できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
func IsMikeNeko(s string) error {
if s == "みけ猫" {
return nil
} else {
return errors.New("みけ猫ではありません!")
}
}
func main() {
err := IsMikeNeko("ぶち猫")
if err != nil {
fmt.Println(err)
}
}
|
IsMikeNeko関数は渡された文字列が「みけ猫」かどうかをチェックする関数です🐱
12行目で、エラー変数をPrintln関数に渡しています。エラー変数は文字列型ではありませんが、エラー内容が表示されています。これは、エラー変数にErrorメソッドが実装されていることをPrintln関数が知っているからです。Println関数の内部でErrorメソッドを呼ぶことによってエラー内容を取得して表示しています。
このErrorメソッドはエラーインターフェースで定義されていますので詳細を見ていきましょう。
エラーインターフェースについて
エラーインターフェースの定義は、次のようになっています。
1
2
3
|
type error interface {
Error() string
}
|
※エラーインターフェースはパッケージではなく組み込みで定義されています
エラーインターフェースの定義は、string型の文字列を返却するErrorメソッドが定義されているだけです。
このErrorメソッドを任意の型に実装すれば、エラーインターフェースとして扱えるということになります。
errors.New関数で作成されるエラー変数は、このErrorメソッドが実装されているということですね。
ではerrors.New関数で作成されるエラー変数の定義を見ていきましょう。
errors.New関数で作成されるエラー変数
errors.New関数で作成されるエラー変数は下記のように、構造体を定義し、Errorメソッドが実装されたものになっています。
1
2
3
4
5
6
7
8
9
|
// ※errorsパッケージより抜粋
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
|
errorString構造体は、string型のs変数が一つだけ定義された単純なデータ構造になっています。
一方メソッドの方はerrorString型のポインタのレシーバ引数を持っています。
(レシーバ引数とは、型に対してメソッドを実装できるようにする言語仕様です。この例ではerrorString型にErrorメソッドを実装しています。)
このErrorメソッドを実装することにより、エラーインターフェースと扱えるエラー変数が作成されるというわけです。
errors.New関数の実装を見てみると返却される型はerror型となっていますが、実際にはerrorString型が返却されています。
1
2
3
4
|
// ※errorsパッケージより抜粋
func New(text string) error {
return &errorString{text}
}
|
エラーインターフェースとして扱えるため、errorString型をerror型として返却できるというわけですね。
このようにGo言語では組み込みのエラーインターフェースで関数のシグネチャを定義することにより、エラー内部のデータ構造を自由に設計できる仕組みを提供しています。
(シグネチャとは関数の名称、引数、データ型、戻り値の組み合わせのことです🙄)
エラーインターフェースの定義がなければ、文字列を表示する際にstring型に変換してPrintln関数に渡す必要が出てきてしまい処理が煩雑になってしまいます。
しかしエラーインターフェースによって実装の幅は広がりますが、学習難度が上がってしまう弊害もあります。
正しく理解するために、次は実際に自分でエラー変数を作ってみましょう。
カスタムエラー変数を作成してみる
先述したようにエラー変数は独自のデータ型にstring型を返却するErrorメソッドを実装すれば作成することができます。
新たにカスタムエラー変数、NekoCheckErrorを作ってみましょう。
1
2
3
4
5
6
7
|
type NekoCheckError struct {
ErrorString string
}
func (e *NekoCheckError) Error() string {
return e.ErrorString
}
|
データ構造を決めて、Errorメソッドを実装したのでこれで独自のエラー変数が定義できました。
このカスタムエラー変数を作成するときは、下記のようにNekoCheckError型の構造体を作成し、errors.New関数と同様に参照で返却します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
func IsMikeNekoVer2(s string) error {
if s == "みけ猫" {
return nil
} else {
return &NekoCheckError{
ErrorString: "みけ猫ではありません!(Ver2)",
}
}
}
func main() {
err = IsMikeNekoVer2("ぶち猫")
if err != nil {
fmt.Println(err)
}
}
|
Println関数の内部でErrorメソッドが呼び出されてエラー文言も表示されています。問題なく動いてますね😉
しかしこの定義では、errors.New関数と同様のデータ構造になっています。
このままではカスタムエラー変数を作成する意味がないので、少し改良してみましょう。
現在のエラー内容の文言を見てみると、何が渡されてエラーになっているのかが分かりませんね。
NekoCheckError構造体を少し改良して、渡された文字列を保持するように修正してみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
type NekoCheckErrorVer3 struct {
CheckString string
ErrorString string
}
func (e *NekoCheckErrorVer3) Error() string {
return fmt.Sprintf("%sは%s", e.CheckString, e.ErrorString)
}
func IsMikeNekoVer3(s string) error {
if s == "みけ猫" {
return nil
} else {
return &NekoCheckErrorVer3{
CheckString: s,
ErrorString: "みけ猫ではありません!(Ver3)",
}
}
}
func main() {
err = IsMikeNekoVer3("ぶち猫")
if err != nil {
fmt.Println(err)
}
}
|
新たにCheckStringという変数を追加し、チェック時にエラーとなったら渡された文字列を保持するように修正しました。
これで渡された文字列の内容が分かるようになりました。
このようにエラーインターフェースによってデータ構造を自由に定義できますし、エラー文言を返す時にもErrorメソッドで処理を書けるのでとても便利です😉
最後に
今回はGo言語のエラー処理の基本編を書いてみましたが、いかがでしたか。
私自身、エラー処理についてふんわりとした理解のままプログラムを書いてましたが、標準パッケージのエラー処理が複雑な所もあり戸惑う場面が多々ありました。
このままではいけないなぁと思い、色々勉強してみると思った以上に奥深いことが分かりました。最初にちゃんと学習する大切さを痛感した次第です😅
本記事では、エラー処理のほんの一部分のことしか書けていません。他の部分については少しずつ執筆していきたいと思います。
2022年4月14日追記
比較編を書きました。
Go言語のエラー処理の仕組み② - 比較編
プログラム全体
今回の解説で使用したすべてのコードをのせています。
コピーしてGo Playground
に貼り付ければ実行できますので、実際の動作を確認してみてください。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
package main
import (
"errors"
"fmt"
)
type NekoCheckError struct {
ErrorString string
}
func (e *NekoCheckError) Error() string {
return e.ErrorString
}
type NekoCheckErrorVer3 struct {
CheckString string
ErrorString string
}
func (e *NekoCheckErrorVer3) Error() string {
return fmt.Sprintf("%sは%s", e.CheckString, e.ErrorString)
}
func IsMikeNeko(s string) error {
if s == "みけ猫" {
return nil
} else {
return errors.New("みけ猫ではありません!")
}
}
func IsMikeNekoVer2(s string) error {
if s == "みけ猫" {
return nil
} else {
return &NekoCheckError{
ErrorString: "みけ猫ではありません!(Ver2)",
}
}
}
func IsMikeNekoVer3(s string) error {
if s == "みけ猫" {
return nil
} else {
return &NekoCheckErrorVer3{
CheckString: s,
ErrorString: "みけ猫ではありません!(Ver3)",
}
}
}
func main() {
//-----------------------------------------------------------
//標準パッケージのエラー変数を作成してみる
//-----------------------------------------------------------
err := IsMikeNeko("ぶち猫")
if err != nil {
fmt.Println(err)
}
//-----------------------------------------------------------
//カスタムエラー変数を作成してみる
//-----------------------------------------------------------
err = IsMikeNekoVer2("ぶち猫")
if err != nil {
fmt.Println(err)
}
//-----------------------------------------------------------
//カスタムエラー変数を作成してみる(改良版)
//-----------------------------------------------------------
err = IsMikeNekoVer3("ぶち猫")
if err != nil {
fmt.Println(err)
}
}
|
1
2
3
|
みけ猫ではありません!
みけ猫ではありません!(Ver2)
ぶち猫はみけ猫ではありません!(Ver3)
|
実行環境
1
2
|
~/$ go version
go version go1.18 linux/amd64
|