- 投稿日:2020-07-06T17:07:49+09:00
Goの標準ライブラリだけでルーティングするの億劫ですよね。静的解析してコード生成しません?
Goは標準ライブラリが豊富で、Webアプリが簡単に書けていいですよね。
ひと通りの機能は多少冗長でも標準ライブラリだけで実装できます。
しかし、パスパラメータが複数のときのルーティングは標準ライブラリだけだと面倒です。
Webフレームワークを導入すればいいという声が聞こえてきそうですが、世の中標準ライブラリだけで実装する需要が少なからずあるみたいなので、ルーターのコードを生成するツールを作って見ました。
ルーティングのお話
本題に入る前にフレームワークと標準ライブラリのルーティング方法を比較していきましょう。
単純なパスのとき
パスパラメータを扱わない、単純なパスをルーティングするときは次のようにできます。
標準ライブラリhttp.HandleFunc("/users", UsersHandler) // HTTPメソッドの判定など、関数内で工夫が必要ちなみに軽めのWebフレームワーク・ライブラリで有名なgin、echo、gorilla/muxなどはこんな感じです。
ginr := gin.Default() r.GET("/users", GetHandler) r.POST("/users", PostHandler)echoe := echo.New() e.GET("/users", GetHandler) e.POST("/users", PostHandler)gorilla/muxr := mux.NewRouter() r.HandleFunc("/users", UsersHandler)この程度の差なら標準ライブラリだけで問題ない印象です。
パスパラメータが1つのとき
ではパスパラメータを扱うときはどうでしょう。
/users/:user_id
をルーティングしてみます。フレームワークを使うとこんな感じで書けます。
ginr := gin.Default() r.GET("/users/:user_id", GetUserHandler) func GetUserHandler(c *gin.Context) { userId := c.Param("user_id") // 引数で渡される*gin.contextからパスパラメータを取得可能 /*...*/ }echoe := echo.New() e.GET("/users/:user_id", GetUserHandler) func GetUserHandler(c echo.Context) { userId := c.Param("user_id") // ginとほぼ同じ /*...*/ }gorilla/muxr := mux.NewRouter() r.HandleFunc("/users/{user_id}", UserHandler) func UserHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) // ライブラリの関数を使うと取得可能 userId := vars["user_id"] /*...*/ }標準ライブラリではどうかというと、
標準ライブラリhttp.HandleFunc("/users/", UserHandler) // /users/:user_idをhandle func UserHandler(w http.ResponseWriter, r *http.Request) { userId := strings.TrimPrefix(r.URL.Path, "/users/") // URLを切り取ってなんとかする }ここまでは、ああそうなのねって感じです。
文字列操作がスマートじゃないですが、特に問題はないでしょう。
パスパラメータが複数で階層になっているとき
次に
/users/:user_id/posts/:post_id/comments/:comment_id
を扱ってみましょう。フレームワークを使うとこんな感じで書けます。
ginr := gin.Default() r.GET("/users/:user_id/posts/:post_id/comments/:comment_id", GetCommentHandler) func GetCommentHandler(c *gin.Context) { userId := c.Param("user_id") postId := c.Param("post_id") commentId := c.Param("comment_id") /*...*/ }echoe := echo.New() e.GET("/users/:user_id", GetCommentHandler) func GetCommentHandler(c echo.Context) { userId := c.Param("user_id") postId := c.Param("post_id") commentId := c.Param("comment_id") /*...*/ }gorilla/muxr := mux.NewRouter() r.HandleFunc("/users/{user_id}/posts/{post_id}/comments/{comment_id}", CommentHandler) func CommentHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) userId := vars["user_id"] postId := vars["post_id"] commentId := vars["comment_id"] /*...*/ }こんな感じでパスパラメータが増えてもシンプルに取得することができます。
標準ライブラリもみてみましょう。
標準ライブラリhttp.HandleFunc("/users/", UserHandler) func UserHandler(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/users/") i := strings.Index(path, "/") userId, path := path[:i], path[i:] PostHandler(w, r, path, userId) } func PostHandler(w http.ResponseWriter, r *http.Request, path string, userId string) { path = strings.TrimPrefix(path, "/posts/") i := strings.Index(path, "/") postId, path := path[:i], path[i:] CommentHandler(w, r, path, userId, postId) } func CommentHandler(w http.ResponseWriter, r *http.Request, path string, userId string, postId string) { commentId := strings.TrimPrefix(path, "/comments/") /*...*/ }
こんなんやってられるか!!!!
- HandleFuncでパスの扱いを明示できない
- ハンドラの中身がどんどん汚れていく
- 分岐が増えると関数の階層地獄になる
- パスパラメータの話に隠れているが、HTTPMethodの分岐も考えなければならない
などなど、問題点が結構あります。
このように、標準ライブラリのルーティングはいけてない点が多すぎて快適には使えません。
標準ライブラリを使った実装の改善
実は先程のハンドラの中身がどんどん汚れていくという点は改善が可能です。
ここまでの
http.HandleFunc
を使った例は、標準ライブラリのサーバーfunc main() { http.HandleFunc("/users/", UserHandler) http.ListenAndServe(":8080", nil) }のように
http.ListenAndServe
の第2引数にnil
を渡す前提になっています。この第2引数はhttp.Handler
インターフェイスで、次のように定義されています。http/server.gotype Handler interface { ServeHTTP(ResponseWriter, *Request) }この
ServeHTTP
関数がリクエストがサーバーにリクエストが来たときのエントリポイントになっていて、登録されたパスのパターンにしたがってルーティング処理をしています。上の例では
nil
を渡しているのでhttpパッケージ内でグローバル宣言されているDefaultServeMux
が代わりに使用されます。ちなみに
http.HandleFunc
の実装は次のようになっていて、DefaultServeMux
にHandlerを登録していることがわかります。http/server.gofunc HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { DefaultServeMux.HandleFunc(pattern, handler) }少し複雑な話が続きましたが、要は
DefaultServeMux
とhttp.HandleFunc
を使わないようにすればいいわけです。http.Handler
を満たす構造体を自分で実装してhttp.ListenAndServe
に渡せば、ルーティング処理をServeHTTP関数で行えることになり、Handler内が汚れないということです。次のコードが
http.Handler
を自分で実装した例になっています。標準ライブラリのサーバー(改善版)func main() { r := NewRouter() http.ListenAndServe(":8080", r) } type Router struct{} func NewRouter() http.Handler { r := &Router{} return r } func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { // パスパラメータの分離の例 endpoint, param := SeparatePath(...) // ルーティング処理の例 switch endpoint { case "/users": switch r.Method { case http.MethodGet: UserHandler(w, r, param) default: MethodNotAllowedHandler(w, r) } /* ... */ default: NotFoundHandler(w, r) } } func UserHandler(w http.ResponseWriter, r *http.Request, userId string) { /* ... */ }本題(ルーターのコード生成)
前置きが非常に長くなってしまいましたが、ここからコード生成の話になります。
前節でルーティング処理の部分とHandlerを分離できることがわかりましたが、それでもswitch文を大量に書くことになり、面倒くさいです。面倒くさいですよね?
できればWebフレームワークみたいに簡単にルーティングしたいです。
ということで、シンプルなルーティング定義からコードを生成するstdrouterというツールを作りました。
インストール
go get してください。
$ go get -u github.com/tetsuzawa/stdrouter/cmd/stdrouter使い方
例としてこんな感じのディレクトリ構成で進めます。
ディレクトリ構成. ├── handler │ └── handler.go // UserHandlerなど ├── main.go // ListenAndServeがあるとこ └── router.go // ルーティング定義
1. router.goにルーティング定義を書く
Goのbuild tagsでビルドファイルを切り替えるので、先頭の
//+build stdrouter
を忘れないでくださいrouter.go//+build stdrouter package main import ( "net/http" "github.com/tetsuzawa/stdrouter" "github.com/tetsuzawa/stdrouter/_example/handler" ) func NewRouter() http.Handler { r := stdrouter.NewRouter() r.HandleFunc("/", http.MethodGet, handler.GetRoot) r.HandleFunc("/api", http.MethodGet, handler.GetAPIRoot) r.HandleFunc("/api/users", http.MethodGet, handler.GetUsers) r.HandleFunc("/api/products", http.MethodGet, handler.GetProducts) r.HandleFunc("/api/products", http.MethodPost, handler.CreateProducts) r.HandleFunc("/api/users/create", http.MethodPost, handler.CreateUser) r.HandleFunc("/api/users/:user_id", http.MethodGet, handler.GetUser) r.HandleFunc("/api/users/:user_id", http.MethodPatch, handler.UpdateUser) r.HandleFunc("/api/users/:user_id", http.MethodDelete, handler.DeleteUser) r.HandleFunc("/api/users/:user_id/posts", http.MethodGet, handler.GetPosts) r.HandleFunc("/api/users/:user_id/posts/:post_id", http.MethodGet, handler.GetPost) r.HandleNotFound(handler.NotFoundHandler) r.HandleMethodNotAllowed(handler.MethodNotAllowedHandler) /* ... */ return r }2. router.goと同じディレクトリで
stdrouter
を実行する$ stdrouter stdrouter: Router file generated to router_gen.go
実行が完了するとrouter_gen.goが生成されます。
ディレクトリ構成(生成後). ├── handler │ └── handler.go ├── main.go ├── router.go └── router_gen.go // 生成されたファイル
router_gen.goの中身(長いので折りたたんでます)
router_gen.go// Code generated by Standard Library Router Generator; DO NOT EDIT" //go:generate stdrouter //+build !stdrouter package main import ( "github.com/tetsuzawa/stdrouter/_example/handler" "net/http" "path" "strings" ) type Router struct{} func NewRouter() http.Handler { r := &Router{} return r } func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { handleBase(w, r, r.URL.Path) } func handleBase(w http.ResponseWriter, r *http.Request, p string) { endpoint, p := SeparatePath(p, 3) switch endpoint { case "/": switch r.Method { case http.MethodGet: handler.GetRoot(w, r) default: handler.MethodNotAllowedHandler(w, r) } case "/api": switch r.Method { case http.MethodGet: handler.GetAPIRoot(w, r) default: handler.MethodNotAllowedHandler(w, r) } case "/api/users": switch r.Method { case http.MethodGet: handler.GetUsers(w, r) default: handler.MethodNotAllowedHandler(w, r) } case "/api/products": switch r.Method { case http.MethodGet: handler.GetProducts(w, r) case http.MethodPost: handler.CreateProducts(w, r) default: handler.MethodNotAllowedHandler(w, r) } case "/api/users/create": switch r.Method { case http.MethodPost: handler.CreateUser(w, r) default: handler.MethodNotAllowedHandler(w, r) } default: endpoint, param := SeparatePath(endpoint, 2) if endpoint == "/api/users" { handleUserId(w, r, p, param[1:]) } else { handler.NotFoundHandler(w, r) } } } func handleUserId(w http.ResponseWriter, r *http.Request, p string, userId string) { endpoint, p := SeparatePath(p, 2) switch endpoint { case "/": switch r.Method { case http.MethodPatch: handler.UpdateUser(w, r, userId) case http.MethodDelete: handler.DeleteUser(w, r, userId) case http.MethodGet: handler.GetUser(w, r, userId) default: handler.MethodNotAllowedHandler(w, r) } case "/posts": switch r.Method { case http.MethodGet: handler.GetPosts(w, r, userId) default: handler.MethodNotAllowedHandler(w, r) } default: endpoint, param := SeparatePath(endpoint, 1) if endpoint == "/posts" { handlePostId(w, r, p, userId, param[1:]) } else { handler.NotFoundHandler(w, r) } } } func handlePostId(w http.ResponseWriter, r *http.Request, p string, userId string, postId string) { endpoint, p := SeparatePath(p, 0) switch endpoint { case "/": switch r.Method { case http.MethodGet: handler.GetPost(w, r, userId, postId) default: handler.MethodNotAllowedHandler(w, r) } default: handler.NotFoundHandler(w, r) } } func SeparatePath(p string, n int) (head, tail string) { p = path.Clean("/" + p) ps := strings.Split(p[1:], "/") if len(ps) < n { return p, "" } head = path.Clean("/" + strings.Join(ps[:n], "/")) tail = path.Clean("/" + strings.Join(ps[n:], "/")) return head, tail }3. 通常通り
go build
してサーバーを動かす$ go build -o server $ ./server使い方は以上です。
ちなみに一度コード生成したあとは、
go generate
でもコード生成できるようになります。これでルーティングのコードを書く作業から開放されるので、メインのロジックを書くことに集中できます!
TODO
- gorutineとcontextに対応
- importの並び替え
- SeparatePathの速い実装を検討
- デフォルトのエラーハンドラの用意
- goパッケージで利用できる関数がないか探す
- ベンチマークをとる
- リファクタ
など、やりたいことが結構あります。PRお待ちしてます!
リポジトリ: github.com/tetsuzawa/stdrouterstdrouter作成の流れ
ここからはstdrouterの作り方の話になります。興味のある方は参考にしていただけると幸いです。
最近Goの界隈で静的解析の波がきてますよね。静的解析で何が作れるのか調べてたときにコード生成ができることを知り、ネタになるとおもったので作りました。
作り方の流れとしては
- ルーティングの定義ファイルと完成形のコードを手動で書いて、途中のロジックを考える
- ルーティングの定義ファイルをAST(抽象構文木)に分解して解析する
- 泥臭くfor文とif文でコードを組み立てるロジックを書く
- リファクタする
のような感じでした。
1の定義ファイルと生成結果はrouter.goとrouter_gen.goのことです。
2のコードの解析などは初めてで完全に手探り状態だったので、まず次のコードでASTを表示してみました。
main.gopackage main import ( "go/ast" "go/parser" "go/token" "log" "os" ) func main() { filename := os.Args[1] // ファイルごとのトークンの位置を記録するFileSetを作成する fset := token.NewFileSet() // ファイル単位で構文解析を行う f, err := parser.ParseFile(fset, filename, nil, 0) if err != nil { log.Fatal("Error:", err) } ast.Print(fset, f) }これをビルドして実行するとrouter.goのASTが見れます。
ビルドと実行$ go build -o ast $ ./ast router.go ...
実行結果 router.goのAST(長いので折りたたんでます)
0 *ast.File { 1 . Package: router.go:6:1 2 . Name: *ast.Ident { 3 . . NamePos: router.go:6:9 4 . . Name: "main" 5 . } 6 . Decls: []ast.Decl (len = 2) { 7 . . 0: *ast.GenDecl { 8 . . . TokPos: router.go:8:1 9 . . . Tok: import 10 . . . Lparen: router.go:8:8 11 . . . Specs: []ast.Spec (len = 3) { 12 . . . . 0: *ast.ImportSpec { 13 . . . . . Path: *ast.BasicLit { 14 . . . . . . ValuePos: router.go:9:2 15 . . . . . . Kind: STRING 16 . . . . . . Value: "\"net/http\"" 17 . . . . . } 18 . . . . . EndPos: - 19 . . . . } 20 . . . . 1: *ast.ImportSpec { 21 . . . . . Path: *ast.BasicLit { 22 . . . . . . ValuePos: router.go:11:2 23 . . . . . . Kind: STRING 24 . . . . . . Value: "\"github.com/tetsuzawa/stdrouter\"" 25 . . . . . } 26 . . . . . EndPos: - 27 . . . . } 28 . . . . 2: *ast.ImportSpec { 29 . . . . . Path: *ast.BasicLit { 30 . . . . . . ValuePos: router.go:12:2 31 . . . . . . Kind: STRING 32 . . . . . . Value: "\"github.com/tetsuzawa/stdrouter/_example/handler\"" 33 . . . . . } 34 . . . . . EndPos: - 35 . . . . } 36 . . . } 37 . . . Rparen: router.go:13:1 38 . . } 39 . . 1: *ast.FuncDecl { 40 . . . Name: *ast.Ident { 41 . . . . NamePos: router.go:16:6 42 . . . . Name: "NewRouter" 43 . . . . Obj: *ast.Object { 44 . . . . . Kind: func 45 . . . . . Name: "NewRouter" 46 . . . . . Decl: *(obj @ 39) 47 . . . . } 48 . . . } 49 . . . Type: *ast.FuncType { 50 . . . . Func: router.go:16:1 51 . . . . Params: *ast.FieldList { 52 . . . . . Opening: router.go:16:15 53 . . . . . Closing: router.go:16:16 54 . . . . } 55 . . . . Results: *ast.FieldList { 56 . . . . . Opening: - 57 . . . . . List: []*ast.Field (len = 1) { 58 . . . . . . 0: *ast.Field { 59 . . . . . . . Type: *ast.SelectorExpr { 60 . . . . . . . . X: *ast.Ident { 61 . . . . . . . . . NamePos: router.go:16:18 62 . . . . . . . . . Name: "http" 63 . . . . . . . . } 64 . . . . . . . . Sel: *ast.Ident { 65 . . . . . . . . . NamePos: router.go:16:23 66 . . . . . . . . . Name: "Handler" 67 . . . . . . . . } 68 . . . . . . . } 69 . . . . . . } 70 . . . . . } 71 . . . . . Closing: - 72 . . . . } 73 . . . } 74 . . . Body: *ast.BlockStmt { 75 . . . . Lbrace: router.go:16:31 76 . . . . List: []ast.Stmt (len = 15) { 77 . . . . . 0: *ast.AssignStmt { 78 . . . . . . Lhs: []ast.Expr (len = 1) { 79 . . . . . . . 0: *ast.Ident { 80 . . . . . . . . NamePos: router.go:17:2 81 . . . . . . . . Name: "r" 82 . . . . . . . . Obj: *ast.Object { 83 . . . . . . . . . Kind: var 84 . . . . . . . . . Name: "r" 85 . . . . . . . . . Decl: *(obj @ 77) 86 . . . . . . . . } 87 . . . . . . . } 88 . . . . . . } 89 . . . . . . TokPos: router.go:17:4 90 . . . . . . Tok: := 91 . . . . . . Rhs: []ast.Expr (len = 1) { 92 . . . . . . . 0: *ast.CallExpr { 93 . . . . . . . . Fun: *ast.SelectorExpr { 94 . . . . . . . . . X: *ast.Ident { 95 . . . . . . . . . . NamePos: router.go:17:7 96 . . . . . . . . . . Name: "stdrouter" 97 . . . . . . . . . } 98 . . . . . . . . . Sel: *ast.Ident { 99 . . . . . . . . . . NamePos: router.go:17:17 100 . . . . . . . . . . Name: "NewRouter" 101 . . . . . . . . . } 102 . . . . . . . . } 103 . . . . . . . . Lparen: router.go:17:26 104 . . . . . . . . Ellipsis: - 105 . . . . . . . . Rparen: router.go:17:27 106 . . . . . . . } 107 . . . . . . } 108 . . . . . } 109 . . . . . 1: *ast.ExprStmt { 110 . . . . . . X: *ast.CallExpr { 111 . . . . . . . Fun: *ast.SelectorExpr { 112 . . . . . . . . X: *ast.Ident { 113 . . . . . . . . . NamePos: router.go:18:2 114 . . . . . . . . . Name: "r" 115 . . . . . . . . . Obj: *(obj @ 82) 116 . . . . . . . . } 117 . . . . . . . . Sel: *ast.Ident { 118 . . . . . . . . . NamePos: router.go:18:4 119 . . . . . . . . . Name: "HandleFunc" 120 . . . . . . . . } 121 . . . . . . . } 122 . . . . . . . Lparen: router.go:18:14 123 . . . . . . . Args: []ast.Expr (len = 3) { 124 . . . . . . . . 0: *ast.BasicLit { 125 . . . . . . . . . ValuePos: router.go:18:15 126 . . . . . . . . . Kind: STRING 127 . . . . . . . . . Value: "\"/\"" 128 . . . . . . . . } 129 . . . . . . . . 1: *ast.SelectorExpr { 130 . . . . . . . . . X: *ast.Ident { 131 . . . . . . . . . . NamePos: router.go:18:20 132 . . . . . . . . . . Name: "http" 133 . . . . . . . . . } 134 . . . . . . . . . Sel: *ast.Ident { 135 . . . . . . . . . . NamePos: router.go:18:25 136 . . . . . . . . . . Name: "MethodGet" 137 . . . . . . . . . } 138 . . . . . . . . } 139 . . . . . . . . 2: *ast.SelectorExpr { 140 . . . . . . . . . X: *ast.Ident { 141 . . . . . . . . . . NamePos: router.go:18:36 142 . . . . . . . . . . Name: "handler" 143 . . . . . . . . . } 144 . . . . . . . . . Sel: *ast.Ident { 145 . . . . . . . . . . NamePos: router.go:18:44 146 . . . . . . . . . . Name: "GetRoot" 147 . . . . . . . . . } 148 . . . . . . . . } 149 . . . . . . . } 150 . . . . . . . Ellipsis: - 151 . . . . . . . Rparen: router.go:18:51 152 . . . . . . } 153 . . . . . } 154 . . . . . 2: *ast.ExprStmt { 155 . . . . . . X: *ast.CallExpr { 156 . . . . . . . Fun: *ast.SelectorExpr { 157 . . . . . . . . X: *ast.Ident { 158 . . . . . . . . . NamePos: router.go:19:2 159 . . . . . . . . . Name: "r" 160 . . . . . . . . . Obj: *(obj @ 82) 161 . . . . . . . . } 162 . . . . . . . . Sel: *ast.Ident { 163 . . . . . . . . . NamePos: router.go:19:4 164 . . . . . . . . . Name: "HandleFunc" 165 . . . . . . . . } 166 . . . . . . . } 167 . . . . . . . Lparen: router.go:19:14 168 . . . . . . . Args: []ast.Expr (len = 3) { 169 . . . . . . . . 0: *ast.BasicLit { 170 . . . . . . . . . ValuePos: router.go:19:15 171 . . . . . . . . . Kind: STRING 172 . . . . . . . . . Value: "\"/api\"" 173 . . . . . . . . } 174 . . . . . . . . 1: *ast.SelectorExpr { 175 . . . . . . . . . X: *ast.Ident { 176 . . . . . . . . . . NamePos: router.go:19:23 177 . . . . . . . . . . Name: "http" 178 . . . . . . . . . } 179 . . . . . . . . . Sel: *ast.Ident { 180 . . . . . . . . . . NamePos: router.go:19:28 181 . . . . . . . . . . Name: "MethodGet" 182 . . . . . . . . . } 183 . . . . . . . . } 184 . . . . . . . . 2: *ast.SelectorExpr { 185 . . . . . . . . . X: *ast.Ident { 186 . . . . . . . . . . NamePos: router.go:19:39 187 . . . . . . . . . . Name: "handler" 188 . . . . . . . . . } 189 . . . . . . . . . Sel: *ast.Ident { 190 . . . . . . . . . . NamePos: router.go:19:47 191 . . . . . . . . . . Name: "GetAPIRoot" 192 . . . . . . . . . } 193 . . . . . . . . } 194 . . . . . . . } 195 . . . . . . . Ellipsis: - 196 . . . . . . . Rparen: router.go:19:57 197 . . . . . . } 198 . . . . . } 199 . . . . . 3: *ast.ExprStmt { 200 . . . . . . X: *ast.CallExpr { 201 . . . . . . . Fun: *ast.SelectorExpr { 202 . . . . . . . . X: *ast.Ident { 203 . . . . . . . . . NamePos: router.go:20:2 204 . . . . . . . . . Name: "r" 205 . . . . . . . . . Obj: *(obj @ 82) 206 . . . . . . . . } 207 . . . . . . . . Sel: *ast.Ident { 208 . . . . . . . . . NamePos: router.go:20:4 209 . . . . . . . . . Name: "HandleFunc" 210 . . . . . . . . } 211 . . . . . . . } 212 . . . . . . . Lparen: router.go:20:14 213 . . . . . . . Args: []ast.Expr (len = 3) { 214 . . . . . . . . 0: *ast.BasicLit { 215 . . . . . . . . . ValuePos: router.go:20:15 216 . . . . . . . . . Kind: STRING 217 . . . . . . . . . Value: "\"/api/users\"" 218 . . . . . . . . } 219 . . . . . . . . 1: *ast.SelectorExpr { 220 . . . . . . . . . X: *ast.Ident { 221 . . . . . . . . . . NamePos: router.go:20:29 222 . . . . . . . . . . Name: "http" 223 . . . . . . . . . } 224 . . . . . . . . . Sel: *ast.Ident { 225 . . . . . . . . . . NamePos: router.go:20:34 226 . . . . . . . . . . Name: "MethodGet" 227 . . . . . . . . . } 228 . . . . . . . . } 229 . . . . . . . . 2: *ast.SelectorExpr { 230 . . . . . . . . . X: *ast.Ident { 231 . . . . . . . . . . NamePos: router.go:20:45 232 . . . . . . . . . . Name: "handler" 233 . . . . . . . . . } 234 . . . . . . . . . Sel: *ast.Ident { 235 . . . . . . . . . . NamePos: router.go:20:53 236 . . . . . . . . . . Name: "GetUsers" 237 . . . . . . . . . } 238 . . . . . . . . } 239 . . . . . . . } 240 . . . . . . . Ellipsis: - 241 . . . . . . . Rparen: router.go:20:61 242 . . . . . . } 243 . . . . . } 244 . . . . . 4: *ast.ExprStmt { 245 . . . . . . X: *ast.CallExpr { 246 . . . . . . . Fun: *ast.SelectorExpr { 247 . . . . . . . . X: *ast.Ident { 248 . . . . . . . . . NamePos: router.go:21:2 249 . . . . . . . . . Name: "r" 250 . . . . . . . . . Obj: *(obj @ 82) 251 . . . . . . . . } 252 . . . . . . . . Sel: *ast.Ident { 253 . . . . . . . . . NamePos: router.go:21:4 254 . . . . . . . . . Name: "HandleFunc" 255 . . . . . . . . } 256 . . . . . . . } 257 . . . . . . . Lparen: router.go:21:14 258 . . . . . . . Args: []ast.Expr (len = 3) { 259 . . . . . . . . 0: *ast.BasicLit { 260 . . . . . . . . . ValuePos: router.go:21:15 261 . . . . . . . . . Kind: STRING 262 . . . . . . . . . Value: "\"/api/products\"" 263 . . . . . . . . } 264 . . . . . . . . 1: *ast.SelectorExpr { 265 . . . . . . . . . X: *ast.Ident { 266 . . . . . . . . . . NamePos: router.go:21:32 267 . . . . . . . . . . Name: "http" 268 . . . . . . . . . } 269 . . . . . . . . . Sel: *ast.Ident { 270 . . . . . . . . . . NamePos: router.go:21:37 271 . . . . . . . . . . Name: "MethodGet" 272 . . . . . . . . . } 273 . . . . . . . . } 274 . . . . . . . . 2: *ast.SelectorExpr { 275 . . . . . . . . . X: *ast.Ident { 276 . . . . . . . . . . NamePos: router.go:21:48 277 . . . . . . . . . . Name: "handler" 278 . . . . . . . . . } 279 . . . . . . . . . Sel: *ast.Ident { 280 . . . . . . . . . . NamePos: router.go:21:56 281 . . . . . . . . . . Name: "GetProducts" 282 . . . . . . . . . } 283 . . . . . . . . } 284 . . . . . . . } 285 . . . . . . . Ellipsis: - 286 . . . . . . . Rparen: router.go:21:67 287 . . . . . . } 288 . . . . . } 289 . . . . . 5: *ast.ExprStmt { 290 . . . . . . X: *ast.CallExpr { 291 . . . . . . . Fun: *ast.SelectorExpr { 292 . . . . . . . . X: *ast.Ident { 293 . . . . . . . . . NamePos: router.go:22:2 294 . . . . . . . . . Name: "r" 295 . . . . . . . . . Obj: *(obj @ 82) 296 . . . . . . . . } 297 . . . . . . . . Sel: *ast.Ident { 298 . . . . . . . . . NamePos: router.go:22:4 299 . . . . . . . . . Name: "HandleFunc" 300 . . . . . . . . } 301 . . . . . . . } 302 . . . . . . . Lparen: router.go:22:14 303 . . . . . . . Args: []ast.Expr (len = 3) { 304 . . . . . . . . 0: *ast.BasicLit { 305 . . . . . . . . . ValuePos: router.go:22:15 306 . . . . . . . . . Kind: STRING 307 . . . . . . . . . Value: "\"/api/products\"" 308 . . . . . . . . } 309 . . . . . . . . 1: *ast.SelectorExpr { 310 . . . . . . . . . X: *ast.Ident { 311 . . . . . . . . . . NamePos: router.go:22:32 312 . . . . . . . . . . Name: "http" 313 . . . . . . . . . } 314 . . . . . . . . . Sel: *ast.Ident { 315 . . . . . . . . . . NamePos: router.go:22:37 316 . . . . . . . . . . Name: "MethodPost" 317 . . . . . . . . . } 318 . . . . . . . . } 319 . . . . . . . . 2: *ast.SelectorExpr { 320 . . . . . . . . . X: *ast.Ident { 321 . . . . . . . . . . NamePos: router.go:22:49 322 . . . . . . . . . . Name: "handler" 323 . . . . . . . . . } 324 . . . . . . . . . Sel: *ast.Ident { 325 . . . . . . . . . . NamePos: router.go:22:57 326 . . . . . . . . . . Name: "CreateProducts" 327 . . . . . . . . . } 328 . . . . . . . . } 329 . . . . . . . } 330 . . . . . . . Ellipsis: - 331 . . . . . . . Rparen: router.go:22:71 332 . . . . . . } 333 . . . . . } 334 . . . . . 6: *ast.ExprStmt { 335 . . . . . . X: *ast.CallExpr { 336 . . . . . . . Fun: *ast.SelectorExpr { 337 . . . . . . . . X: *ast.Ident { 338 . . . . . . . . . NamePos: router.go:23:2 339 . . . . . . . . . Name: "r" 340 . . . . . . . . . Obj: *(obj @ 82) 341 . . . . . . . . } 342 . . . . . . . . Sel: *ast.Ident { 343 . . . . . . . . . NamePos: router.go:23:4 344 . . . . . . . . . Name: "HandleFunc" 345 . . . . . . . . } 346 . . . . . . . } 347 . . . . . . . Lparen: router.go:23:14 348 . . . . . . . Args: []ast.Expr (len = 3) { 349 . . . . . . . . 0: *ast.BasicLit { 350 . . . . . . . . . ValuePos: router.go:23:15 351 . . . . . . . . . Kind: STRING 352 . . . . . . . . . Value: "\"/api/users/create\"" 353 . . . . . . . . } 354 . . . . . . . . 1: *ast.SelectorExpr { 355 . . . . . . . . . X: *ast.Ident { 356 . . . . . . . . . . NamePos: router.go:23:36 357 . . . . . . . . . . Name: "http" 358 . . . . . . . . . } 359 . . . . . . . . . Sel: *ast.Ident { 360 . . . . . . . . . . NamePos: router.go:23:41 361 . . . . . . . . . . Name: "MethodPost" 362 . . . . . . . . . } 363 . . . . . . . . } 364 . . . . . . . . 2: *ast.SelectorExpr { 365 . . . . . . . . . X: *ast.Ident { 366 . . . . . . . . . . NamePos: router.go:23:53 367 . . . . . . . . . . Name: "handler" 368 . . . . . . . . . } 369 . . . . . . . . . Sel: *ast.Ident { 370 . . . . . . . . . . NamePos: router.go:23:61 371 . . . . . . . . . . Name: "CreateUser" 372 . . . . . . . . . } 373 . . . . . . . . } 374 . . . . . . . } 375 . . . . . . . Ellipsis: - 376 . . . . . . . Rparen: router.go:23:71 377 . . . . . . } 378 . . . . . } 379 . . . . . 7: *ast.ExprStmt { 380 . . . . . . X: *ast.CallExpr { 381 . . . . . . . Fun: *ast.SelectorExpr { 382 . . . . . . . . X: *ast.Ident { 383 . . . . . . . . . NamePos: router.go:24:2 384 . . . . . . . . . Name: "r" 385 . . . . . . . . . Obj: *(obj @ 82) 386 . . . . . . . . } 387 . . . . . . . . Sel: *ast.Ident { 388 . . . . . . . . . NamePos: router.go:24:4 389 . . . . . . . . . Name: "HandleFunc" 390 . . . . . . . . } 391 . . . . . . . } 392 . . . . . . . Lparen: router.go:24:14 393 . . . . . . . Args: []ast.Expr (len = 3) { 394 . . . . . . . . 0: *ast.BasicLit { 395 . . . . . . . . . ValuePos: router.go:24:15 396 . . . . . . . . . Kind: STRING 397 . . . . . . . . . Value: "\"/api/users/:user_id\"" 398 . . . . . . . . } 399 . . . . . . . . 1: *ast.SelectorExpr { 400 . . . . . . . . . X: *ast.Ident { 401 . . . . . . . . . . NamePos: router.go:24:38 402 . . . . . . . . . . Name: "http" 403 . . . . . . . . . } 404 . . . . . . . . . Sel: *ast.Ident { 405 . . . . . . . . . . NamePos: router.go:24:43 406 . . . . . . . . . . Name: "MethodGet" 407 . . . . . . . . . } 408 . . . . . . . . } 409 . . . . . . . . 2: *ast.SelectorExpr { 410 . . . . . . . . . X: *ast.Ident { 411 . . . . . . . . . . NamePos: router.go:24:54 412 . . . . . . . . . . Name: "handler" 413 . . . . . . . . . } 414 . . . . . . . . . Sel: *ast.Ident { 415 . . . . . . . . . . NamePos: router.go:24:62 416 . . . . . . . . . . Name: "GetUser" 417 . . . . . . . . . } 418 . . . . . . . . } 419 . . . . . . . } 420 . . . . . . . Ellipsis: - 421 . . . . . . . Rparen: router.go:24:69 422 . . . . . . } 423 . . . . . } 424 . . . . . 8: *ast.ExprStmt { 425 . . . . . . X: *ast.CallExpr { 426 . . . . . . . Fun: *ast.SelectorExpr { 427 . . . . . . . . X: *ast.Ident { 428 . . . . . . . . . NamePos: router.go:25:2 429 . . . . . . . . . Name: "r" 430 . . . . . . . . . Obj: *(obj @ 82) 431 . . . . . . . . } 432 . . . . . . . . Sel: *ast.Ident { 433 . . . . . . . . . NamePos: router.go:25:4 434 . . . . . . . . . Name: "HandleFunc" 435 . . . . . . . . } 436 . . . . . . . } 437 . . . . . . . Lparen: router.go:25:14 438 . . . . . . . Args: []ast.Expr (len = 3) { 439 . . . . . . . . 0: *ast.BasicLit { 440 . . . . . . . . . ValuePos: router.go:25:15 441 . . . . . . . . . Kind: STRING 442 . . . . . . . . . Value: "\"/api/users/:user_id\"" 443 . . . . . . . . } 444 . . . . . . . . 1: *ast.SelectorExpr { 445 . . . . . . . . . X: *ast.Ident { 446 . . . . . . . . . . NamePos: router.go:25:38 447 . . . . . . . . . . Name: "http" 448 . . . . . . . . . } 449 . . . . . . . . . Sel: *ast.Ident { 450 . . . . . . . . . . NamePos: router.go:25:43 451 . . . . . . . . . . Name: "MethodPatch" 452 . . . . . . . . . } 453 . . . . . . . . } 454 . . . . . . . . 2: *ast.SelectorExpr { 455 . . . . . . . . . X: *ast.Ident { 456 . . . . . . . . . . NamePos: router.go:25:56 457 . . . . . . . . . . Name: "handler" 458 . . . . . . . . . } 459 . . . . . . . . . Sel: *ast.Ident { 460 . . . . . . . . . . NamePos: router.go:25:64 461 . . . . . . . . . . Name: "UpdateUser" 462 . . . . . . . . . } 463 . . . . . . . . } 464 . . . . . . . } 465 . . . . . . . Ellipsis: - 466 . . . . . . . Rparen: router.go:25:74 467 . . . . . . } 468 . . . . . } 469 . . . . . 9: *ast.ExprStmt { 470 . . . . . . X: *ast.CallExpr { 471 . . . . . . . Fun: *ast.SelectorExpr { 472 . . . . . . . . X: *ast.Ident { 473 . . . . . . . . . NamePos: router.go:26:2 474 . . . . . . . . . Name: "r" 475 . . . . . . . . . Obj: *(obj @ 82) 476 . . . . . . . . } 477 . . . . . . . . Sel: *ast.Ident { 478 . . . . . . . . . NamePos: router.go:26:4 479 . . . . . . . . . Name: "HandleFunc" 480 . . . . . . . . } 481 . . . . . . . } 482 . . . . . . . Lparen: router.go:26:14 483 . . . . . . . Args: []ast.Expr (len = 3) { 484 . . . . . . . . 0: *ast.BasicLit { 485 . . . . . . . . . ValuePos: router.go:26:15 486 . . . . . . . . . Kind: STRING 487 . . . . . . . . . Value: "\"/api/users/:user_id\"" 488 . . . . . . . . } 489 . . . . . . . . 1: *ast.SelectorExpr { 490 . . . . . . . . . X: *ast.Ident { 491 . . . . . . . . . . NamePos: router.go:26:38 492 . . . . . . . . . . Name: "http" 493 . . . . . . . . . } 494 . . . . . . . . . Sel: *ast.Ident { 495 . . . . . . . . . . NamePos: router.go:26:43 496 . . . . . . . . . . Name: "MethodDelete" 497 . . . . . . . . . } 498 . . . . . . . . } 499 . . . . . . . . 2: *ast.SelectorExpr { 500 . . . . . . . . . X: *ast.Ident { 501 . . . . . . . . . . NamePos: router.go:26:57 502 . . . . . . . . . . Name: "handler" 503 . . . . . . . . . } 504 . . . . . . . . . Sel: *ast.Ident { 505 . . . . . . . . . . NamePos: router.go:26:65 506 . . . . . . . . . . Name: "DeleteUser" 507 . . . . . . . . . } 508 . . . . . . . . } 509 . . . . . . . } 510 . . . . . . . Ellipsis: - 511 . . . . . . . Rparen: router.go:26:75 512 . . . . . . } 513 . . . . . } 514 . . . . . 10: *ast.ExprStmt { 515 . . . . . . X: *ast.CallExpr { 516 . . . . . . . Fun: *ast.SelectorExpr { 517 . . . . . . . . X: *ast.Ident { 518 . . . . . . . . . NamePos: router.go:27:2 519 . . . . . . . . . Name: "r" 520 . . . . . . . . . Obj: *(obj @ 82) 521 . . . . . . . . } 522 . . . . . . . . Sel: *ast.Ident { 523 . . . . . . . . . NamePos: router.go:27:4 524 . . . . . . . . . Name: "HandleFunc" 525 . . . . . . . . } 526 . . . . . . . } 527 . . . . . . . Lparen: router.go:27:14 528 . . . . . . . Args: []ast.Expr (len = 3) { 529 . . . . . . . . 0: *ast.BasicLit { 530 . . . . . . . . . ValuePos: router.go:27:15 531 . . . . . . . . . Kind: STRING 532 . . . . . . . . . Value: "\"/api/users/:user_id/posts\"" 533 . . . . . . . . } 534 . . . . . . . . 1: *ast.SelectorExpr { 535 . . . . . . . . . X: *ast.Ident { 536 . . . . . . . . . . NamePos: router.go:27:44 537 . . . . . . . . . . Name: "http" 538 . . . . . . . . . } 539 . . . . . . . . . Sel: *ast.Ident { 540 . . . . . . . . . . NamePos: router.go:27:49 541 . . . . . . . . . . Name: "MethodGet" 542 . . . . . . . . . } 543 . . . . . . . . } 544 . . . . . . . . 2: *ast.SelectorExpr { 545 . . . . . . . . . X: *ast.Ident { 546 . . . . . . . . . . NamePos: router.go:27:60 547 . . . . . . . . . . Name: "handler" 548 . . . . . . . . . } 549 . . . . . . . . . Sel: *ast.Ident { 550 . . . . . . . . . . NamePos: router.go:27:68 551 . . . . . . . . . . Name: "GetPosts" 552 . . . . . . . . . } 553 . . . . . . . . } 554 . . . . . . . } 555 . . . . . . . Ellipsis: - 556 . . . . . . . Rparen: router.go:27:76 557 . . . . . . } 558 . . . . . } 559 . . . . . 11: *ast.ExprStmt { 560 . . . . . . X: *ast.CallExpr { 561 . . . . . . . Fun: *ast.SelectorExpr { 562 . . . . . . . . X: *ast.Ident { 563 . . . . . . . . . NamePos: router.go:28:2 564 . . . . . . . . . Name: "r" 565 . . . . . . . . . Obj: *(obj @ 82) 566 . . . . . . . . } 567 . . . . . . . . Sel: *ast.Ident { 568 . . . . . . . . . NamePos: router.go:28:4 569 . . . . . . . . . Name: "HandleFunc" 570 . . . . . . . . } 571 . . . . . . . } 572 . . . . . . . Lparen: router.go:28:14 573 . . . . . . . Args: []ast.Expr (len = 3) { 574 . . . . . . . . 0: *ast.BasicLit { 575 . . . . . . . . . ValuePos: router.go:28:15 576 . . . . . . . . . Kind: STRING 577 . . . . . . . . . Value: "\"/api/users/:user_id/posts/:post_id\"" 578 . . . . . . . . } 579 . . . . . . . . 1: *ast.SelectorExpr { 580 . . . . . . . . . X: *ast.Ident { 581 . . . . . . . . . . NamePos: router.go:28:53 582 . . . . . . . . . . Name: "http" 583 . . . . . . . . . } 584 . . . . . . . . . Sel: *ast.Ident { 585 . . . . . . . . . . NamePos: router.go:28:58 586 . . . . . . . . . . Name: "MethodGet" 587 . . . . . . . . . } 588 . . . . . . . . } 589 . . . . . . . . 2: *ast.SelectorExpr { 590 . . . . . . . . . X: *ast.Ident { 591 . . . . . . . . . . NamePos: router.go:28:69 592 . . . . . . . . . . Name: "handler" 593 . . . . . . . . . } 594 . . . . . . . . . Sel: *ast.Ident { 595 . . . . . . . . . . NamePos: router.go:28:77 596 . . . . . . . . . . Name: "GetPost" 597 . . . . . . . . . } 598 . . . . . . . . } 599 . . . . . . . } 600 . . . . . . . Ellipsis: - 601 . . . . . . . Rparen: router.go:28:84 602 . . . . . . } 603 . . . . . } 604 . . . . . 12: *ast.ExprStmt { 605 . . . . . . X: *ast.CallExpr { 606 . . . . . . . Fun: *ast.SelectorExpr { 607 . . . . . . . . X: *ast.Ident { 608 . . . . . . . . . NamePos: router.go:29:2 609 . . . . . . . . . Name: "r" 610 . . . . . . . . . Obj: *(obj @ 82) 611 . . . . . . . . } 612 . . . . . . . . Sel: *ast.Ident { 613 . . . . . . . . . NamePos: router.go:29:4 614 . . . . . . . . . Name: "HandleMethodNotAllowed" 615 . . . . . . . . } 616 . . . . . . . } 617 . . . . . . . Lparen: router.go:29:26 618 . . . . . . . Args: []ast.Expr (len = 1) { 619 . . . . . . . . 0: *ast.SelectorExpr { 620 . . . . . . . . . X: *ast.Ident { 621 . . . . . . . . . . NamePos: router.go:29:27 622 . . . . . . . . . . Name: "handler" 623 . . . . . . . . . } 624 . . . . . . . . . Sel: *ast.Ident { 625 . . . . . . . . . . NamePos: router.go:29:35 626 . . . . . . . . . . Name: "MethodNotAllowedHandler" 627 . . . . . . . . . } 628 . . . . . . . . } 629 . . . . . . . } 630 . . . . . . . Ellipsis: - 631 . . . . . . . Rparen: router.go:29:58 632 . . . . . . } 633 . . . . . } 634 . . . . . 13: *ast.ExprStmt { 635 . . . . . . X: *ast.CallExpr { 636 . . . . . . . Fun: *ast.SelectorExpr { 637 . . . . . . . . X: *ast.Ident { 638 . . . . . . . . . NamePos: router.go:30:2 639 . . . . . . . . . Name: "r" 640 . . . . . . . . . Obj: *(obj @ 82) 641 . . . . . . . . } 642 . . . . . . . . Sel: *ast.Ident { 643 . . . . . . . . . NamePos: router.go:30:4 644 . . . . . . . . . Name: "HandleNotFound" 645 . . . . . . . . } 646 . . . . . . . } 647 . . . . . . . Lparen: router.go:30:18 648 . . . . . . . Args: []ast.Expr (len = 1) { 649 . . . . . . . . 0: *ast.SelectorExpr { 650 . . . . . . . . . X: *ast.Ident { 651 . . . . . . . . . . NamePos: router.go:30:19 652 . . . . . . . . . . Name: "handler" 653 . . . . . . . . . } 654 . . . . . . . . . Sel: *ast.Ident { 655 . . . . . . . . . . NamePos: router.go:30:27 656 . . . . . . . . . . Name: "NotFoundHandler" 657 . . . . . . . . . } 658 . . . . . . . . } 659 . . . . . . . } 660 . . . . . . . Ellipsis: - 661 . . . . . . . Rparen: router.go:30:42 662 . . . . . . } 663 . . . . . } 664 . . . . . 14: *ast.ReturnStmt { 665 . . . . . . Return: router.go:34:2 666 . . . . . . Results: []ast.Expr (len = 1) { 667 . . . . . . . 0: *ast.Ident { 668 . . . . . . . . NamePos: router.go:34:9 669 . . . . . . . . Name: "r" 670 . . . . . . . . Obj: *(obj @ 82) 671 . . . . . . . } 672 . . . . . . } 673 . . . . . } 674 . . . . } 675 . . . . Rbrace: router.go:35:1 676 . . . } 677 . . } 678 . } 679 . Scope: *ast.Scope { 680 . . Objects: map[string]*ast.Object (len = 1) { 681 . . . "NewRouter": *(obj @ 43) 682 . . } 683 . } 684 . Imports: []*ast.ImportSpec (len = 3) { 685 . . 0: *(obj @ 12) 686 . . 1: *(obj @ 20) 687 . . 2: *(obj @ 28) 688 . } 689 . Unresolved: []*ast.Ident (len = 26) { 690 . . 0: *(obj @ 60) 691 . . 1: *(obj @ 94) 692 . . 2: *(obj @ 130) 693 . . 3: *(obj @ 140) 694 . . 4: *(obj @ 175) 695 . . 5: *(obj @ 185) 696 . . 6: *(obj @ 220) 697 . . 7: *(obj @ 230) 698 . . 8: *(obj @ 265) 699 . . 9: *(obj @ 275) 700 . . 10: *(obj @ 310) 701 . . 11: *(obj @ 320) 702 . . 12: *(obj @ 355) 703 . . 13: *(obj @ 365) 704 . . 14: *(obj @ 400) 705 . . 15: *(obj @ 410) 706 . . 16: *(obj @ 445) 707 . . 17: *(obj @ 455) 708 . . 18: *(obj @ 490) 709 . . 19: *(obj @ 500) 710 . . 20: *(obj @ 535) 711 . . 21: *(obj @ 545) 712 . . 22: *(obj @ 580) 713 . . 23: *(obj @ 590) 714 . . 24: *(obj @ 620) 715 . . 25: *(obj @ 650) 716 . } 717 }適切なルーティングのswitch文を生成するには、3のコード生成をする前にパスの木構造を把握する必要があります。
go/ast
パッケージにはast.Inspect
という関数が用意されていて、深さ優先でASTを探索することができます。stdrouterではこの関数を使って順々に型や関数名を確認しながらパスの木構造を構築してます。
- [/api/users, GET, GetUsers]を追加
- [/api/users/:user_id, GET, GetUser]を追加
- [/api/users/:user_id/posts, GET, GetPosts]を追加
パスの木構造が構築できたら、あとは泥臭くコードを生成していくだけです。
コードの生成には
text/template
をつかっています。他には
fmt.Spintf
を使う方法やASTを直接操作して生成する方法などがあるようです。作り終わってみると、そこまで難しいことをしているわけではないと感じました。
処理系の周りにふれるので勉強になりますし、goパッケージを使ってなにか作ってみるといいかも知れません。
参考
- goパッケージで簡単に静的解析して世界を広げよう #golang
静的解析関連でやりたいことに対する道筋が示されてて助かりました。- github/yudppp/json_snake_case
ASTの解析とコード生成の点を参考にしました。- github.com/google/wire
ディレクトリ構成やgo generateの使い方を参考にしました。- Goの標準パッケージだけでRESTfulなHandlerを作る
パスパラメータの扱いなどを参考にしました。
- 投稿日:2020-07-06T17:07:49+09:00
Goの標準ライブラリだけでルーティングするの億劫ですよね。コード生成しません?
Goは標準ライブラリが豊富で、Webアプリが簡単に書けていいですよね。
ひと通りの機能は多少冗長でも標準ライブラリだけで実装できます。
しかし、パスパラメータが複数のときのルーティングは標準ライブラリだけだと面倒です。
Webフレームワークを導入すればいいという声が聞こえてきそうですが、世の中標準ライブラリだけで実装する需要が少なからずあるみたいなので、ルーターのコードを生成するツールを作って見ました。
ルーティングのお話
本題に入る前にフレームワークと標準ライブラリのルーティング方法を比較していきましょう。
単純なパスのとき
パスパラメータを扱わない、単純なパスをルーティングするときは次のようにできます。
標準ライブラリhttp.HandleFunc("/users", UsersHandler) // HTTPメソッドの判定など、関数内で工夫が必要ちなみに軽めのWebフレームワーク・ライブラリで有名なgin、echo、gorilla/muxなどはこんな感じです。
ginr := gin.Default() r.GET("/users", GetHandler) r.POST("/users", PostHandler)echoe := echo.New() e.GET("/users", GetHandler) e.POST("/users", PostHandler)gorilla/muxr := mux.NewRouter() r.HandleFunc("/users", UsersHandler)この程度の差なら標準ライブラリだけで問題ない印象です。
パスパラメータが1つのとき
ではパスパラメータを扱うときはどうでしょう。
/users/:user_id
をルーティングしてみます。フレームワークを使うとこんな感じで書けます。
ginr := gin.Default() r.GET("/users/:user_id", GetUserHandler) func GetUserHandler(c *gin.Context) { userId := c.Param("user_id") // 引数で渡される*gin.contextからパスパラメータを取得可能 /*...*/ }echoe := echo.New() e.GET("/users/:user_id", GetUserHandler) func GetUserHandler(c echo.Context) { userId := c.Param("user_id") // ginとほぼ同じ /*...*/ }gorilla/muxr := mux.NewRouter() r.HandleFunc("/users/{user_id}", UserHandler) func UserHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) // ライブラリの関数を使うと取得可能 userId := vars["user_id"] /*...*/ }標準ライブラリではどうかというと、
標準ライブラリhttp.HandleFunc("/users/", UserHandler) // /users/:user_idをhandle func UserHandler(w http.ResponseWriter, r *http.Request) { userId := strings.TrimPrefix(r.URL.Path, "/users/") // URLを切り取ってなんとかする }ここまでは、ああそうなのねって感じです。
文字列操作がスマートじゃないですが、特に問題はないでしょう。
パスパラメータが複数で階層になっているとき
次に
/users/:user_id/posts/:post_id/comments/:comment_id
を扱ってみましょう。フレームワークを使うとこんな感じで書けます。
ginr := gin.Default() r.GET("/users/:user_id/posts/:post_id/comments/:comment_id", GetCommentHandler) func GetCommentHandler(c *gin.Context) { userId := c.Param("user_id") postId := c.Param("post_id") commentId := c.Param("comment_id") /*...*/ }echoe := echo.New() e.GET("/users/:user_id", GetCommentHandler) func GetCommentHandler(c echo.Context) { userId := c.Param("user_id") postId := c.Param("post_id") commentId := c.Param("comment_id") /*...*/ }gorilla/muxr := mux.NewRouter() r.HandleFunc("/users/{user_id}/posts/{post_id}/comments/{comment_id}", CommentHandler) func CommentHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) userId := vars["user_id"] postId := vars["post_id"] commentId := vars["comment_id"] /*...*/ }こんな感じでパスパラメータが増えてもシンプルに取得することができます。
標準ライブラリもみてみましょう。
標準ライブラリhttp.HandleFunc("/users/", UserHandler) func UserHandler(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/users/") i := strings.Index(path, "/") userId, path := path[:i], path[i:] PostHandler(w, r, path, userId) } func PostHandler(w http.ResponseWriter, r *http.Request, path string, userId string) { path = strings.TrimPrefix(path, "/posts/") i := strings.Index(path, "/") postId, path := path[:i], path[i:] CommentHandler(w, r, path, userId, postId) } func CommentHandler(w http.ResponseWriter, r *http.Request, path string, userId string, postId string) { commentId := strings.TrimPrefix(path, "/comments/") /*...*/ }
こんなんやってられるか!!!!
- HandleFuncでパスの扱いを明示できない
- ハンドラの中身がどんどん汚れていく
- 分岐が増えると関数の階層地獄になる
- パスパラメータの話に隠れているが、HTTPMethodの分岐も考えなければならない
などなど、問題点が結構あります。
このように、標準ライブラリのルーティングはいけてない点が多すぎて快適には使えません。
標準ライブラリを使った実装の改善
実は先程のハンドラの中身がどんどん汚れていくという点は改善が可能です。
ここまでの
http.HandleFunc
を使った例は、標準ライブラリのサーバーfunc main() { http.HandleFunc("/users/", UserHandler) http.ListenAndServe(":8080", nil) }のように
http.ListenAndServe
の第2引数にnil
を渡す前提になっています。この第2引数はhttp.Handler
インターフェイスで、次のように定義されています。http/server.gotype Handler interface { ServeHTTP(ResponseWriter, *Request) }この
ServeHTTP
関数がリクエストがサーバーにリクエストが来たときのエントリポイントになっていて、登録されたパスのパターンにしたがってルーティング処理をしています。上の例では
nil
を渡しているのでhttpパッケージ内でグローバル宣言されているDefaultServeMux
が代わりに使用されます。ちなみに
http.HandleFunc
の実装は次のようになっていて、DefaultServeMux
にHandlerを登録していることがわかります。http/server.gofunc HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { DefaultServeMux.HandleFunc(pattern, handler) }少し複雑な話が続きましたが、要は
DefaultServeMux
とhttp.HandleFunc
を使わないようにすればいいわけです。http.Handler
を満たす構造体を自分で実装してhttp.ListenAndServe
に渡せば、ルーティング処理をServeHTTP関数で行えることになり、Handler内が汚れないということです。次のコードが
http.Handler
を自分で実装した例になっています。標準ライブラリのサーバー(改善版)func main() { r := NewRouter() http.ListenAndServe(":8080", r) } type Router struct{} func NewRouter() http.Handler { r := &Router{} return r } func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { // パスパラメータの分離の例 endpoint, param := SeparatePath(...) // ルーティング処理の例 switch endpoint { case "/users": switch r.Method { case http.MethodGet: UserHandler(w, r, param) default: MethodNotAllowedHandler(w, r) } /* ... */ default: NotFoundHandler(w, r) } } func UserHandler(w http.ResponseWriter, r *http.Request, userId string) { /* ... */ }本題(ルーターのコード生成)
前置きが非常に長くなってしまいましたが、ここからコード生成の話になります。
前節でルーティング処理の部分とHandlerを分離できることがわかりましたが、それでもswitch文を大量に書くことになり、面倒くさいです。面倒くさいですよね?
できればWebフレームワークみたいに簡単にルーティングしたいです。
ということで、シンプルなルーティング定義からコードを生成するstdrouterというツールを作りました。
インストール
go get してください。
$ go get -u github.com/tetsuzawa/stdrouter/cmd/stdrouter使い方
例としてこんな感じのディレクトリ構成で進めます。
ディレクトリ構成. ├── handler │ └── handler.go // UserHandlerなど ├── main.go // ListenAndServeがあるとこ └── router.go // ルーティング定義
1. router.goにルーティング定義を書く
Goのbuild tagsでビルドファイルを切り替えるので、先頭の
//+build stdrouter
を忘れないでくださいrouter.go//+build stdrouter package main import ( "net/http" "github.com/tetsuzawa/stdrouter" "github.com/tetsuzawa/stdrouter/_example/handler" ) func NewRouter() http.Handler { r := stdrouter.NewRouter() r.HandleFunc("/", http.MethodGet, handler.GetRoot) r.HandleFunc("/api", http.MethodGet, handler.GetAPIRoot) r.HandleFunc("/api/users", http.MethodGet, handler.GetUsers) r.HandleFunc("/api/products", http.MethodGet, handler.GetProducts) r.HandleFunc("/api/products", http.MethodPost, handler.CreateProducts) r.HandleFunc("/api/users/create", http.MethodPost, handler.CreateUser) r.HandleFunc("/api/users/:user_id", http.MethodGet, handler.GetUser) r.HandleFunc("/api/users/:user_id", http.MethodPatch, handler.UpdateUser) r.HandleFunc("/api/users/:user_id", http.MethodDelete, handler.DeleteUser) r.HandleFunc("/api/users/:user_id/posts", http.MethodGet, handler.GetPosts) r.HandleFunc("/api/users/:user_id/posts/:post_id", http.MethodGet, handler.GetPost) r.HandleNotFound(handler.NotFoundHandler) r.HandleMethodNotAllowed(handler.MethodNotAllowedHandler) /* ... */ return r }2. router.goと同じディレクトリで
stdrouter
を実行する$ stdrouter stdrouter: Router file generated to router_gen.go
実行が完了するとrouter_gen.goが生成されます。
ディレクトリ構成(生成後). ├── handler │ └── handler.go ├── main.go ├── router.go └── router_gen.go // 生成されたファイル
router_gen.goの中身(長いので折りたたんでます)
router_gen.go// Code generated by Standard Library Router Generator; DO NOT EDIT" //go:generate stdrouter //+build !stdrouter package main import ( "github.com/tetsuzawa/stdrouter/_example/handler" "net/http" "path" "strings" ) type Router struct{} func NewRouter() http.Handler { r := &Router{} return r } func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { handleBase(w, r, r.URL.Path) } func handleBase(w http.ResponseWriter, r *http.Request, p string) { endpoint, p := SeparatePath(p, 3) switch endpoint { case "/": switch r.Method { case http.MethodGet: handler.GetRoot(w, r) default: handler.MethodNotAllowedHandler(w, r) } case "/api": switch r.Method { case http.MethodGet: handler.GetAPIRoot(w, r) default: handler.MethodNotAllowedHandler(w, r) } case "/api/users": switch r.Method { case http.MethodGet: handler.GetUsers(w, r) default: handler.MethodNotAllowedHandler(w, r) } case "/api/products": switch r.Method { case http.MethodGet: handler.GetProducts(w, r) case http.MethodPost: handler.CreateProducts(w, r) default: handler.MethodNotAllowedHandler(w, r) } case "/api/users/create": switch r.Method { case http.MethodPost: handler.CreateUser(w, r) default: handler.MethodNotAllowedHandler(w, r) } default: endpoint, param := SeparatePath(endpoint, 2) if endpoint == "/api/users" { handleUserId(w, r, p, param[1:]) } else { handler.NotFoundHandler(w, r) } } } func handleUserId(w http.ResponseWriter, r *http.Request, p string, userId string) { endpoint, p := SeparatePath(p, 2) switch endpoint { case "/": switch r.Method { case http.MethodPatch: handler.UpdateUser(w, r, userId) case http.MethodDelete: handler.DeleteUser(w, r, userId) case http.MethodGet: handler.GetUser(w, r, userId) default: handler.MethodNotAllowedHandler(w, r) } case "/posts": switch r.Method { case http.MethodGet: handler.GetPosts(w, r, userId) default: handler.MethodNotAllowedHandler(w, r) } default: endpoint, param := SeparatePath(endpoint, 1) if endpoint == "/posts" { handlePostId(w, r, p, userId, param[1:]) } else { handler.NotFoundHandler(w, r) } } } func handlePostId(w http.ResponseWriter, r *http.Request, p string, userId string, postId string) { endpoint, p := SeparatePath(p, 0) switch endpoint { case "/": switch r.Method { case http.MethodGet: handler.GetPost(w, r, userId, postId) default: handler.MethodNotAllowedHandler(w, r) } default: handler.NotFoundHandler(w, r) } } func SeparatePath(p string, n int) (head, tail string) { p = path.Clean("/" + p) ps := strings.Split(p[1:], "/") if len(ps) < n { return p, "" } head = path.Clean("/" + strings.Join(ps[:n], "/")) tail = path.Clean("/" + strings.Join(ps[n:], "/")) return head, tail }3. 通常通り
go build
してサーバーを動かす$ go build -o server $ ./server使い方は以上です。
ちなみに一度コード生成したあとは、
go generate
でもコード生成できるようになります。これでルーティングのコードを書く作業から開放されるので、メインのロジックを書くことに集中できます!
TODO
- gorutineとcontextに対応
- importの並び替え
- SeparatePathの速い実装を検討
- デフォルトのエラーハンドラの用意
- goパッケージで利用できる関数がないか探す
- ベンチマークをとる
- リファクタ
など、やりたいことが結構あります。PRお待ちしてます!
リポジトリ: github.com/tetsuzawa/stdrouterstdrouter作成の流れ
ここからはstdrouterの作り方の話になります。興味のある方は参考にしていただけると幸いです。
最近Goの界隈で静的解析の波がきてますよね。静的解析で何が作れるのか調べてたときにコード生成ができることを知り、ネタになるとおもったので作りました。
作り方の流れとしては
- ルーティングの定義ファイルと完成形のコードを手動で書いて、途中のロジックを考える
- ルーティングの定義ファイルをAST(抽象構文木)に分解して解析する
- 泥臭くfor文とif文でコードを組み立てるロジックを書く
- リファクタする
のような感じでした。
1の定義ファイルと生成結果はrouter.goとrouter_gen.goのことです。
2のコードの解析などは初めてで完全に手探り状態だったので、まず次のコードでASTを表示してみました。
main.gopackage main import ( "go/ast" "go/parser" "go/token" "log" "os" ) func main() { filename := os.Args[1] // ファイルごとのトークンの位置を記録するFileSetを作成する fset := token.NewFileSet() // ファイル単位で構文解析を行う f, err := parser.ParseFile(fset, filename, nil, 0) if err != nil { log.Fatal("Error:", err) } ast.Print(fset, f) }これをビルドして実行するとrouter.goのASTが見れます。
ビルドと実行$ go build -o ast $ ./ast router.go ...
実行結果 router.goのAST(長いので折りたたんでます)
0 *ast.File { 1 . Package: router.go:6:1 2 . Name: *ast.Ident { 3 . . NamePos: router.go:6:9 4 . . Name: "main" 5 . } 6 . Decls: []ast.Decl (len = 2) { 7 . . 0: *ast.GenDecl { 8 . . . TokPos: router.go:8:1 9 . . . Tok: import 10 . . . Lparen: router.go:8:8 11 . . . Specs: []ast.Spec (len = 3) { 12 . . . . 0: *ast.ImportSpec { 13 . . . . . Path: *ast.BasicLit { 14 . . . . . . ValuePos: router.go:9:2 15 . . . . . . Kind: STRING 16 . . . . . . Value: "\"net/http\"" 17 . . . . . } 18 . . . . . EndPos: - 19 . . . . } 20 . . . . 1: *ast.ImportSpec { 21 . . . . . Path: *ast.BasicLit { 22 . . . . . . ValuePos: router.go:11:2 23 . . . . . . Kind: STRING 24 . . . . . . Value: "\"github.com/tetsuzawa/stdrouter\"" 25 . . . . . } 26 . . . . . EndPos: - 27 . . . . } 28 . . . . 2: *ast.ImportSpec { 29 . . . . . Path: *ast.BasicLit { 30 . . . . . . ValuePos: router.go:12:2 31 . . . . . . Kind: STRING 32 . . . . . . Value: "\"github.com/tetsuzawa/stdrouter/_example/handler\"" 33 . . . . . } 34 . . . . . EndPos: - 35 . . . . } 36 . . . } 37 . . . Rparen: router.go:13:1 38 . . } 39 . . 1: *ast.FuncDecl { 40 . . . Name: *ast.Ident { 41 . . . . NamePos: router.go:16:6 42 . . . . Name: "NewRouter" 43 . . . . Obj: *ast.Object { 44 . . . . . Kind: func 45 . . . . . Name: "NewRouter" 46 . . . . . Decl: *(obj @ 39) 47 . . . . } 48 . . . } 49 . . . Type: *ast.FuncType { 50 . . . . Func: router.go:16:1 51 . . . . Params: *ast.FieldList { 52 . . . . . Opening: router.go:16:15 53 . . . . . Closing: router.go:16:16 54 . . . . } 55 . . . . Results: *ast.FieldList { 56 . . . . . Opening: - 57 . . . . . List: []*ast.Field (len = 1) { 58 . . . . . . 0: *ast.Field { 59 . . . . . . . Type: *ast.SelectorExpr { 60 . . . . . . . . X: *ast.Ident { 61 . . . . . . . . . NamePos: router.go:16:18 62 . . . . . . . . . Name: "http" 63 . . . . . . . . } 64 . . . . . . . . Sel: *ast.Ident { 65 . . . . . . . . . NamePos: router.go:16:23 66 . . . . . . . . . Name: "Handler" 67 . . . . . . . . } 68 . . . . . . . } 69 . . . . . . } 70 . . . . . } 71 . . . . . Closing: - 72 . . . . } 73 . . . } 74 . . . Body: *ast.BlockStmt { 75 . . . . Lbrace: router.go:16:31 76 . . . . List: []ast.Stmt (len = 15) { 77 . . . . . 0: *ast.AssignStmt { 78 . . . . . . Lhs: []ast.Expr (len = 1) { 79 . . . . . . . 0: *ast.Ident { 80 . . . . . . . . NamePos: router.go:17:2 81 . . . . . . . . Name: "r" 82 . . . . . . . . Obj: *ast.Object { 83 . . . . . . . . . Kind: var 84 . . . . . . . . . Name: "r" 85 . . . . . . . . . Decl: *(obj @ 77) 86 . . . . . . . . } 87 . . . . . . . } 88 . . . . . . } 89 . . . . . . TokPos: router.go:17:4 90 . . . . . . Tok: := 91 . . . . . . Rhs: []ast.Expr (len = 1) { 92 . . . . . . . 0: *ast.CallExpr { 93 . . . . . . . . Fun: *ast.SelectorExpr { 94 . . . . . . . . . X: *ast.Ident { 95 . . . . . . . . . . NamePos: router.go:17:7 96 . . . . . . . . . . Name: "stdrouter" 97 . . . . . . . . . } 98 . . . . . . . . . Sel: *ast.Ident { 99 . . . . . . . . . . NamePos: router.go:17:17 100 . . . . . . . . . . Name: "NewRouter" 101 . . . . . . . . . } 102 . . . . . . . . } 103 . . . . . . . . Lparen: router.go:17:26 104 . . . . . . . . Ellipsis: - 105 . . . . . . . . Rparen: router.go:17:27 106 . . . . . . . } 107 . . . . . . } 108 . . . . . } 109 . . . . . 1: *ast.ExprStmt { 110 . . . . . . X: *ast.CallExpr { 111 . . . . . . . Fun: *ast.SelectorExpr { 112 . . . . . . . . X: *ast.Ident { 113 . . . . . . . . . NamePos: router.go:18:2 114 . . . . . . . . . Name: "r" 115 . . . . . . . . . Obj: *(obj @ 82) 116 . . . . . . . . } 117 . . . . . . . . Sel: *ast.Ident { 118 . . . . . . . . . NamePos: router.go:18:4 119 . . . . . . . . . Name: "HandleFunc" 120 . . . . . . . . } 121 . . . . . . . } 122 . . . . . . . Lparen: router.go:18:14 123 . . . . . . . Args: []ast.Expr (len = 3) { 124 . . . . . . . . 0: *ast.BasicLit { 125 . . . . . . . . . ValuePos: router.go:18:15 126 . . . . . . . . . Kind: STRING 127 . . . . . . . . . Value: "\"/\"" 128 . . . . . . . . } 129 . . . . . . . . 1: *ast.SelectorExpr { 130 . . . . . . . . . X: *ast.Ident { 131 . . . . . . . . . . NamePos: router.go:18:20 132 . . . . . . . . . . Name: "http" 133 . . . . . . . . . } 134 . . . . . . . . . Sel: *ast.Ident { 135 . . . . . . . . . . NamePos: router.go:18:25 136 . . . . . . . . . . Name: "MethodGet" 137 . . . . . . . . . } 138 . . . . . . . . } 139 . . . . . . . . 2: *ast.SelectorExpr { 140 . . . . . . . . . X: *ast.Ident { 141 . . . . . . . . . . NamePos: router.go:18:36 142 . . . . . . . . . . Name: "handler" 143 . . . . . . . . . } 144 . . . . . . . . . Sel: *ast.Ident { 145 . . . . . . . . . . NamePos: router.go:18:44 146 . . . . . . . . . . Name: "GetRoot" 147 . . . . . . . . . } 148 . . . . . . . . } 149 . . . . . . . } 150 . . . . . . . Ellipsis: - 151 . . . . . . . Rparen: router.go:18:51 152 . . . . . . } 153 . . . . . } 154 . . . . . 2: *ast.ExprStmt { 155 . . . . . . X: *ast.CallExpr { 156 . . . . . . . Fun: *ast.SelectorExpr { 157 . . . . . . . . X: *ast.Ident { 158 . . . . . . . . . NamePos: router.go:19:2 159 . . . . . . . . . Name: "r" 160 . . . . . . . . . Obj: *(obj @ 82) 161 . . . . . . . . } 162 . . . . . . . . Sel: *ast.Ident { 163 . . . . . . . . . NamePos: router.go:19:4 164 . . . . . . . . . Name: "HandleFunc" 165 . . . . . . . . } 166 . . . . . . . } 167 . . . . . . . Lparen: router.go:19:14 168 . . . . . . . Args: []ast.Expr (len = 3) { 169 . . . . . . . . 0: *ast.BasicLit { 170 . . . . . . . . . ValuePos: router.go:19:15 171 . . . . . . . . . Kind: STRING 172 . . . . . . . . . Value: "\"/api\"" 173 . . . . . . . . } 174 . . . . . . . . 1: *ast.SelectorExpr { 175 . . . . . . . . . X: *ast.Ident { 176 . . . . . . . . . . NamePos: router.go:19:23 177 . . . . . . . . . . Name: "http" 178 . . . . . . . . . } 179 . . . . . . . . . Sel: *ast.Ident { 180 . . . . . . . . . . NamePos: router.go:19:28 181 . . . . . . . . . . Name: "MethodGet" 182 . . . . . . . . . } 183 . . . . . . . . } 184 . . . . . . . . 2: *ast.SelectorExpr { 185 . . . . . . . . . X: *ast.Ident { 186 . . . . . . . . . . NamePos: router.go:19:39 187 . . . . . . . . . . Name: "handler" 188 . . . . . . . . . } 189 . . . . . . . . . Sel: *ast.Ident { 190 . . . . . . . . . . NamePos: router.go:19:47 191 . . . . . . . . . . Name: "GetAPIRoot" 192 . . . . . . . . . } 193 . . . . . . . . } 194 . . . . . . . } 195 . . . . . . . Ellipsis: - 196 . . . . . . . Rparen: router.go:19:57 197 . . . . . . } 198 . . . . . } 199 . . . . . 3: *ast.ExprStmt { 200 . . . . . . X: *ast.CallExpr { 201 . . . . . . . Fun: *ast.SelectorExpr { 202 . . . . . . . . X: *ast.Ident { 203 . . . . . . . . . NamePos: router.go:20:2 204 . . . . . . . . . Name: "r" 205 . . . . . . . . . Obj: *(obj @ 82) 206 . . . . . . . . } 207 . . . . . . . . Sel: *ast.Ident { 208 . . . . . . . . . NamePos: router.go:20:4 209 . . . . . . . . . Name: "HandleFunc" 210 . . . . . . . . } 211 . . . . . . . } 212 . . . . . . . Lparen: router.go:20:14 213 . . . . . . . Args: []ast.Expr (len = 3) { 214 . . . . . . . . 0: *ast.BasicLit { 215 . . . . . . . . . ValuePos: router.go:20:15 216 . . . . . . . . . Kind: STRING 217 . . . . . . . . . Value: "\"/api/users\"" 218 . . . . . . . . } 219 . . . . . . . . 1: *ast.SelectorExpr { 220 . . . . . . . . . X: *ast.Ident { 221 . . . . . . . . . . NamePos: router.go:20:29 222 . . . . . . . . . . Name: "http" 223 . . . . . . . . . } 224 . . . . . . . . . Sel: *ast.Ident { 225 . . . . . . . . . . NamePos: router.go:20:34 226 . . . . . . . . . . Name: "MethodGet" 227 . . . . . . . . . } 228 . . . . . . . . } 229 . . . . . . . . 2: *ast.SelectorExpr { 230 . . . . . . . . . X: *ast.Ident { 231 . . . . . . . . . . NamePos: router.go:20:45 232 . . . . . . . . . . Name: "handler" 233 . . . . . . . . . } 234 . . . . . . . . . Sel: *ast.Ident { 235 . . . . . . . . . . NamePos: router.go:20:53 236 . . . . . . . . . . Name: "GetUsers" 237 . . . . . . . . . } 238 . . . . . . . . } 239 . . . . . . . } 240 . . . . . . . Ellipsis: - 241 . . . . . . . Rparen: router.go:20:61 242 . . . . . . } 243 . . . . . } 244 . . . . . 4: *ast.ExprStmt { 245 . . . . . . X: *ast.CallExpr { 246 . . . . . . . Fun: *ast.SelectorExpr { 247 . . . . . . . . X: *ast.Ident { 248 . . . . . . . . . NamePos: router.go:21:2 249 . . . . . . . . . Name: "r" 250 . . . . . . . . . Obj: *(obj @ 82) 251 . . . . . . . . } 252 . . . . . . . . Sel: *ast.Ident { 253 . . . . . . . . . NamePos: router.go:21:4 254 . . . . . . . . . Name: "HandleFunc" 255 . . . . . . . . } 256 . . . . . . . } 257 . . . . . . . Lparen: router.go:21:14 258 . . . . . . . Args: []ast.Expr (len = 3) { 259 . . . . . . . . 0: *ast.BasicLit { 260 . . . . . . . . . ValuePos: router.go:21:15 261 . . . . . . . . . Kind: STRING 262 . . . . . . . . . Value: "\"/api/products\"" 263 . . . . . . . . } 264 . . . . . . . . 1: *ast.SelectorExpr { 265 . . . . . . . . . X: *ast.Ident { 266 . . . . . . . . . . NamePos: router.go:21:32 267 . . . . . . . . . . Name: "http" 268 . . . . . . . . . } 269 . . . . . . . . . Sel: *ast.Ident { 270 . . . . . . . . . . NamePos: router.go:21:37 271 . . . . . . . . . . Name: "MethodGet" 272 . . . . . . . . . } 273 . . . . . . . . } 274 . . . . . . . . 2: *ast.SelectorExpr { 275 . . . . . . . . . X: *ast.Ident { 276 . . . . . . . . . . NamePos: router.go:21:48 277 . . . . . . . . . . Name: "handler" 278 . . . . . . . . . } 279 . . . . . . . . . Sel: *ast.Ident { 280 . . . . . . . . . . NamePos: router.go:21:56 281 . . . . . . . . . . Name: "GetProducts" 282 . . . . . . . . . } 283 . . . . . . . . } 284 . . . . . . . } 285 . . . . . . . Ellipsis: - 286 . . . . . . . Rparen: router.go:21:67 287 . . . . . . } 288 . . . . . } 289 . . . . . 5: *ast.ExprStmt { 290 . . . . . . X: *ast.CallExpr { 291 . . . . . . . Fun: *ast.SelectorExpr { 292 . . . . . . . . X: *ast.Ident { 293 . . . . . . . . . NamePos: router.go:22:2 294 . . . . . . . . . Name: "r" 295 . . . . . . . . . Obj: *(obj @ 82) 296 . . . . . . . . } 297 . . . . . . . . Sel: *ast.Ident { 298 . . . . . . . . . NamePos: router.go:22:4 299 . . . . . . . . . Name: "HandleFunc" 300 . . . . . . . . } 301 . . . . . . . } 302 . . . . . . . Lparen: router.go:22:14 303 . . . . . . . Args: []ast.Expr (len = 3) { 304 . . . . . . . . 0: *ast.BasicLit { 305 . . . . . . . . . ValuePos: router.go:22:15 306 . . . . . . . . . Kind: STRING 307 . . . . . . . . . Value: "\"/api/products\"" 308 . . . . . . . . } 309 . . . . . . . . 1: *ast.SelectorExpr { 310 . . . . . . . . . X: *ast.Ident { 311 . . . . . . . . . . NamePos: router.go:22:32 312 . . . . . . . . . . Name: "http" 313 . . . . . . . . . } 314 . . . . . . . . . Sel: *ast.Ident { 315 . . . . . . . . . . NamePos: router.go:22:37 316 . . . . . . . . . . Name: "MethodPost" 317 . . . . . . . . . } 318 . . . . . . . . } 319 . . . . . . . . 2: *ast.SelectorExpr { 320 . . . . . . . . . X: *ast.Ident { 321 . . . . . . . . . . NamePos: router.go:22:49 322 . . . . . . . . . . Name: "handler" 323 . . . . . . . . . } 324 . . . . . . . . . Sel: *ast.Ident { 325 . . . . . . . . . . NamePos: router.go:22:57 326 . . . . . . . . . . Name: "CreateProducts" 327 . . . . . . . . . } 328 . . . . . . . . } 329 . . . . . . . } 330 . . . . . . . Ellipsis: - 331 . . . . . . . Rparen: router.go:22:71 332 . . . . . . } 333 . . . . . } 334 . . . . . 6: *ast.ExprStmt { 335 . . . . . . X: *ast.CallExpr { 336 . . . . . . . Fun: *ast.SelectorExpr { 337 . . . . . . . . X: *ast.Ident { 338 . . . . . . . . . NamePos: router.go:23:2 339 . . . . . . . . . Name: "r" 340 . . . . . . . . . Obj: *(obj @ 82) 341 . . . . . . . . } 342 . . . . . . . . Sel: *ast.Ident { 343 . . . . . . . . . NamePos: router.go:23:4 344 . . . . . . . . . Name: "HandleFunc" 345 . . . . . . . . } 346 . . . . . . . } 347 . . . . . . . Lparen: router.go:23:14 348 . . . . . . . Args: []ast.Expr (len = 3) { 349 . . . . . . . . 0: *ast.BasicLit { 350 . . . . . . . . . ValuePos: router.go:23:15 351 . . . . . . . . . Kind: STRING 352 . . . . . . . . . Value: "\"/api/users/create\"" 353 . . . . . . . . } 354 . . . . . . . . 1: *ast.SelectorExpr { 355 . . . . . . . . . X: *ast.Ident { 356 . . . . . . . . . . NamePos: router.go:23:36 357 . . . . . . . . . . Name: "http" 358 . . . . . . . . . } 359 . . . . . . . . . Sel: *ast.Ident { 360 . . . . . . . . . . NamePos: router.go:23:41 361 . . . . . . . . . . Name: "MethodPost" 362 . . . . . . . . . } 363 . . . . . . . . } 364 . . . . . . . . 2: *ast.SelectorExpr { 365 . . . . . . . . . X: *ast.Ident { 366 . . . . . . . . . . NamePos: router.go:23:53 367 . . . . . . . . . . Name: "handler" 368 . . . . . . . . . } 369 . . . . . . . . . Sel: *ast.Ident { 370 . . . . . . . . . . NamePos: router.go:23:61 371 . . . . . . . . . . Name: "CreateUser" 372 . . . . . . . . . } 373 . . . . . . . . } 374 . . . . . . . } 375 . . . . . . . Ellipsis: - 376 . . . . . . . Rparen: router.go:23:71 377 . . . . . . } 378 . . . . . } 379 . . . . . 7: *ast.ExprStmt { 380 . . . . . . X: *ast.CallExpr { 381 . . . . . . . Fun: *ast.SelectorExpr { 382 . . . . . . . . X: *ast.Ident { 383 . . . . . . . . . NamePos: router.go:24:2 384 . . . . . . . . . Name: "r" 385 . . . . . . . . . Obj: *(obj @ 82) 386 . . . . . . . . } 387 . . . . . . . . Sel: *ast.Ident { 388 . . . . . . . . . NamePos: router.go:24:4 389 . . . . . . . . . Name: "HandleFunc" 390 . . . . . . . . } 391 . . . . . . . } 392 . . . . . . . Lparen: router.go:24:14 393 . . . . . . . Args: []ast.Expr (len = 3) { 394 . . . . . . . . 0: *ast.BasicLit { 395 . . . . . . . . . ValuePos: router.go:24:15 396 . . . . . . . . . Kind: STRING 397 . . . . . . . . . Value: "\"/api/users/:user_id\"" 398 . . . . . . . . } 399 . . . . . . . . 1: *ast.SelectorExpr { 400 . . . . . . . . . X: *ast.Ident { 401 . . . . . . . . . . NamePos: router.go:24:38 402 . . . . . . . . . . Name: "http" 403 . . . . . . . . . } 404 . . . . . . . . . Sel: *ast.Ident { 405 . . . . . . . . . . NamePos: router.go:24:43 406 . . . . . . . . . . Name: "MethodGet" 407 . . . . . . . . . } 408 . . . . . . . . } 409 . . . . . . . . 2: *ast.SelectorExpr { 410 . . . . . . . . . X: *ast.Ident { 411 . . . . . . . . . . NamePos: router.go:24:54 412 . . . . . . . . . . Name: "handler" 413 . . . . . . . . . } 414 . . . . . . . . . Sel: *ast.Ident { 415 . . . . . . . . . . NamePos: router.go:24:62 416 . . . . . . . . . . Name: "GetUser" 417 . . . . . . . . . } 418 . . . . . . . . } 419 . . . . . . . } 420 . . . . . . . Ellipsis: - 421 . . . . . . . Rparen: router.go:24:69 422 . . . . . . } 423 . . . . . } 424 . . . . . 8: *ast.ExprStmt { 425 . . . . . . X: *ast.CallExpr { 426 . . . . . . . Fun: *ast.SelectorExpr { 427 . . . . . . . . X: *ast.Ident { 428 . . . . . . . . . NamePos: router.go:25:2 429 . . . . . . . . . Name: "r" 430 . . . . . . . . . Obj: *(obj @ 82) 431 . . . . . . . . } 432 . . . . . . . . Sel: *ast.Ident { 433 . . . . . . . . . NamePos: router.go:25:4 434 . . . . . . . . . Name: "HandleFunc" 435 . . . . . . . . } 436 . . . . . . . } 437 . . . . . . . Lparen: router.go:25:14 438 . . . . . . . Args: []ast.Expr (len = 3) { 439 . . . . . . . . 0: *ast.BasicLit { 440 . . . . . . . . . ValuePos: router.go:25:15 441 . . . . . . . . . Kind: STRING 442 . . . . . . . . . Value: "\"/api/users/:user_id\"" 443 . . . . . . . . } 444 . . . . . . . . 1: *ast.SelectorExpr { 445 . . . . . . . . . X: *ast.Ident { 446 . . . . . . . . . . NamePos: router.go:25:38 447 . . . . . . . . . . Name: "http" 448 . . . . . . . . . } 449 . . . . . . . . . Sel: *ast.Ident { 450 . . . . . . . . . . NamePos: router.go:25:43 451 . . . . . . . . . . Name: "MethodPatch" 452 . . . . . . . . . } 453 . . . . . . . . } 454 . . . . . . . . 2: *ast.SelectorExpr { 455 . . . . . . . . . X: *ast.Ident { 456 . . . . . . . . . . NamePos: router.go:25:56 457 . . . . . . . . . . Name: "handler" 458 . . . . . . . . . } 459 . . . . . . . . . Sel: *ast.Ident { 460 . . . . . . . . . . NamePos: router.go:25:64 461 . . . . . . . . . . Name: "UpdateUser" 462 . . . . . . . . . } 463 . . . . . . . . } 464 . . . . . . . } 465 . . . . . . . Ellipsis: - 466 . . . . . . . Rparen: router.go:25:74 467 . . . . . . } 468 . . . . . } 469 . . . . . 9: *ast.ExprStmt { 470 . . . . . . X: *ast.CallExpr { 471 . . . . . . . Fun: *ast.SelectorExpr { 472 . . . . . . . . X: *ast.Ident { 473 . . . . . . . . . NamePos: router.go:26:2 474 . . . . . . . . . Name: "r" 475 . . . . . . . . . Obj: *(obj @ 82) 476 . . . . . . . . } 477 . . . . . . . . Sel: *ast.Ident { 478 . . . . . . . . . NamePos: router.go:26:4 479 . . . . . . . . . Name: "HandleFunc" 480 . . . . . . . . } 481 . . . . . . . } 482 . . . . . . . Lparen: router.go:26:14 483 . . . . . . . Args: []ast.Expr (len = 3) { 484 . . . . . . . . 0: *ast.BasicLit { 485 . . . . . . . . . ValuePos: router.go:26:15 486 . . . . . . . . . Kind: STRING 487 . . . . . . . . . Value: "\"/api/users/:user_id\"" 488 . . . . . . . . } 489 . . . . . . . . 1: *ast.SelectorExpr { 490 . . . . . . . . . X: *ast.Ident { 491 . . . . . . . . . . NamePos: router.go:26:38 492 . . . . . . . . . . Name: "http" 493 . . . . . . . . . } 494 . . . . . . . . . Sel: *ast.Ident { 495 . . . . . . . . . . NamePos: router.go:26:43 496 . . . . . . . . . . Name: "MethodDelete" 497 . . . . . . . . . } 498 . . . . . . . . } 499 . . . . . . . . 2: *ast.SelectorExpr { 500 . . . . . . . . . X: *ast.Ident { 501 . . . . . . . . . . NamePos: router.go:26:57 502 . . . . . . . . . . Name: "handler" 503 . . . . . . . . . } 504 . . . . . . . . . Sel: *ast.Ident { 505 . . . . . . . . . . NamePos: router.go:26:65 506 . . . . . . . . . . Name: "DeleteUser" 507 . . . . . . . . . } 508 . . . . . . . . } 509 . . . . . . . } 510 . . . . . . . Ellipsis: - 511 . . . . . . . Rparen: router.go:26:75 512 . . . . . . } 513 . . . . . } 514 . . . . . 10: *ast.ExprStmt { 515 . . . . . . X: *ast.CallExpr { 516 . . . . . . . Fun: *ast.SelectorExpr { 517 . . . . . . . . X: *ast.Ident { 518 . . . . . . . . . NamePos: router.go:27:2 519 . . . . . . . . . Name: "r" 520 . . . . . . . . . Obj: *(obj @ 82) 521 . . . . . . . . } 522 . . . . . . . . Sel: *ast.Ident { 523 . . . . . . . . . NamePos: router.go:27:4 524 . . . . . . . . . Name: "HandleFunc" 525 . . . . . . . . } 526 . . . . . . . } 527 . . . . . . . Lparen: router.go:27:14 528 . . . . . . . Args: []ast.Expr (len = 3) { 529 . . . . . . . . 0: *ast.BasicLit { 530 . . . . . . . . . ValuePos: router.go:27:15 531 . . . . . . . . . Kind: STRING 532 . . . . . . . . . Value: "\"/api/users/:user_id/posts\"" 533 . . . . . . . . } 534 . . . . . . . . 1: *ast.SelectorExpr { 535 . . . . . . . . . X: *ast.Ident { 536 . . . . . . . . . . NamePos: router.go:27:44 537 . . . . . . . . . . Name: "http" 538 . . . . . . . . . } 539 . . . . . . . . . Sel: *ast.Ident { 540 . . . . . . . . . . NamePos: router.go:27:49 541 . . . . . . . . . . Name: "MethodGet" 542 . . . . . . . . . } 543 . . . . . . . . } 544 . . . . . . . . 2: *ast.SelectorExpr { 545 . . . . . . . . . X: *ast.Ident { 546 . . . . . . . . . . NamePos: router.go:27:60 547 . . . . . . . . . . Name: "handler" 548 . . . . . . . . . } 549 . . . . . . . . . Sel: *ast.Ident { 550 . . . . . . . . . . NamePos: router.go:27:68 551 . . . . . . . . . . Name: "GetPosts" 552 . . . . . . . . . } 553 . . . . . . . . } 554 . . . . . . . } 555 . . . . . . . Ellipsis: - 556 . . . . . . . Rparen: router.go:27:76 557 . . . . . . } 558 . . . . . } 559 . . . . . 11: *ast.ExprStmt { 560 . . . . . . X: *ast.CallExpr { 561 . . . . . . . Fun: *ast.SelectorExpr { 562 . . . . . . . . X: *ast.Ident { 563 . . . . . . . . . NamePos: router.go:28:2 564 . . . . . . . . . Name: "r" 565 . . . . . . . . . Obj: *(obj @ 82) 566 . . . . . . . . } 567 . . . . . . . . Sel: *ast.Ident { 568 . . . . . . . . . NamePos: router.go:28:4 569 . . . . . . . . . Name: "HandleFunc" 570 . . . . . . . . } 571 . . . . . . . } 572 . . . . . . . Lparen: router.go:28:14 573 . . . . . . . Args: []ast.Expr (len = 3) { 574 . . . . . . . . 0: *ast.BasicLit { 575 . . . . . . . . . ValuePos: router.go:28:15 576 . . . . . . . . . Kind: STRING 577 . . . . . . . . . Value: "\"/api/users/:user_id/posts/:post_id\"" 578 . . . . . . . . } 579 . . . . . . . . 1: *ast.SelectorExpr { 580 . . . . . . . . . X: *ast.Ident { 581 . . . . . . . . . . NamePos: router.go:28:53 582 . . . . . . . . . . Name: "http" 583 . . . . . . . . . } 584 . . . . . . . . . Sel: *ast.Ident { 585 . . . . . . . . . . NamePos: router.go:28:58 586 . . . . . . . . . . Name: "MethodGet" 587 . . . . . . . . . } 588 . . . . . . . . } 589 . . . . . . . . 2: *ast.SelectorExpr { 590 . . . . . . . . . X: *ast.Ident { 591 . . . . . . . . . . NamePos: router.go:28:69 592 . . . . . . . . . . Name: "handler" 593 . . . . . . . . . } 594 . . . . . . . . . Sel: *ast.Ident { 595 . . . . . . . . . . NamePos: router.go:28:77 596 . . . . . . . . . . Name: "GetPost" 597 . . . . . . . . . } 598 . . . . . . . . } 599 . . . . . . . } 600 . . . . . . . Ellipsis: - 601 . . . . . . . Rparen: router.go:28:84 602 . . . . . . } 603 . . . . . } 604 . . . . . 12: *ast.ExprStmt { 605 . . . . . . X: *ast.CallExpr { 606 . . . . . . . Fun: *ast.SelectorExpr { 607 . . . . . . . . X: *ast.Ident { 608 . . . . . . . . . NamePos: router.go:29:2 609 . . . . . . . . . Name: "r" 610 . . . . . . . . . Obj: *(obj @ 82) 611 . . . . . . . . } 612 . . . . . . . . Sel: *ast.Ident { 613 . . . . . . . . . NamePos: router.go:29:4 614 . . . . . . . . . Name: "HandleMethodNotAllowed" 615 . . . . . . . . } 616 . . . . . . . } 617 . . . . . . . Lparen: router.go:29:26 618 . . . . . . . Args: []ast.Expr (len = 1) { 619 . . . . . . . . 0: *ast.SelectorExpr { 620 . . . . . . . . . X: *ast.Ident { 621 . . . . . . . . . . NamePos: router.go:29:27 622 . . . . . . . . . . Name: "handler" 623 . . . . . . . . . } 624 . . . . . . . . . Sel: *ast.Ident { 625 . . . . . . . . . . NamePos: router.go:29:35 626 . . . . . . . . . . Name: "MethodNotAllowedHandler" 627 . . . . . . . . . } 628 . . . . . . . . } 629 . . . . . . . } 630 . . . . . . . Ellipsis: - 631 . . . . . . . Rparen: router.go:29:58 632 . . . . . . } 633 . . . . . } 634 . . . . . 13: *ast.ExprStmt { 635 . . . . . . X: *ast.CallExpr { 636 . . . . . . . Fun: *ast.SelectorExpr { 637 . . . . . . . . X: *ast.Ident { 638 . . . . . . . . . NamePos: router.go:30:2 639 . . . . . . . . . Name: "r" 640 . . . . . . . . . Obj: *(obj @ 82) 641 . . . . . . . . } 642 . . . . . . . . Sel: *ast.Ident { 643 . . . . . . . . . NamePos: router.go:30:4 644 . . . . . . . . . Name: "HandleNotFound" 645 . . . . . . . . } 646 . . . . . . . } 647 . . . . . . . Lparen: router.go:30:18 648 . . . . . . . Args: []ast.Expr (len = 1) { 649 . . . . . . . . 0: *ast.SelectorExpr { 650 . . . . . . . . . X: *ast.Ident { 651 . . . . . . . . . . NamePos: router.go:30:19 652 . . . . . . . . . . Name: "handler" 653 . . . . . . . . . } 654 . . . . . . . . . Sel: *ast.Ident { 655 . . . . . . . . . . NamePos: router.go:30:27 656 . . . . . . . . . . Name: "NotFoundHandler" 657 . . . . . . . . . } 658 . . . . . . . . } 659 . . . . . . . } 660 . . . . . . . Ellipsis: - 661 . . . . . . . Rparen: router.go:30:42 662 . . . . . . } 663 . . . . . } 664 . . . . . 14: *ast.ReturnStmt { 665 . . . . . . Return: router.go:34:2 666 . . . . . . Results: []ast.Expr (len = 1) { 667 . . . . . . . 0: *ast.Ident { 668 . . . . . . . . NamePos: router.go:34:9 669 . . . . . . . . Name: "r" 670 . . . . . . . . Obj: *(obj @ 82) 671 . . . . . . . } 672 . . . . . . } 673 . . . . . } 674 . . . . } 675 . . . . Rbrace: router.go:35:1 676 . . . } 677 . . } 678 . } 679 . Scope: *ast.Scope { 680 . . Objects: map[string]*ast.Object (len = 1) { 681 . . . "NewRouter": *(obj @ 43) 682 . . } 683 . } 684 . Imports: []*ast.ImportSpec (len = 3) { 685 . . 0: *(obj @ 12) 686 . . 1: *(obj @ 20) 687 . . 2: *(obj @ 28) 688 . } 689 . Unresolved: []*ast.Ident (len = 26) { 690 . . 0: *(obj @ 60) 691 . . 1: *(obj @ 94) 692 . . 2: *(obj @ 130) 693 . . 3: *(obj @ 140) 694 . . 4: *(obj @ 175) 695 . . 5: *(obj @ 185) 696 . . 6: *(obj @ 220) 697 . . 7: *(obj @ 230) 698 . . 8: *(obj @ 265) 699 . . 9: *(obj @ 275) 700 . . 10: *(obj @ 310) 701 . . 11: *(obj @ 320) 702 . . 12: *(obj @ 355) 703 . . 13: *(obj @ 365) 704 . . 14: *(obj @ 400) 705 . . 15: *(obj @ 410) 706 . . 16: *(obj @ 445) 707 . . 17: *(obj @ 455) 708 . . 18: *(obj @ 490) 709 . . 19: *(obj @ 500) 710 . . 20: *(obj @ 535) 711 . . 21: *(obj @ 545) 712 . . 22: *(obj @ 580) 713 . . 23: *(obj @ 590) 714 . . 24: *(obj @ 620) 715 . . 25: *(obj @ 650) 716 . } 717 }適切なルーティングのswitch文を生成するには、3のコード生成をする前にパスの木構造を把握する必要があります。
go/ast
パッケージにはast.Inspect
という関数が用意されていて、深さ優先でASTを探索することができます。stdrouterではこの関数を使って順々に型や関数名を確認しながらパスの木構造を構築してます。
- [/api/users, GET, GetUsers]を追加
- [/api/users/:user_id, GET, GetUser]を追加
- [/api/users/:user_id/posts, GET, GetPosts]を追加
パスの木構造が構築できたら、あとは泥臭くコードを生成していくだけです。
コードの生成には
text/template
をつかっています。他には
fmt.Spintf
を使う方法やASTを直接操作して生成する方法などがあるようです。作り終わってみると、そこまで難しいことをしているわけではないと感じました。
処理系の周りにふれるので勉強になりますし、goパッケージを使ってなにか作ってみるといいかも知れません。
参考
- goパッケージで簡単に静的解析して世界を広げよう #golang
静的解析関連でやりたいことに対する道筋が示されてて助かりました。- github/yudppp/json_snake_case
ASTの解析とコード生成の点を参考にしました。- github.com/google/wire
ディレクトリ構成やgo generateの使い方を参考にしました。- Goの標準パッケージだけでRESTfulなHandlerを作る
パスパラメータの扱いなどを参考にしました。
- 投稿日:2020-07-06T14:40:56+09:00
【Go】Goroutine使った並列処理サンプル - API呼び出し
はじめに
はじめまして。Go初学者です。
関わっているプロダクト的にも、自身のスキル的にも、並列処理そのものもあまり書いてこなかったこともあり、使い方も使い所もいまいちわからなかったゴルーチンですが、真面目に勉強中です。
今回は試しにゴルーチンを利用してみたので、同じ境遇の方に向けて、実装例と共にご紹介させていただきます。作ったもの
- Qiita APIを実行し、自分自身の投稿情報を取得していいね率等を表示するCLIツール
簡単な処理フロー
- 実行引数にて、userIDとQiitaのアクセストークンを渡す
- ユーザーの記事一覧をAPIで取得
- 各記事ごとに、記事単体を取得するAPIを叩く
- 必要な情報を標準出力に出力
ゴルーチンの使い所
今回は、3の処理を直列に実行すると、記事の数だけ直列実行されるため、n+1の処理になってしまいます。
なので、3の処理は並列に実行することでパフォーマンスを上げてみました。そもそも呼び出しの回数は変わってないのでn+1問題自体は解決していない気もしますが、
私の投稿数なんて大したことないので一旦置いておきます^q^ソースコードで解説
- ユーザーの記事一覧をAPIで取得
- 各記事ごとに、記事単体を取得するAPIを叩く
こちらについて、ソースコードを元にみていきます。
aggregatemyqiita/aggregateMyQiita.gofunc Aggregate(params Params) error { usersURL := "https://qiita.com/api/v2/users/" + params.UserID + "/items" client := client{ token: params.Token, } res, err := client.request(usersURL) if err != nil { return err } var pageItems pageItems decodeBody(res, &pageItems) pageDetailItemCh := make(chan pageDetailItem, len(pageItems)) go time.AfterFunc(time.Second*3, func() { close(pageDetailItemCh) }) for _, item := range pageItems { go client.parallelRequest(pageDetailItemCh, "https://qiita.com/api/v2/items/"+item.ID) } for pageDetailItem := range pageDetailItemCh { fmt.Println("==========================================================") fmt.Println("ID: ", pageDetailItem.ID) fmt.Println("タイトル: ", pageDetailItem.Title) fmt.Printf("タグ: ") for _, tag := range pageDetailItem.Tags { fmt.Printf("%s, ", tag.Name) } fmt.Printf("%s", "\n") fmt.Println("いいね数: ", pageDetailItem.LikesCount) fmt.Println("閲覧数: ", pageDetailItem.PageViewsCount) fmt.Printf("いいね率:%.2f%%\n", pageDetailItem.likeRatio()*100) } fmt.Println("==========================================================") return nilaggregatemyqiita/client.gofunc (client *client) request(url string) (*http.Response, error) { //fmt.Printf("[INFO]: %s\n", "Request to "+url) request, err := http.NewRequest("GET", url, nil) if err != nil { return &http.Response{}, fmt.Errorf("[ERR] :%s", err) } request.Header.Set("Authorization", "Bearer "+client.token) res, err := client.do(request) if err != nil { return res, err } return res, nil } func (client *client) parallelRequest(pageDetailItemCh chan pageDetailItem, url string) error { //fmt.Printf("[INFO]: %s\n", "Request to "+url) request, err := http.NewRequest("GET", url, nil) if err != nil { return fmt.Errorf("[ERR] :%s", err) } request.Header.Set("Authorization", "Bearer "+client.token) res, err := client.do(request) if err != nil { return err } var pageDetailItem pageDetailItem decodeBody(res, &pageDetailItem) pageDetailItemCh <- pageDetailItem return nil }
- > ユーザーの記事一覧をAPIで取得 以下の部分にて、GETで該当のURLからJSONを取得し、structに変換してスライスで受け取っています。 ここは普通に一回呼び出すだけで良いので、並列処理は必要ありません。
aggregatemyqiita/aggregateMyQiita.gousersURL := "https://qiita.com/api/v2/users/" + params.UserID + "/items" client := client{ token: params.Token, } res, err := client.request(usersURL) if err != nil { return err } var pageItems pageItems decodeBody(res, &pageItems)
- > 各記事ごとに、記事単体を取得するAPIを叩く ここからが本題です。まず、最初に記事数分のバッファを持ったチャネルを作成します。
aggregatemyqiita/aggregateMyQiita.gopageDetailItemCh := make(chan pageDetailItem, len(pageItems))その後、goキーワードの後に無名関数を呼び出し、記事数分、並列でリクエスト処理を呼び出します。
また、このままだとチャネルがクローズされないので、3秒でクローズされるように別のゴルーチンを利用する。aggregatemyqiita/aggregateMyQiita.gofor _, item := range pageItems { go client.parallelRequest(pageDetailItemCh, "https://qiita.com/api/v2/items/"+item.ID) } go time.AfterFunc(time.Second*2, func() { close(pageDetailItemCh) })この
parallelRequest
という関数は、以下の通り実行結果をチャネルに送信します。aggregatemyqiita/client.gofunc (client *client) parallelRequest(pageDetailItemCh chan pageDetailItem, url string) error { //fmt.Printf("[INFO]: %s\n", "Request to "+url) request, err := http.NewRequest("GET", url, nil) if err != nil { return fmt.Errorf("[ERR] :%s", err) } request.Header.Set("Authorization", "Bearer "+client.token) res, err := client.do(request) if err != nil { return err } var pageDetailItem pageDetailItem decodeBody(res, &pageDetailItem) pageDetailItemCh <- pageDetailItem return nil }チャネルに送信されたレスポンスについては、以下のrangeにて待ち受けます。
aggregatemyqiita/aggregateMyQiita.gofor pageDetailItem := range pageDetailItemCh { fmt.Println("==========================================================") fmt.Println("ID: ", pageDetailItem.ID) fmt.Println("タイトル: ", pageDetailItem.Title) fmt.Printf("タグ: ") for _, tag := range pageDetailItem.Tags { fmt.Printf("%s, ", tag.Name) } fmt.Printf("%s", "\n") fmt.Println("いいね数: ", pageDetailItem.LikesCount) fmt.Println("閲覧数: ", pageDetailItem.PageViewsCount) fmt.Printf("いいね率:%.2f%%\n", pageDetailItem.likeRatio()*100) }おわりに
結構勉強会などでも、「並列処理の利用するシーンってどんな時でしょう?」みたいな質問が上がっていて、
自分自身もまだまだわかりませんが、こんな使い方もあるかなと思い投稿させていただきました。
内容へのご指摘も、「こんなんあるよ」みたいなお話も大歓迎です。お待ちしています。参考
プログラミング言語Go完全入門(執筆時点で2020/07/31までの期間限定で無償で公開されてます!)
上記の資料については、直接URLを貼るのは憚られますので以下よりご参照ください!
「プログラミング言語Go完全入門」の期間限定公開のお知らせ - Mercari Enfineering Blog
- 投稿日:2020-07-06T01:12:33+09:00
Goでガンマ補正してみる
前回の投稿 でセットアップしたGo+GoCV環境においての演習としてガンマ補正を行うプログラムを作ってみました。
考え方
GoCVには、というより、OpenCVにはガンマ補正のライブラリ関数はないのでCVにあるLUT(LookUpTable)の仕組みを用いてガンマ補正用のルックアップテーブルを作って変換させる方法です。
ガンマ補正の計算式
入力の画素値を$I(x, y)$、出力の画素値を$I'(x, y)$、画素の最大値を$I_{max}$、ガンマ補正値を$\gamma$とすると以下の式で与えられます。
I’(x, y) = I_{max}\times\Bigl(\frac{I(x, y)}{I_{max}}\Bigr)^{1/\gamma}そして、上記の式を画素ごとに適用するのではなく、0~255の出力画素値を計算しておいたルックアップテーブルを用意し、LUT関数を用いて変換をかける形で実行していきます。
Goのソース
gamma.gopackage main import ( "fmt" "os" "math" "gocv.io/x/gocv" ) func main() { if len(os.Args) < 2 { fmt.Println("How to run:\n\tshowimage [imgfile]") return } filename := os.Args[1] window := gocv.NewWindow("before") window_2 := gocv.NewWindow("after") img := gocv.IMRead(filename, gocv.IMReadColor) if img.Empty() { fmt.Printf("Error reading image from: %v\n", filename) return } window.IMShow(img) img_gamma := myGamma_conversion(img, 1.5) for { window_2.IMShow(img_gamma) if window_2.WaitKey(1) >= 0 { break } } } func myGamma_conversion(src gocv.Mat, gamma float64) gocv.Mat { gamma_cvt := gocv.NewMatWithSize(256, 1, gocv.MatTypeCV8U) for r := 0 ; r < 256 ; r++ { gamma_element := 255.0 * math.Pow(float64(r)/255.0, 1.0/float64(gamma)) gamma_cvt.SetUCharAt(r, 0, uint8(gamma_element)) } ret_gamma := src.Clone() gocv.LUT(src, gamma_cvt, &ret_gamma) return ret_gamma }実行結果
上記のサンプルソースをどこかに入れておきます。
ソースと同じフォルダに適用させたい画像を置いて、以下のように動かすと走ることが出来ます。
go run gamma.go ./lenna.png
※今回のサンプルは例の「レナ」様です。