20200706のGoに関する記事は4件です。

Goの標準ライブラリだけでルーティングするの億劫ですよね。静的解析してコード生成しません?

Goは標準ライブラリが豊富で、Webアプリが簡単に書けていいですよね。

ひと通りの機能は多少冗長でも標準ライブラリだけで実装できます。

しかし、パスパラメータが複数のときのルーティングは標準ライブラリだけだと面倒です。

Webフレームワークを導入すればいいという声が聞こえてきそうですが、世の中標準ライブラリだけで実装する需要が少なからずあるみたいなので、ルーターのコードを生成するツールを作って見ました。

ルーティングのお話

本題に入る前にフレームワークと標準ライブラリのルーティング方法を比較していきましょう。

単純なパスのとき

パスパラメータを扱わない、単純なパスをルーティングするときは次のようにできます。

標準ライブラリ
http.HandleFunc("/users", UsersHandler) // HTTPメソッドの判定など、関数内で工夫が必要

ちなみに軽めのWebフレームワーク・ライブラリで有名なgin、echo、gorilla/muxなどはこんな感じです。

gin
r := gin.Default()
r.GET("/users", GetHandler)
r.POST("/users", PostHandler)
echo
e := echo.New()
e.GET("/users", GetHandler)
e.POST("/users", PostHandler)
gorilla/mux
r := mux.NewRouter()
r.HandleFunc("/users", UsersHandler) 

この程度の差なら標準ライブラリだけで問題ない印象です。

パスパラメータが1つのとき

ではパスパラメータを扱うときはどうでしょう。/users/:user_idをルーティングしてみます。

フレームワークを使うとこんな感じで書けます。

gin
r := gin.Default()
r.GET("/users/:user_id", GetUserHandler) 

func GetUserHandler(c *gin.Context) {
    userId := c.Param("user_id") // 引数で渡される*gin.contextからパスパラメータを取得可能
    /*...*/
}
echo
e := echo.New()
e.GET("/users/:user_id", GetUserHandler)

func GetUserHandler(c echo.Context) {
    userId := c.Param("user_id") // ginとほぼ同じ
    /*...*/
}
gorilla/mux
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を扱ってみましょう。

フレームワークを使うとこんな感じで書けます。

gin
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")
    /*...*/
}
echo
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")
    /*...*/
}
gorilla/mux
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インターフェイスで、次のように定義されています。

http/server.go
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

このServeHTTP関数がリクエストがサーバーにリクエストが来たときのエントリポイントになっていて、登録されたパスのパターンにしたがってルーティング処理をしています。

上の例ではnilを渡しているのでhttpパッケージ内でグローバル宣言されているDefaultServeMuxが代わりに使用されます。

ちなみにhttp.HandleFuncの実装は次のようになっていて、DefaultServeMuxにHandlerを登録していることがわかります。

http/server.go
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

少し複雑な話が続きましたが、要はDefaultServeMuxhttp.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/stdrouter

stdrouter作成の流れ

ここからはstdrouterの作り方の話になります。興味のある方は参考にしていただけると幸いです。

最近Goの界隈で静的解析の波がきてますよね。静的解析で何が作れるのか調べてたときにコード生成ができることを知り、ネタになるとおもったので作りました。

作り方の流れとしては

  1. ルーティングの定義ファイルと完成形のコードを手動で書いて、途中のロジックを考える
  2. ルーティングの定義ファイルをAST(抽象構文木)に分解して解析する
  3. 泥臭くfor文とif文でコードを組み立てるロジックを書く
  4. リファクタする

のような感じでした。

1の定義ファイルと生成結果はrouter.goとrouter_gen.goのことです。

2のコードの解析などは初めてで完全に手探り状態だったので、まず次のコードでASTを表示してみました。

main.go
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ではこの関数を使って順々に型や関数名を確認しながらパスの木構造を構築してます。

  1. [/api/users, GET, GetUsers]を追加 tree_1.jpeg
  2. [/api/users/:user_id, GET, GetUser]を追加 tree_2.jpeg
  3. [/api/users/:user_id/posts, GET, GetPosts]を追加 tree_3.jpeg

パスの木構造が構築できたら、あとは泥臭くコードを生成していくだけです。

コードの生成にはtext/templateをつかっています。

他にはfmt.Spintfを使う方法やASTを直接操作して生成する方法などがあるようです。

作り終わってみると、そこまで難しいことをしているわけではないと感じました。

処理系の周りにふれるので勉強になりますし、goパッケージを使ってなにか作ってみるといいかも知れません。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goの標準ライブラリだけでルーティングするの億劫ですよね。コード生成しません?

Goは標準ライブラリが豊富で、Webアプリが簡単に書けていいですよね。

ひと通りの機能は多少冗長でも標準ライブラリだけで実装できます。

しかし、パスパラメータが複数のときのルーティングは標準ライブラリだけだと面倒です。

Webフレームワークを導入すればいいという声が聞こえてきそうですが、世の中標準ライブラリだけで実装する需要が少なからずあるみたいなので、ルーターのコードを生成するツールを作って見ました。

ルーティングのお話

本題に入る前にフレームワークと標準ライブラリのルーティング方法を比較していきましょう。

単純なパスのとき

パスパラメータを扱わない、単純なパスをルーティングするときは次のようにできます。

標準ライブラリ
http.HandleFunc("/users", UsersHandler) // HTTPメソッドの判定など、関数内で工夫が必要

ちなみに軽めのWebフレームワーク・ライブラリで有名なgin、echo、gorilla/muxなどはこんな感じです。

gin
r := gin.Default()
r.GET("/users", GetHandler)
r.POST("/users", PostHandler)
echo
e := echo.New()
e.GET("/users", GetHandler)
e.POST("/users", PostHandler)
gorilla/mux
r := mux.NewRouter()
r.HandleFunc("/users", UsersHandler) 

この程度の差なら標準ライブラリだけで問題ない印象です。

パスパラメータが1つのとき

ではパスパラメータを扱うときはどうでしょう。/users/:user_idをルーティングしてみます。

フレームワークを使うとこんな感じで書けます。

gin
r := gin.Default()
r.GET("/users/:user_id", GetUserHandler) 

func GetUserHandler(c *gin.Context) {
    userId := c.Param("user_id") // 引数で渡される*gin.contextからパスパラメータを取得可能
    /*...*/
}
echo
e := echo.New()
e.GET("/users/:user_id", GetUserHandler)

func GetUserHandler(c echo.Context) {
    userId := c.Param("user_id") // ginとほぼ同じ
    /*...*/
}
gorilla/mux
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を扱ってみましょう。

フレームワークを使うとこんな感じで書けます。

gin
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")
    /*...*/
}
echo
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")
    /*...*/
}
gorilla/mux
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インターフェイスで、次のように定義されています。

http/server.go
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

このServeHTTP関数がリクエストがサーバーにリクエストが来たときのエントリポイントになっていて、登録されたパスのパターンにしたがってルーティング処理をしています。

上の例ではnilを渡しているのでhttpパッケージ内でグローバル宣言されているDefaultServeMuxが代わりに使用されます。

ちなみにhttp.HandleFuncの実装は次のようになっていて、DefaultServeMuxにHandlerを登録していることがわかります。

http/server.go
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

少し複雑な話が続きましたが、要はDefaultServeMuxhttp.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/stdrouter

stdrouter作成の流れ

ここからはstdrouterの作り方の話になります。興味のある方は参考にしていただけると幸いです。

最近Goの界隈で静的解析の波がきてますよね。静的解析で何が作れるのか調べてたときにコード生成ができることを知り、ネタになるとおもったので作りました。

作り方の流れとしては

  1. ルーティングの定義ファイルと完成形のコードを手動で書いて、途中のロジックを考える
  2. ルーティングの定義ファイルをAST(抽象構文木)に分解して解析する
  3. 泥臭くfor文とif文でコードを組み立てるロジックを書く
  4. リファクタする

のような感じでした。

1の定義ファイルと生成結果はrouter.goとrouter_gen.goのことです。

2のコードの解析などは初めてで完全に手探り状態だったので、まず次のコードでASTを表示してみました。

main.go
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ではこの関数を使って順々に型や関数名を確認しながらパスの木構造を構築してます。

  1. [/api/users, GET, GetUsers]を追加 tree_1.jpeg
  2. [/api/users/:user_id, GET, GetUser]を追加 tree_2.jpeg
  3. [/api/users/:user_id/posts, GET, GetPosts]を追加 tree_3.jpeg

パスの木構造が構築できたら、あとは泥臭くコードを生成していくだけです。

コードの生成にはtext/templateをつかっています。

他にはfmt.Spintfを使う方法やASTを直接操作して生成する方法などがあるようです。

作り終わってみると、そこまで難しいことをしているわけではないと感じました。

処理系の周りにふれるので勉強になりますし、goパッケージを使ってなにか作ってみるといいかも知れません。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go】Goroutine使った並列処理サンプル - API呼び出し

はじめに

はじめまして。Go初学者です。

関わっているプロダクト的にも、自身のスキル的にも、並列処理そのものもあまり書いてこなかったこともあり、使い方も使い所もいまいちわからなかったゴルーチンですが、真面目に勉強中です。
今回は試しにゴルーチンを利用してみたので、同じ境遇の方に向けて、実装例と共にご紹介させていただきます。

作ったもの

  • Qiita APIを実行し、自分自身の投稿情報を取得していいね率等を表示するCLIツール

githubはこちら

簡単な処理フロー

  1. 実行引数にて、userIDとQiitaのアクセストークンを渡す
  2. ユーザーの記事一覧をAPIで取得
  3. 各記事ごとに、記事単体を取得するAPIを叩く
  4. 必要な情報を標準出力に出力

ゴルーチンの使い所

今回は、3の処理を直列に実行すると、記事の数だけ直列実行されるため、n+1の処理になってしまいます。
なので、3の処理は並列に実行することでパフォーマンスを上げてみました。

n+1とは...?

そもそも呼び出しの回数は変わってないのでn+1問題自体は解決していない気もしますが、
私の投稿数なんて大したことないので一旦置いておきます^q^

ソースコードで解説

  1. ユーザーの記事一覧をAPIで取得
  2. 各記事ごとに、記事単体を取得するAPIを叩く

こちらについて、ソースコードを元にみていきます。

aggregatemyqiita/aggregateMyQiita.go
func 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 nil
aggregatemyqiita/client.go
func (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.go
    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)
  • > 各記事ごとに、記事単体を取得するAPIを叩く ここからが本題です。まず、最初に記事数分のバッファを持ったチャネルを作成します。
aggregatemyqiita/aggregateMyQiita.go
    pageDetailItemCh := make(chan pageDetailItem, len(pageItems))

その後、goキーワードの後に無名関数を呼び出し、記事数分、並列でリクエスト処理を呼び出します。
また、このままだとチャネルがクローズされないので、3秒でクローズされるように別のゴルーチンを利用する。

aggregatemyqiita/aggregateMyQiita.go
    for _, 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.go
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
}

チャネルに送信されたレスポンスについては、以下のrangeにて待ち受けます。

aggregatemyqiita/aggregateMyQiita.go
    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)
    }

おわりに

結構勉強会などでも、「並列処理の利用するシーンってどんな時でしょう?」みたいな質問が上がっていて、
自分自身もまだまだわかりませんが、こんな使い方もあるかなと思い投稿させていただきました。
内容へのご指摘も、「こんなんあるよ」みたいなお話も大歓迎です。お待ちしています。

参考

プログラミング言語Go完全入門(執筆時点で2020/07/31までの期間限定で無償で公開されてます!)
上記の資料については、直接URLを貼るのは憚られますので以下よりご参照ください!
「プログラミング言語Go完全入門」の期間限定公開のお知らせ - Mercari Enfineering Blog

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.go
package 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
※今回のサンプルは例の「レナ」様です。

サンプルソースはガンマ補正値を1.5にしましたが、こんな感じで実行してくれました。
変換前
before.jpg
変換後
after.jpg

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む