DEV Community

Cover image for PIANO: A Simple and Lightweight HTTP Framework Implemented in Go
Lorain
Lorain

Posted on

PIANO: 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
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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 main

  import (
    "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 route
    p.GET("/ping", func(ctx context.Context, pk *core.PianoKey) {
        pk.JSON(http.StatusOK, core.M{
            "ping": "pong",
        })
    })
    p.Play()
  }
Enter fullscreen mode Exit fullscreen mode
  • Param Route

We can set a route in the form of :yourparam to match the HTTP request URL, and PIANO will automatically parse the request URL and store the parameters as key / value pairs, which we can then retrieve using the Param method. For example, if we set a route like /param/:username, when we send a request with URL localhost:7246/param/lorain, the param username will be assigned with value lorain. And we can get username by Param function.

  package main

  import (
    "context"
    "net/http"

    "github.com/B1NARY-GR0UP/piano/core"
    "github.com/B1NARY-GR0UP/piano/core/bin"
  )

  func main() {
    p := bin.Default()
    // param route
    p.GET("/param/:username", func(ctx context.Context, pk *core.PianoKey) {
        pk.JSON(http.StatusOK, core.M{
            "username": pk.Param("username"),
        })
    })
    p.Play()
  }
Enter fullscreen mode Exit fullscreen mode
  • Wildcard Route

The wildcard route matches all routes and can be set in the form *foobar.

  package main

  import (
    "context"
    "net/http"

    "github.com/B1NARY-GR0UP/piano/core"
    "github.com/B1NARY-GR0UP/piano/core/bin"
  )

  func main() {
    p := bin.Default()
    // wildcard route
    p.GET("/wild/*", func(ctx context.Context, pk *core.PianoKey) {
        pk.JSON(http.StatusOK, core.M{
            "route": "wildcard route",
        })
    })
    p.Play()
  }
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode
  • 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()
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Context

The design of PIANO context is as follows:

// PianoKey play the piano with PianoKeys
type PianoKey struct {
    Request *http.Request
    Writer  http.ResponseWriter

    index    int // initialize with -1
    Params   Params
    handlers HandlersChain
    rwMutex  sync.RWMutex
    KVs      M
}
Enter fullscreen mode Exit fullscreen mode

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 PIANO
type HandlerFunc func(ctx context.Context, pk *PianoKey)
Enter fullscreen mode Exit fullscreen mode

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 tree
func (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
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Search
// search matched node in trie tree, return nil when no matched
func (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
}
Enter fullscreen mode Exit fullscreen mode

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.

Reference List

Top comments (2)

Collapse
 
zakariachahboun profile image
zakaria chahboun

Good Job bro!

Collapse
 
justlorain profile image
Lorain

Thanks!