Awesome
Don - Go API Framework
Don is a fast & simple API framework written in Go. It features a super-simple API and thanks to fasthttp and a custom version of httprouter it's blazing fast and has a low memory footprint.
As Don uses Go generics it requires Go 1.18 to work.
Minor version updates should be considered breaking changes.
Contents
Basic Example
package main
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/abemedia/go-don"
_ "github.com/abemedia/go-don/encoding/json" // Enable JSON parsing & rendering.
_ "github.com/abemedia/go-don/encoding/yaml" // Enable YAML parsing & rendering.
)
type GreetRequest struct {
Name string `path:"name"` // Get name from the URL path.
Age int `header:"X-User-Age"` // Get age from HTTP header.
}
type GreetResponse struct {
// Remember to add all the tags for the renderers you enable.
Greeting string `json:"data" yaml:"data"`
}
func Greet(ctx context.Context, req GreetRequest) (*GreetResponse, error) {
if req.Name == "" {
return nil, don.Error(errors.New("missing name"), http.StatusBadRequest)
}
res := &GreetResponse{
Greeting: fmt.Sprintf("Hello %s, you're %d years old.", req.Name, req.Age),
}
return res, nil
}
func Pong(context.Context, any) (string, error) {
return "pong", nil
}
func main() {
r := don.New(nil)
r.Get("/ping", don.H(Pong)) // Handlers are wrapped with `don.H`.
r.Post("/greet/:name", don.H(Greet))
r.ListenAndServe(":8080")
}
Configuration
Don is configured by passing in the Config
struct to don.New
.
r := don.New(&don.Config{
DefaultEncoding: "application/json",
DisableNoContent: false,
})
DefaultEncoding
Set this to the format you'd like to use if no Content-Type
or Accept
headers are in the
request.
DisableNoContent
If you return nil
from your handler, Don will respond with an empty body and a 204 No Content
status code. Set this to true
to disable that behaviour.
Support multiple formats
Support multiple request & response formats without writing extra parsing or rendering code. The API
uses the Content-Type
and Accept
headers to determine what input and output encoding to use.
You can mix multiple formats, for example if the Content-Type
header is set to application/json
,
however the Accept
header is set to application/x-yaml
, then the request will be parsed as JSON,
and the response will be YAML encoded.
If no Content-Type
or Accept
header is passed the default will be used.
Formats need to be explicitly imported e.g.
import _ "github.com/abemedia/go-don/encoding/yaml"
Currently supported formats
JSON
MIME: application/json
Parses JSON requests & encodes responses as JSON. Use the json
tag in your request & response
structs.
XML
MIME: application/xml
, text/xml
Parses XML requests & encodes responses as XML. Use the xml
tag in your request & response
structs.
YAML
MIME: application/yaml
, text/yaml
, application/x-yaml
, text/x-yaml
, text/vnd.yaml
Parses YAML requests & encodes responses as YAML. Use the yaml
tag in your request & response
structs.
Form (input only)
MIME: application/x-www-form-urlencoded
, multipart/form-data
Parses form data requests. Use the form
tag in your request struct.
Text
MIME: text/plain
Parses non-struct requests and encodes non-struct responses e.g. string
, int
, bool
etc.
func MyHandler(ctx context.Context, req int64) (string, error) {
// ...
}
If the request is a struct and the Content-Type
header is set to text/plain
it returns a
415 Unsupported Media Type
error.
MessagePack
MIME: application/msgpack
, application/x-msgpack
, application/vnd.msgpack
Parses MessagePack requests & encodes responses as MessagePack. Use the msgpack
tag in your
request & response structs.
TOML
MIME: application/toml
Parses TOML requests & encodes responses as TOML. Use the toml
tag in your request & response
structs.
Protocol Buffers
MIME: application/protobuf
, application/x-protobuf
Parses protobuf requests & encodes responses as protobuf. Use pointer types generated with protoc
as your request & response structs.
Adding custom encoding
Adding your own is easy. See encoding/json/json.go.
Request parsing
Automatically unmarshals values from headers, URL query, URL path & request body into your request struct.
type MyRequest struct {
// Get from the URL path.
ID int64 `path:"id"`
// Get from the URL query.
Filter string `query:"filter"`
// Get from the JSON, YAML, XML or form body.
Content float64 `form:"bar" json:"bar" yaml:"bar" xml:"bar"`
// Get from the HTTP header.
Lang string `header:"Accept-Language"`
}
Please note that using a pointer as the request type negatively affects performance.
Headers & response codes
Implement the StatusCoder
and Headerer
interfaces to customise headers and response codes.
type MyResponse struct {
Foo string `json:"foo"`
}
// Set a custom HTTP response code.
func (nr *MyResponse) StatusCode() int {
return 201
}
// Add custom headers to the response.
func (nr *MyResponse) Header() http.Header {
header := http.Header{}
header.Set("foo", "bar")
return header
}
Sub-routers
You can create sub-routers using the Group
function:
r := don.New(nil)
sub := r.Group("/api")
sub.Get("/hello", don.H(Hello))
Middleware
Don uses the standard fasthttp middleware format of
func(fasthttp.RequestHandler) fasthttp.RequestHandler
.
For example:
func loggingMiddleware(next fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
log.Println(string(ctx.RequestURI()))
next(ctx)
}
}
It is registered on a router using Use
e.g.
r := don.New(nil)
r.Post("/", don.H(handler))
r.Use(loggingMiddleware)
Middleware registered on a group only applies to routes in that group and child groups.
r := don.New(nil)
r.Get("/login", don.H(loginHandler))
r.Use(loggingMiddleware) // applied to all routes
api := r.Group("/api")
api.Get("/hello", don.H(helloHandler))
api.Use(authMiddleware) // applied to routes `/api/hello` and `/api/v2/bye`
v2 := api.Group("/v2")
v2.Get("/bye", don.H(byeHandler))
v2.Use(corsMiddleware) // only applied to `/api/v2/bye`
To pass values from the middleware to the handler extend the context e.g.
func myMiddleware(next fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
ctx.SetUserValue(ContextUserKey, "my_user")
next(ctx)
}
}
This can now be accessed in the handler:
user := ctx.Value(ContextUserKey).(string)
Benchmarks
To give you a rough idea of Don's performance, here is a comparison with Gin.
Request Parsing
Don has extremely fast & efficient binding of request data.
Benchmark name | (1) | (2) | (3) | (4) |
---|---|---|---|---|
BenchmarkDon_BindRequest | 2947474 | 390.3 ns/op | 72 B/op | 2 allocs/op |
BenchmarkGin_BindRequest | 265609 | 4377 ns/op | 1193 B/op | 21 allocs/op |
Source: benchmarks/binding_test.go
Serving HTTP Requests
Keep in mind that the majority of time here is actually the HTTP roundtrip.
Benchmark name | (1) | (2) | (3) | (4) |
---|---|---|---|---|
BenchmarkDon_HTTP | 45500 | 25384 ns/op | 32 B/op | 3 allocs/op |
BenchmarkGin_HTTP | 22995 | 49865 ns/op | 2313 B/op | 21 allocs/op |
Source: benchmarks/http_test.go