Go で API サーバを作る場合のエラーハンドリングについて勉強する
Go のエラーハンドリングの中でも API サーバの場合について勉強したメモ。
Go は今現在の標準エラーパッケージでは StackTrace を出せなかったりして他にも考えることがあるのですが、とりあえず API サーバの場合にエラーを中央集権的に取り扱うには……ということを勉強したいと思う。
ググったらいきなりいい感じの記事が見つかった。
APIサーバのおけるGoのエラーハンドリングについて考えてみる
まずビジネスロジックを担う関数はエラーを返すようにして、ハンドラと分離することで、エラーハンドリングとビジネスロジックを切り離してみる。
下記が元実装。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
func inputHaldler(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
err := r.ParseMultipartForm(1024 * 5)
if err != nil {
log.Fatalln(err)
}
c := Content{
Text: r.MultipartForm.Value["text"][0],
Created: time.Now().UTC(),
Updated: time.Now().UTC(),
}
_, _, err = client.Collection("posts").Add(ctx, c)
if err != nil {
log.Fatalln("An error has occurred: %s", err)
}
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
|
これを下記のように分離した。
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
|
func inputHaldler(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
err := input(w, r)
if err != nil {
http.Error(w, fmt.Sprintf("...: %w", err), 500)
log.Fatalln(err)
}
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func input(w http.ResponseWriter, r *http.Request) error {
err := r.ParseMultipartForm(1024 * 5)
if err != nil {
return err
}
c := Content{
Text: r.MultipartForm.Value["text"][0],
Created: time.Now().UTC(),
Updated: time.Now().UTC(),
}
_, _, err = client.Collection("posts").Add(ctx, c)
if err != nil {
return err
}
return nil
}
|
試しに input 内で標準エラーパッケージで errors.New("test)
として inputHandler に返したとき、ログには
2021/10/09 14:41:03 test
と出る状態となった。
ここから更にエラーをラップするということをしてみる。
参考記事を元に独自エラーを定義してみた。
なお参考記事を丸写しすると Error 関数でスタックオーバーフローが発生するので注意。
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
|
package errors
type AppError interface {
error
Code() string
}
// パースエラー
type ParseError struct {
Err error
}
func (e *ParseError) Error() string {
msg := "Parse Error"
ne := e.Unwrap()
if ne != nil {
return msg + ": " + ne.Error()
}
return msg
}
func (e *ParseError) Unwrap() error {
return e.Err
}
func (e *ParseError) Code() string {
return "parse_error"
}
// Firestoreエラー
type FirestoreError struct {
Err error
}
func (e *FirestoreError) Error() string {
msg := "Firestore IO Error"
ne := e.Unwrap()
if ne != nil {
return msg + ": " + ne.Error()
}
return msg
}
func (e *FirestoreError) Unwrap() error {
return e.Err
}
func (e *FirestoreError) Code() string {
return "firestore_io_error"
}
|
すごく冗長に見えるので今後何とかしたい。(最終的にスタックトレースを出したいのでたぶん既存のパッケージに乗り換えると思う)
これを使って元の input 関数を書き換えた。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
func input(w http.ResponseWriter, r *http.Request) error {
err := r.ParseMultipartForm(1024 * 5)
if err != nil {
return &ae.ParseError{err}
}
c := Content{
Text: r.MultipartForm.Value["text"][0],
Created: time.Now().UTC(),
Updated: time.Now().UTC(),
}
_, _, err = client.Collection("posts").Add(ctx, c)
if err != nil {
return &ae.FirestoreError{err}
}
return nil
}
|
エラーハンドリング元は、今回はあらゆるケースで 500 エラーしか出さないので特に変更なし。
これでエラーを書き出すと
2021/10/09 15:32:46 Firestore IO Error: test
のようになり、エラーの種類が書き出せるようになった。
次回、スタックトレースを出したりする予定。
以上