golang.tokyo #28 2019.12.4
Rewrite Go error handlingusing AST transformationgolang.tokyo #28Hidetake Iwata (@int128)
View Slide
岩田 英丈 / Hidetake Iwata2Software Engineer at NTT DATA (Senior YAML/Terraform Engineer )Open Source Developer at https://github.com/int128
Agenda3お話しすること● Goにおける抽象構文木の変換と操作● アプリケーションのエラー処理を書き換える方法お話ししないこと● Goにおけるエラー処理の詳細
Error handling in GoGoでエラー処理を行うには主に以下の方法がある● errorsパッケージを利用する(Go 1.13で拡張された)● golang.org/x/xerrors パッケージを利用する● github.com/pkg/errors パッケージを利用する他にもサードパーティのパッケージが公開されている(e.g. github.com/morikuni/failure)4
以下のような目的で、アプリケーション内のエラー処理を別の方法に書き換えたいことがある● 特定の機能を使いたい(e.g. エラー型判定、スタックトレース)● 外部パッケージを使いたくない● ソースコードの可読性を改善したいRewrite the Go error handling5
例:pkg/errors.Wrapf() を xerrors.Errorf() に書き換える● import文を変更する● 関数名を変更する● 引数の順序を入れ替える● 引数のフォーマット文字列に : %w を付け加える6// pkg/errorsの場合return nil, errors.Wrapf(err, "item id=%s not found", id)// xerrorsの場合return nil, xerrors.Errorf("item id=%s not found: %w", id, err)
簡単な書き換えは正規表現による一括置換でできるが、複雑な書き換えには以下の手法が広く利用されている1. ソースコードを抽象構文木に変換する2. 抽象構文木を操作する3. 抽象構文木をソースコードに書き出す@tenntennさんのスライドが分かりやすいですhttps://www.slideshare.net/takuyaueda967/go-74970321抽象構文木の操作による書き換え7
golang.org/x/tools/go/packages パッケージ等を利用すると、ソースコードと抽象構文木を相互に変換できるGoのソースコードと抽象構文木の相互変換https://godoc.org/golang.org/x/tools/go/packagescfg := &packages.Config{/* … */}pkgs, err := packages.Load(cfg, pkgNames...) // ソースコードを解析するif err != nil {/* … */}if packages.PrintErrors(pkgs) > 0 {/* … */}for _, pkg := range pkgs {for _, file := range pkg.Syntax {err := printer.Fprint(os.Stdout, pkg.Fset, file) // 抽象構文木を出力する}}8
0 *packages.Package {1 . ID: "_/hello-go-ast-transformation"2 . Name: ""3 . PkgPath: ""4 . CompiledGoFiles: []string (len = 1) {5 . . 0: "/hello-go-ast-transformation/main.go"6 . }7 . ExportFile: ""8 . Types: *types.Package {}9 . Fset: *token.FileSet {}10 . IllTyped: false11 . Syntax: []*ast.File (len = 1) {12 . . 0: *ast.File {13 . . . Package: /hello-go-ast-transformation/main.go:1:114 . . . Name: *ast.Ident {15 . . . . NamePos: /hello-go-ast-transformation/main.go:1:916 . . . . Name: "main"17 . . . }18 . . . Decls: []ast.Decl (len = 3) {19 . . . . 0: *ast.GenDecl {20 . . . . . TokPos: /hello-go-ast-transformation/main.go:3:121 . . . . . Tok: import22 . . . . . Lparen: /hello-go-ast-transformation/main.go:3:823 . . . . . Specs: []ast.Spec (len = 5) {24 . . . . . . 0: *ast.ImportSpec {25 . . . . . . . Path: *ast.BasicLit {packages.Load() が返す抽象構文木の基本構造パッケージファイルファイルimport宣言関数宣言文文https://github.com/int128/hello-go-ast-transformation9
148 1: *ast.CallExpr {149 . Fun: *ast.SelectorExpr {150 . . X: *ast.Ident {151 . . . NamePos: hello.go:12:14152 . . . Name: "errors"153 . . }154 . . Sel: *ast.Ident {155 . . . NamePos: hello.go:12:21156 . . . Name: "Wrapf"157 . . }158 . }159 . Lparen: hello.go:12:26160 . Args: []ast.Expr (len = 3) {161 . . 0: *ast.Ident {162 . . . NamePos: hello.go:12:27163 . . . Name: "err"164 . . . Obj: *(obj @ 107)165 . . }166 . . 1: *ast.BasicLit {167 . . . ValuePos: hello.go:12:32168 . . . Kind: STRING169 . . . Value: "\"item id=%s not found\""170 . . }171 . . 2: *ast.Ident {172 . . . NamePos: hello.go:12:56173 . . . Name: "id"174 . . . Obj: *(obj @ 33)175 . . }176 . }errors.Wrapf(err, "item id=%s not found", id) に対応する部分木CallExprSelectorExprIdent“errors”Ident“Wrapf”Ident“err”BasicLit“item...”Ident“id”[]Expr10
156 1: *ast.CallExpr {157 . Fun: *ast.SelectorExpr {158 . . X: *ast.Ident {159 . . . NamePos: hello.go:13:14160 . . . Name: "xerrors"161 . . }162 . . Sel: *ast.Ident {163 . . . NamePos: hello.go:13:22164 . . . Name: "Errorf"165 . . }166 . }167 . Lparen: hello.go:13:28168 . Args: []ast.Expr (len = 3) {169 . . 0: *ast.BasicLit {170 . . . ValuePos: hello.go:13:29171 . . . Kind: STRING172 . . . Value: "\"item id=%s not found: %w\""173 . . }174 . . 1: *ast.Ident {175 . . . NamePos: hello.go:13:57176 . . . Name: "id"177 . . . Obj: *(obj @ 41)178 . . }179 . . 2: *ast.Ident {180 . . . NamePos: hello.go:13:61181 . . . Name: "err"182 . . . Obj: *(obj @ 115)183 . . }184 . }xerrors.Errorf("item id=%s not found: %w", id, err) に対応する部分木CallExprSelectorExprIdent“xerrors”Ident“Errorf”Ident“err”BasicLit“item...”Ident“id”[]Expr11
pkg/errors.Wrapf() を xerrors.Errorf() に書き換える = 抽象構文木を操作するCallExprSelectorExprIdent“errors”Ident“Wrapf”Ident“err”BasicLit“item...”Ident“id”[]ExprCallExprSelectorExprIdent“xerrors”Ident“Errorf”Ident“err”BasicLit“item...”Ident“id”[]Exprpkg/errors.Wrapf() の部分木 xerrors.Errorf() の部分木12
抽象構文木の探索go/astパッケージの Inspect() や Walk() を利用すると、抽象構文木を深さ優先探索できるast.Inspect(file, func(node ast.Node) bool {switch node := node.(type) {case *ast.CallExpr: // 関数呼び出しの場合switch fun := node.Fun.(type) {case *ast.SelectorExpr:switch x := fun.X.(type) {case *ast.Ident:if x.Sel.Name == "errors" { // パッケージ名がerrorsの場合x.Sel.Name = "xerrors" // パッケージ名を書き換える}13https://golang.org/pkg/go/ast/#Inspect
関数呼び出しの型解決抽象構文木のノードには型情報が含まれないので、そのままでは変数や関数呼び出しの実体を判断できない例:ソースコードに書かれているerrorsの実体はどれでしょう?● 標準パッケージ● 別名インポートされたパッケージ?● パッケージスコープのerrors変数?14func HelloWorld() error {return errors.New("hello")}
go/typesによる型解決15go/typesパッケージの Info.ObjectOf() で識別子の型を解決できるast.Inspect(file, func(node ast.Node) bool {switch node := node.(type) {case *ast.CallExpr: // 関数呼び出しの場合switch fun := node.Fun.(type) {case *ast.SelectorExpr:switch x := fun.X.(type) {case *ast.Ident:switch o := pkg.TypesInfo.ObjectOf(x).(type) {case *types.PkgName: // 関数呼び出しの左辺がパッケージの場合pkgPath := o.Imported().Path()https://golang.org/pkg/go/types/#Info.ObjectOf
まとめGoにおける抽象構文木の変換と操作を用いて、アプリケーションのエラー処理を書き換える方法を紹介しましたアプリケーションのエラー処理を書き換えるツールを作っているので、よかったらフィードバックをお願いします!!✨ https://github.com/int128/transerr16
We are hiring!www.nttdata-careers.com17