Goは標準ライブラリが豊富で、Webアプリが簡単に書けていいですよね。
ひと通りの機能は多少冗長でも標準ライブラリだけで実装できます。
しかし、パスパラメータが複数のときのルーティングは標準ライブラリだけだと面倒です。
Webフレームワークを導入すればいいという声が聞こえてきそうですが、世の中標準ライブラリだけで実装する需要が少なからずあるみたいなので、ルーターのコードを生成するツールを作って見ました。
ルーティングのお話
本題に入る前にフレームワークと標準ライブラリのルーティング方法を比較していきましょう。
単純なパスのとき
パスパラメータを扱わない、単純なパスをルーティングするときは次のようにできます。
http.HandleFunc("/users", UsersHandler) // HTTPメソッドの判定など、関数内で工夫が必要
ちなみに軽めのWebフレームワーク・ライブラリで有名なgin、echo、gorilla/muxなどはこんな感じです。
r := gin.Default()
r.GET("/users", GetHandler)
r.POST("/users", PostHandler)
e := echo.New()
e.GET("/users", GetHandler)
e.POST("/users", PostHandler)
r := mux.NewRouter()
r.HandleFunc("/users", UsersHandler)
この程度の差なら標準ライブラリだけで問題ない印象です。
パスパラメータが1つのとき
ではパスパラメータを扱うときはどうでしょう。/users/:user_id
をルーティングしてみます。
フレームワークを使うとこんな感じで書けます。
r := gin.Default()
r.GET("/users/:user_id", GetUserHandler)
func GetUserHandler(c *gin.Context) {
userId := c.Param("user_id") // 引数で渡される*gin.contextからパスパラメータを取得可能
/*...*/
}
e := echo.New()
e.GET("/users/:user_id", GetUserHandler)
func GetUserHandler(c echo.Context) {
userId := c.Param("user_id") // ginとほぼ同じ
/*...*/
}
r := 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
を扱ってみましょう。
フレームワークを使うとこんな感じで書けます。
r := 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")
/*...*/
}
e := 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")
/*...*/
}
r := 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
インターフェイスで、次のように定義されています。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
このServeHTTP
関数がリクエストがサーバーにリクエストが来たときのエントリポイントになっていて、登録されたパスのパターンにしたがってルーティング処理をしています。
上の例ではnil
を渡しているのでhttpパッケージ内でグローバル宣言されているDefaultServeMux
が代わりに使用されます。
ちなみにhttp.HandleFunc
の実装は次のようになっていて、DefaultServeMux
にHandlerを登録していることがわかります。
func 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
を忘れないでください
//+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の中身(長いので折りたたんでます)
// 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/stdrouter
stdrouter作成の流れ
ここからはstdrouterの作り方の話になります。興味のある方は参考にしていただけると幸いです。
最近Goの界隈で静的解析の波がきてますよね。静的解析で何が作れるのか調べてたときにコード生成ができることを知り、ネタになるとおもったので作りました。
作り方の流れとしては
- ルーティングの定義ファイルと完成形のコードを手動で書いて、途中のロジックを考える
- ルーティングの定義ファイルをAST(抽象構文木)に分解して解析する
- 泥臭くfor文とif文でコードを組み立てるロジックを書く
- リファクタする
のような感じでした。
1の定義ファイルと生成結果はrouter.goとrouter_gen.goのことです。
2のコードの解析などは初めてで完全に手探り状態だったので、まず次のコードでASTを表示してみました。
package 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を作る
パスパラメータの扱いなどを参考にしました。