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

のようになり、エラーの種類が書き出せるようになった。

次回、スタックトレースを出したりする予定。

以上