Go でエラーのスタックトレースを出す

Go でエラーのスタックトレースを出す方法のメモ

下記の記事を参考にした。

今goのエラーハンドリングを無難にしておく方法(2021.09現在)

pkg/errors を使う。

参考記事では

① pkg/errors.WithStackをつかうパターン
エレガントな実装だけど、stacktraceつけ忘れがち

② pkg/errors.Wrapをしまくるパターン
ちょっと強引な実装だけど、stacktraceつけ忘れにくい

と紹介されている。私は個人開発なので①の方法でやることにする。

実装に際しては、外部ライブラリから返ってきたエラーのエラーメッセージは保持しつつ、独自に定義したエラーでラップしたかったので、これがいいのかわからないが下記のようにした。

まず internal/errors/errors.go にエラーを定義する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package errors

import (
	"github.com/pkg/errors"
)

var (
	ParseError     = errors.New("Parse Error")
	IOError        = errors.New("IO Error")
	FirestoreError = errors.New("Firestore IO 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 repository

import (
	"context"
	"time"

	"cloud.google.com/go/firestore"
	"github.com/pkg/errors"

	ae "github.com/k1350/amicroblog/internal/errors"
)

const limit = 5

type Docs struct {
	Docs    []*firestore.DocumentSnapshot
	HasNext bool
	LastId  string
}

func Get(client *firestore.Client, next string) (Docs, error) {
	ctx := context.Background()
	pc := client.Collection("posts")

	var iter *firestore.DocumentIterator
	if next != "" {
		dsnap, err := pc.Doc(next).Get(ctx)
		if err != nil {
			return Docs{}, errors.WithStack(errors.WithMessage(ae.FirestoreError, err.Error()))
		}
		iter = pc.OrderBy("created", firestore.Desc).StartAt(dsnap.Data()["created"]).Limit(limit + 1).Documents(ctx)
	} else {
		iter = pc.OrderBy("created", firestore.Desc).Limit(limit + 1).Documents(ctx)
	}

	docs, err := iter.GetAll()
	if err != nil {
		return Docs{}, errors.WithStack(errors.WithMessage(ae.FirestoreError, err.Error()))
	}
	hasNext := len(docs) > limit

	lastId := ""
	if hasNext {
		lastDoc := docs[len(docs)-1]
		lastDocRef := lastDoc.Ref
		lastId = lastDocRef.ID
		docs = docs[0 : len(docs)-1]
	}

	return Docs{Docs: docs, HasNext: hasNext, LastId: lastId}, nil
}

最後にエラーの書き出し場所でスタックトレースを出すようにする。

1
2
3
4
5
6
7
func rootHandler(w http.ResponseWriter, r *http.Request) {
	err := root(w, r)
	if err != nil {
		http.Error(w, fmt.Sprintf("...: %w", err), 500)
		log.Fatalf("%+v", err)
	}
}

これで、たとえば 38 行目で Limit(limit + 1) となっているところを Limit(-1) にしてわざとエラーを出すと下記のようなログが書き出される。
12 行目に元のエラーのログが書き出されている。

 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
2021/10/17 15:53:40 Firestore IO Error
github.com/k1350/amicroblog/internal/errors.init
        /app/internal/errors/errors.go:10
runtime.doInit
        /usr/local/go/src/runtime/proc.go:6498
runtime.doInit
        /usr/local/go/src/runtime/proc.go:6475
runtime.main
        /usr/local/go/src/runtime/proc.go:238
runtime.goexit
        /usr/local/go/src/runtime/asm_amd64.s:1581
rpc error: code = InvalidArgument desc = limit is negative
github.com/k1350/amicroblog/internal/repository.Get
        /app/internal/repository/repository.go:44
main.root
        /app/cmd/amicroblog/main.go:42
main.rootHandler
        /app/cmd/amicroblog/main.go:32
net/http.HandlerFunc.ServeHTTP
        /usr/local/go/src/net/http/server.go:2046
net/http.(*ServeMux).ServeHTTP
        /usr/local/go/src/net/http/server.go:2424
net/http.serverHandler.ServeHTTP
        /usr/local/go/src/net/http/server.go:2878
net/http.(*conn).serve
        /usr/local/go/src/net/http/server.go:1929
runtime.goexit
        /usr/local/go/src/runtime/asm_amd64.s:1581

以上