/ 5 min read
A Simple and Lightweight HTTP Framework Implemented in Go
Forward
I had been using a lot Go HTTP frameworks like Hertz, Gin and Fiber, while reading through the source code of these frameworks I tried to implement a simple HTTP framework myself.
A few days later, PIANO was born. I referenced code from Hertz and Gin and implemented most of the features that an HTTP framework should have.
For example, PIANO supports parameter routing, wildcard routing, and static routing, supports route grouping and middleware, and multiple forms of parameter fetching and returning. I’ll introduce these features next.
Quick Start
Install
go get github.com/B1NARY-GR0UP/piano
Or you can clone it and read the code directly.
Hello PIANO
package main
import ( "context" "net/http"
"github.com/B1NARY-GR0UP/piano/core" "github.com/B1NARY-GR0UP/piano/core/bin")
func main() { p := bin.Default() p.GET("/hello", func(ctx context.Context, pk *core.PianoKey) { pk.String(http.StatusOK, "piano") }) p.Play()}
As you can see, PIANO
’s Hello World is very simple: we register a route with the GET
function, and then all you have to do is visit localhost:7246/hello
in your browser or some other tool to see the value returned (which is “piano”).
7246
is the default listening port I set for PIANO
, you can change it to your preferred listening port with the WithHostAddr
function.
Features
Route
-
Static Route
Static route is the simplest and most common form of routing, and the one we just demonstrated in Quick Start is static route. We set up a handler to handle a fixed HTTP request URL.
package mainimport ("context""net/http""github.com/B1NARY-GR0UP/piano/core""github.com/B1NARY-GR0UP/piano/core/bin")func main() {p := bin.Default()// static route or common routep.GET("/ping", func(ctx context.Context, pk *core.PianoKey) {pk.JSON(http.StatusOK, core.M{"ping": "pong",})})p.Play()} -
Param Route
We can set a route in the form of
:yourparam
to match the HTTP request URL, andPIANO
will automatically parse the request URL and store the parameters as key / value pairs, which we can then retrieve using theParam
method. For example, if we set a route like/param/:username
, when we send a request with URLlocalhost:7246/param/lorain
, the paramusername
will be assigned with valuelorain
. And we can getusername
byParam
function.package mainimport ("context""net/http""github.com/B1NARY-GR0UP/piano/core""github.com/B1NARY-GR0UP/piano/core/bin")func main() {p := bin.Default()// param routep.GET("/param/:username", func(ctx context.Context, pk *core.PianoKey) {pk.JSON(http.StatusOK, core.M{"username": pk.Param("username"),})})p.Play()} -
Wildcard Route
The wildcard route matches all routes and can be set in the form
*foobar
.package mainimport ("context""net/http""github.com/B1NARY-GR0UP/piano/core""github.com/B1NARY-GR0UP/piano/core/bin")func main() {p := bin.Default()// wildcard routep.GET("/wild/*", func(ctx context.Context, pk *core.PianoKey) {pk.JSON(http.StatusOK, core.M{"route": "wildcard route",})})p.Play()}
Route Group
PIANO also implements route group, where we can group routes with the same prefix to simplify our code.
package main
import ( "context" "net/http"
"github.com/B1NARY-GR0UP/piano/core" "github.com/B1NARY-GR0UP/piano/core/bin")
func main() { p := bin.Default() auth := p.GROUP("/auth") auth.GET("/ping", func(ctx context.Context, pk *core.PianoKey) { pk.String(http.StatusOK, "pong") }) auth.GET("/binary", func(ctx context.Context, pk *core.PianoKey) { pk.String(http.StatusOK, "lorain") }) p.Play()}
Parameter Acquisition
The HTTP request URL or parameters in the request body can be retrieved using methods Query
, PostForm
.
- Query
package main
import ( "context" "net/http"
"github.com/B1NARY-GR0UP/piano/core" "github.com/B1NARY-GR0UP/piano/core/bin")
func main() { p := bin.Default() p.GET("/query", func(ctx context.Context, pk *core.PianoKey) { pk.JSON(http.StatusOK, core.M{ "username": pk.Query("username"), }) }) p.Play()}
- PostForm
package main
import ( "context" "net/http"
"github.com/B1NARY-GR0UP/piano/core" "github.com/B1NARY-GR0UP/piano/core/bin")
func main() { p := bin.Default() p.POST("/form", func(ctx context.Context, pk *core.PianoKey) { pk.JSON(http.StatusOK, core.M{ "username": pk.PostForm("username"), "password": pk.PostForm("password"), }) }) p.Play()}
Other
PIANO
has many other small features such as storing information in the request context, returning a response as JSON or string, middleware support, so I won’t cover them all here.
Design
Here we will introduce some of the core design of PIANO
. PIANO
is based entirely on the Golang standard library, with only one dependency for a simple log named inquisitor that I implemented myself.
go.mod
module github.com/B1NARY-GR0UP/piano
go 1.18
require github.com/B1NARY-GR0UP/inquisitor v0.1.0
Context
The design of PIANO
context is as follows:
// PianoKey play the piano with PianoKeystype PianoKey struct { Request *http.Request Writer http.ResponseWriter
index int // initialize with -1 Params Params handlers HandlersChain rwMutex sync.RWMutex KVs M}
And after referring to the context design of several frameworks, I decided to adopt Hertz’s scheme, which separates the request context from the context.Context
, this can be well used in tracing and other scenarios through the correct management of the two different lifecycle contexts.
// HandlerFunc is the core type of PIANOtype HandlerFunc func(ctx context.Context, pk *PianoKey)
The current context only provides some simple functionality, such as storing key-value pairs in KVs
, but more features will be supported later.
Route Tree
The design of the route tree uses the trie tree data structure, which inserts and searches in the form of iteration.
- Insert
// insert into trie treefunc (t *tree) insert(path string, handlers HandlersChain) { if t.root == nil { t.root = &node{ kind: root, fragment: strSlash, } } currNode := t.root fragments := splitPath(path) for i, fragment := range fragments { child := currNode.matchChild(fragment) if child == nil { child = &node{ kind: matchKind(fragment), fragment: fragment, parent: currNode, } currNode.children = append(currNode.children, child) } if i == len(fragments)-1 { child.handlers = handlers } currNode = child }}
- Search
// search matched node in trie tree, return nil when no matchedfunc (t *tree) search(path string, params *Params) *node { fragments := splitPath(path) var matchedNode *node currNode := t.root for i, fragment := range fragments { child := currNode.matchChildWithParam(fragment, params) if child == nil { return nil } if i == len(fragments)-1 { matchedNode = child } currNode = child } return matchedNode}
It is worth mentioning that the different routing support is also implemented here.
Future Work
- Encapsulation protocol layer
- Add more middleware support
- Improve engine and context functionality
- …
Summary
That’s all for this article. Hopefully, this has given you an idea of the PIANO
framework and how you can implement a simple version of HTTP framework yourself. I would be appreciate if you could give PIANO a star.
If you have any questions, please leave them in the comments or as issues. Thanks for reading.