├── LICENSE └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Alexey Diyan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang Web Framework Comparison 2 | 3 | This suite aims to compare the public API of various Go web frameworks and routers. 4 | 5 | NOTE While code blocks are self-explained the list of PROs an CONs are highly opinionated and targeted Go 1.7+. Even if some frameworks has a more :thumbsdown: than another they still are awesome and may work better for your use cases. 6 | 7 | ## Contents 8 | - [Reviewed libraries and frameworks](#reviewed-libraries-and-frameworks) 9 | - [HTTP handler. Signature](#http-handler-signature) 10 | - [HTTP middleware. Signature and sample code](#http-middleware-signature-and-sample-code) 11 | - [HTTP handler. Write Go struct as JSON response](#http-handler-write-go-struct-as-json-response) 12 | - [HTTP handler. Bind JSON payload into Go struct](#http-handler-bind-json-payload-into-go-struct) 13 | 14 | ## Reviewed libraries and frameworks 15 | 16 | - stdlib net/http 17 | - gin-gonic/gin 18 | - go-macaron/macaron 19 | - go-martini/martini 20 | - go-ozzo/ozzo-routing 21 | - gocraft/web 22 | - goji/goji 23 | - gorilla/mux 24 | - hoisie/web 25 | - julienschmidt/httprouter (router) 26 | - labstack/echo 27 | - mholt/binding (request binding) 28 | - pressly/chi 29 | - TODO astaxie/beego 30 | - TODO revel/revel 31 | - unrolled/render (response rendering) 32 | - urfave/negroni 33 | - zenazn/goji 34 | 35 | ## HTTP handler. Signature 36 | 37 | - :thumbsdown: :exclamation: revel, beego are not idiomatic Go because they forces you to embed handler to the Framework's struct 38 | - :thumbsdown: martini, hoisie/web, macaron handlers are not strongly typed due to reflective dependency injection (which leads to poor performance) 39 | - :thumbsdown: zenazn/goji handler and middleware is not strongly typed to emulate function overload 40 | - :thumbsdown: gocraft/web handler is not strongly typed because it's a method with pointer receiver that could be any user-defined struct 41 | - :thumbsdown: negroni, stdlib net/http does not dispatch request by HTTP verb (more boilerplate code) 42 | - :thumbsdown: hoisie/web, zenazn/goji offers to use singletone istance of the server struct which is quite bad practice 43 | - :question: goji/goji has quite unusual API to dispatch requests by HTTP verb but it's still more verbose than in echo, gin, julienschmidt/httprouter 44 | - :thumbsup: echo, gin, julienschmidt/httprouter, zenazn/goji, goji/goji, ozzo-routing, pressly/chi handers are stronly typed 45 | - :thumbsup: echo, gin, julienschmidt/httprouter, zenazn/goji, ozzo-routing, pressly/chi do dispatch requests by HTTP verb 46 | - :thumbsup: echo, gin, zenazn/goji, ozzo-routing, pressly/chi support HTTP middleware 47 | - :thumbsup: echo, ozzo-routing handers returns error value which could be handled in next middlewares in the chain 48 | - :question: julienschmidt/httprouter does not support HTTP middleware, gorilla/handlers are recommended instead 49 | - :question: goji/goji keeps handler interface standard but it's quite verbose to type 50 | - FYI labstack/echo has own router, supports most handler / middleware APIs 51 | - FYI gin uses julienschmidt/httprouter, per-request context map 52 | - FYI negroni recommends gorilla/mux router; Golang 1.7 context can be used (or gorilla/context for Golang < 1.7) 53 | - :thumbsdown: gorilla/context uses global context map which may lead to lock contention 54 | - :thumbsup: Golang 1.7 context uses per-request context map, Request.WithContext does a shallow copy of *Request 55 | 56 | ### stdlib net/http 57 | stdlib net/http or negroni + stdlib net/http or negroni + gorilla/mux, https://golang.org/pkg/net/http/#ServeMux.HandleFunc 58 | ```go 59 | func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) 60 | 61 | mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { ... 62 | ``` 63 | 64 | ### gin-gonic/gin 65 | https://godoc.org/github.com/gin-gonic/gin#RouterGroup.GET 66 | ```go 67 | func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes 68 | type HandlerFunc func(*gin.Context) 69 | 70 | g.GET("/", func(c *gin.Context) { ... 71 | ``` 72 | 73 | ### go-macaron/macaron 74 | https://godoc.org/github.com/go-macaron/macaron#Router.Get 75 | ```go 76 | func (r *Router) Get(pattern string, h ...Handler) (leaf *Route) 77 | type Handler interface{} 78 | 79 | m.Get("/", func(w http.ResponseWriter, req *http.Request, log *log.Logger) { ... 80 | m.Get("/", func(ctx *macaron.Context) (int, []byte) { ... 81 | ``` 82 | 83 | ### go-martini/martini 84 | https://godoc.org/github.com/go-martini/martini#Router 85 | ```go 86 | func (r *Router) Get(string, ...Handler) Route 87 | type Handler interface{} 88 | 89 | m.Get("/", func(w http.ResponseWriter, r *http.Request, u *SomeUserService) { ... 90 | m.Get("/", func() (int, string) { ... 91 | ``` 92 | 93 | ### gocraft/web 94 | https://godoc.org/github.com/gocraft/web#Router.Get 95 | ```go 96 | func (r *Router) Get(path string, fn interface{}) *Router 97 | 98 | w.Get("/", func (c *SomeUserContext) SayHello(w web.ResponseWriter, r *web.Request) { ... 99 | ``` 100 | 101 | ### goji/goji 102 | https://godoc.org/github.com/goji/goji#Mux.HandleFunc 103 | ```go 104 | func (m *Mux) HandleFunc(p Pattern, h func(http.ResponseWriter, *http.Request)) 105 | func (m *Mux) HandleFuncC(p Pattern, h func(context.Context, http.ResponseWriter, *http.Request)) 106 | type Pattern interface { 107 | Match(context.Context, *http.Request) context.Context 108 | } 109 | 110 | g.HandleFunc(pat.Get("/"), func (w http.ResponseWriter, r *http.Request) { ... 111 | g.HandleFuncC(pat.Get("/"), func (ctx context.Context, w http.ResponseWriter, r *http.Request) { ... 112 | ``` 113 | 114 | ### go-ozzo/ozzo-routing 115 | https://godoc.org/github.com/go-ozzo/ozzo-routing#RouteGroup.Get 116 | ```go 117 | func (r *RouteGroup) Get(path string, handlers ...Handler) *Route 118 | type Handler func(*routing.Context) error 119 | 120 | r.Get("/", func(c *routing.Context) error { ... 121 | ``` 122 | 123 | ### hoisie/web 124 | https://godoc.org/github.com/hoisie/web#Get 125 | ```go 126 | func Get(route string, handler interface{}) 127 | 128 | w.Get("/", func(ctx *web.Context, val string) { ... 129 | w.Get("/", func(val string) string { ... 130 | ``` 131 | 132 | ### julienschmidt/httprouter 133 | https://godoc.org/github.com/julienschmidt/httprouter#Router.GET 134 | ```go 135 | func (r *Router) GET(path string, handle Handle) 136 | type Handle func(http.ResponseWriter, *http.Request, Params) 137 | 138 | r.GET("/", func Index(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { ... 139 | ``` 140 | 141 | ### labstack/echo 142 | https://godoc.org/github.com/labstack/echo#Echo.Get 143 | ```go 144 | func (e *Echo) Get(path string, h HandlerFunc, m ...MiddlewareFunc) 145 | type HandlerFunc func(echo.Context) error 146 | type MiddlewareFunc func(HandlerFunc) HandlerFunc 147 | 148 | e.Get("/", func(c echo.Context) error { ... 149 | ``` 150 | 151 | ### pressly/chi 152 | https://godoc.org/github.com/pressly/chi#Mux.Get 153 | ```go 154 | func (mx *Mux) Get(pattern string, handlerFn http.HandlerFunc) 155 | 156 | c.Get("/", func(w http.ResponseWriter, r *http.Request) { ... 157 | ``` 158 | 159 | ### zenazn/goji 160 | https://godoc.org/github.com/zenazn/goji#Get 161 | ```go 162 | func Get(pattern web.PatternType, handler web.HandlerType) 163 | type PatternType interface{} 164 | type HandlerType interface{} 165 | 166 | g.Get("/", func(w http.ResponseWriter, req *http.Request) { ... 167 | ``` 168 | 169 | ## HTTP middleware. Signature and sample code 170 | 171 | TODO add godoc urls 172 | 173 | - :thumbsdown: revel, beego are not considered, they forces you to embed handler to the Framework's struct 174 | - :thumbsdown: martini, hoisie/web, macaron are not considered, their handers are not strongly typed due to reflective dependency injection 175 | - :thumbsdown: zenazn/goji handler and middleware are not strongly typed to emulate function overload 176 | - :thumbsdown: gin has "func (c *Context) Next()" function that visible to all handlers but must be called only inside middleware 177 | - :thumbsup: echo, goji/goji, ozzo-routing, pressly/chi, negroni has strongly typed middleware with reasonable signatures 178 | - :question: negroni uses quite unusual signature for middleware. Why? I have only one explanation at this point. Author decide to avoid usage of higher-order function, so it would be easier to grasp for not-experienced developers 179 | - TODO gocraft/web PROs and CONs that related to the middleware API 180 | 181 | ### stdlib net/http, gorilla/mux, julienschmidt/httprouter, pressly/chi 182 | ```go 183 | func(http.Handler) http.Handler 184 | 185 | func Middleware(next http.Handler) http.Handler { 186 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 187 | // do some stuff before 188 | next.ServeHTTP(w, r) 189 | // do some stuff after 190 | }) 191 | } 192 | ``` 193 | 194 | ### gin-gonic/gin 195 | ```go 196 | // gin uses same signature for both handler and middleware 197 | type HandlerFunc func(*Context) 198 | 199 | func Middleware() gin.HandlerFunc { 200 | return func(c *gin.Context) { 201 | // do some stuff before 202 | c.Next() 203 | // do some stuff after 204 | } 205 | } 206 | ``` 207 | 208 | ### gocraft/web 209 | ```go 210 | // TODO 211 | 212 | func (c *SomeUserContext) SetHelloCount(w web.ResponseWriter, r *web.Request, next web.NextMiddlewareFunc) { 213 | // do some stuff before 214 | next(w, r) 215 | // do some stuff after 216 | } 217 | ``` 218 | 219 | ### goji/goji 220 | ```go 221 | func(http.Handler) http.Handler // standard middleware 222 | func(goji.Handler) goji.Handler // context-aware middleware 223 | 224 | func Middleware(inner goji.Handler) goji.Handler { 225 | return goji.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 226 | // do some stuff before 227 | inner.ServeHTTPC(ctx, w, r) 228 | // do some stuff after 229 | }) 230 | } 231 | ``` 232 | 233 | ### go-ozzo/ozzo-routing 234 | ```go 235 | // middlewares and handers share the same type "Handler" in ozzo-routing 236 | 237 | func Middleware(c *routing.Context) error { 238 | // do some stuff before 239 | err := c.Next() 240 | // do some stuff after 241 | return err 242 | } 243 | ``` 244 | 245 | ### labstack/echo 246 | ```go 247 | type MiddlewareFunc func(HandlerFunc) HandlerFunc 248 | type HandlerFunc func(Context) error 249 | 250 | func Middleware(next echo.HandlerFunc) echo.HandlerFunc { 251 | return func(c echo.Context) error { 252 | // do some stuff before 253 | err := next(c) 254 | // do some stuff after 255 | return err 256 | } 257 | } 258 | ``` 259 | 260 | ### urfave/negroni 261 | ```go 262 | type HandlerFunc func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) 263 | 264 | func Middleware(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 265 | // do some stuff before 266 | next(rw, r) 267 | // do some stuff after 268 | } 269 | ``` 270 | 271 | ## HTTP handler. Write Go struct as JSON response 272 | 273 | ### Common part 274 | ```go 275 | type Greeting struct { 276 | Hello string `json:"hello"` 277 | } 278 | ``` 279 | 280 | ### stdlib net/http, gorilla/mux, negroni, zenazn/goji, goji/goji, julienschmidt/httprouter. V1 - use only stdlib 281 | ```go 282 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 283 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 284 | w.WriteHeader(http.StatusOK) 285 | if err := json.NewEncoder(w).Encode(Greeting{Hello: "world"}); err != nil { 286 | panic(err) 287 | } 288 | }) 289 | ``` 290 | 291 | ### stdlib net/http, gorilla/mux, negroni, zenazn/goji, goji/goji, julienschmidt/httprouter. V2 - use unrolled/render that mentioned in negroni's README 292 | ```go 293 | r := render.New() 294 | mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { 295 | if err := r.JSON(w, http.StatusOK, Greeting{Hello: "world"}); err != nil { 296 | panic(err) 297 | } 298 | }) 299 | ``` 300 | 301 | ### gin-gonic/gin 302 | ```go 303 | r.GET("/", func(c *gin.Context) { 304 | // method will panic if serialization fails 305 | c.JSON(http.StatusOK, Greeting{Hello: "world"}) 306 | }) 307 | ``` 308 | 309 | ### go-macaron/macaron. Use either macaron.Context or macaron.render.Render 310 | ```go 311 | // render.Render or macaron.Context will be passed to the handler by using dependency injection 312 | m.Get("/", func(r render.Render) { 313 | // method will render HTTP 500 response if serialization fails 314 | r.JSON(http.StatusOK, Greeting{Hello: "world"}) 315 | }) 316 | 317 | // V2 318 | m.Get("/", func(ctx *macaron.Context) { 319 | ctx.JSON(http.StatusOK, Greeting{Hello: "world"}) 320 | } 321 | ``` 322 | 323 | ### go-martini/martini + martini-contrib/render that menioned in README 324 | ```go 325 | // render.Render will be passed to the handler by using dependency injection 326 | m.Get("/", func(r render.Render) { 327 | // method will render HTTP 500 response if serialization fails 328 | r.JSON(http.StatusOK, Greeting{Hello: "world"}) 329 | }) 330 | ``` 331 | 332 | ### go-ozzo/ozzo-routing 333 | ```go 334 | // Context.Write will write the data in the format determined by the content negotiation middleware (included) 335 | r.Get("/", func(c *routing.Context) error { 336 | // if Write returns an error, the framework will render HTTP 500 response by default 337 | return c.Write(Greeting{Hello: "world"}) 338 | }) 339 | ``` 340 | 341 | ### gocraft/web + corneldamian/json-binding than mentioned in README 342 | TODO Review implementation in https://github.com/corneldamian/json-binding/blob/master/response.go#L47 343 | 344 | ### hoisie/web 345 | ```go 346 | // web.Context will be passed to the handler by using dependency injection 347 | w.Get("/", func(ctx *web.Context) string { 348 | ctx.ContentType("json") 349 | data, err := json.Marshal(Greeting{Hello: "world"}) 350 | if err != nil { 351 | panic(err) 352 | } 353 | return string(data) 354 | }) 355 | 356 | // V2 357 | w.Get("/", func(ctx *Context) []byte { 358 | ctx.ContentType("json") 359 | data, err := json.Marshal(Greeting{Hello: "world"}) 360 | if err != nil { 361 | panic(err) 362 | } 363 | return data 364 | }) 365 | ``` 366 | 367 | ### labstack/echo 368 | ```go 369 | e.Get("/", func(c echo.Context) error { 370 | // method will return error if serialization fails, then handler return that error to the middleware/framework 371 | return c.JSON(http.StatusOK, Greeting{Hello: "world"}) 372 | }) 373 | ``` 374 | 375 | ### pressly/chi 376 | ```go 377 | c.Get("/", func(w http.ResponseWriter, r *http.Request) { 378 | // method will render HTTP 500 response if serialization fails 379 | render.JSON(w, r, Greeting{Hello: "world"}) 380 | }) 381 | ``` 382 | 383 | ## HTTP handler. Bind JSON payload into Go struct 384 | 385 | ### Common part 386 | ```go 387 | type Greeting struct { 388 | Hello string `json:"hello"` 389 | } 390 | ``` 391 | 392 | ### stdlib net/http, gorilla/mux, negroni, zenazn/goji, goji/goji, julienschmidt/httprouter, go-macaron/macaron, hoisie/web. V1 - use only stdlib 393 | ```go 394 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 395 | g := new(Greeting) 396 | defer r.Body.Close() 397 | if err := json.NewDecoder(r.Body).Decode(g); err != nil { 398 | panic(err) 399 | } 400 | // TODO put here validation code 401 | w.WriteHeader(http.StatusCreated) 402 | } 403 | ``` 404 | 405 | ### stdlib net/http, gorilla/mux, negroni, zenazn/goji, goji/goji, julienschmidt/httprouter, go-macaron/macaron, hoisie/web. V2 - use mholt/binding 406 | ```go 407 | // mholt/binding is the reflectioness data binding library developed by co-author of the martini-contrib/binding 408 | 409 | // for JSON you can return an empty field map 410 | func (g *Greeting) FieldMap() binding.FieldMap { 411 | return binding.FieldMap{} 412 | } 413 | 414 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 415 | g := new(Greeting) 416 | errs := binding.Bind(r, g) 417 | // If parsing failed Handle will render HTTP 400 response with error details and return true 418 | if errs.Handle(w) { 419 | return 420 | } 421 | w.WriteHeader(http.StatusCreated) 422 | } 423 | ``` 424 | 425 | ### gin-gonic/gin 426 | ```go 427 | r.POST("/", func(c *gin.Context) { 428 | g := new(Greeting) 429 | if c.BindJSON(g); err != nil { 430 | panic(err) 431 | } 432 | c.Abort(http.StatusCreated) 433 | }) 434 | ``` 435 | 436 | ### go-martini/martini + martini-contrib/binding that menioned in README 437 | ```go 438 | // Greeting struct may also contain binding tags 439 | type Greeting struct { 440 | Hello string `json:"hello" binding:"required"` 441 | } 442 | // TODO check is binding.Bind(..) renders HTTP 400 response if validation fails 443 | m.Post("/", binding.Bind(Greeting{}), func(g Greeting) (int, string) { 444 | return http.StatusNoContent, "" 445 | }) 446 | ``` 447 | 448 | ### go-ozzo/ozzo-routing 449 | ```go 450 | r.Post("/", func(c *routing.Context) error { 451 | var g Greeting 452 | // Context.Read will read the data in the format specified by the "Content-Type" header 453 | if err := c.Read(&g); err != nil { 454 | return err 455 | } 456 | c.Response.WriteHeader(http.StatusCreated) 457 | return nil 458 | }) 459 | ``` 460 | 461 | ### gocraft/web + corneldamian/json-binding than mentioned in README 462 | TODO Review implementation in https://github.com/corneldamian/json-binding/blob/master/request.go#L56 463 | 464 | ### labstack/echo 465 | ```go 466 | e.POST("/", func(c echo.Context) error { 467 | g := new(Greeting) 468 | if err := c.Bind(g); err != nil { 469 | return err 470 | } 471 | return c.NoContent(http.StatusCreated) 472 | }) 473 | ``` 474 | 475 | ### pressly/chi 476 | ```go 477 | c.Post("/", func(w http.ResponseWriter, r *http.Request) { 478 | g := new(Greeting) 479 | if err := render.Bind(r.Body, g); err != nil { 480 | panic(err) 481 | } 482 | render.NoContent(w, r) 483 | }) 484 | ``` 485 | --------------------------------------------------------------------------------