GithubHelp home page GithubHelp logo

bmf-san / goblin Goto Github PK

View Code? Open in Web Editor NEW
76.0 2.0 7.0 9.88 MB

A golang http router based on trie tree.

License: MIT License

Go 95.70% Makefile 4.30%
golang http-router router url-router trie trie-tree go http httprouter middleware nethttp routing

goblin's Introduction

English 日本語

goblin

Mentioned in Awesome Go GitHub release CircleCI Go Report Card codecov GitHub license Go Reference Sourcegraph

A golang http router based on trie tree.

goblin

This logo was created by gopherize.me.

Table of contents

Features

  • Go1.21 >= 1.16
  • Simple data structure based on trie tree
  • Lightweight
    • Lines of codes: 2428
    • Package size: 140K
  • No dependencies other than standard packages
  • Compatible with net/http
  • More advanced than net/http's Servemux
    • Method based routing
    • Named parameter routing
    • Regular expression based routing
    • Middleware
    • Customizable error handlers
    • Default OPTIONS handler
  • 0allocs
    • Achieve 0 allocations in static routing
    • About 3allocs for named routes
      • Heap allocation occurs when creating parameter slices and storing parameters in context

Install

go get -u github.com/bmf-san/goblin

Example

A sample implementation is available.

Please refer to example_goblin_test.go.

Usage

Method based routing

Routing can be defined based on any HTTP method.

The following HTTP methods are supported. GET/POST/PUT/PATCH/DELETE/OPTIONS

r := goblin.NewRouter()

r.Methods(http.MethodGet).Handler(`/`, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "/")
}))

r.Methods(http.MethodGet, http.MethodPost).Handler(`/methods`, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    if r.Method == http.MethodGet {
        fmt.Fprintf(w, "GET")
    }
    if r.Method == http.MethodPost {
        fmt.Fprintf(w, "POST")
    }
}))

http.ListenAndServe(":9999", r)

Named parameter routing

You can define routing with named parameters (:paramName).

r := goblin.NewRouter()

r.Methods(http.MethodGet).Handler(`/foo/:id`, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    id := goblin.GetParam(r.Context(), "id")
    fmt.Fprintf(w, "/foo/%v", id)
}))

r.Methods(http.MethodGet).Handler(`/foo/:name`, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    name := goblin.GetParam(r.Context(), "name")
    fmt.Fprintf(w, "/foo/%v", name)
}))

http.ListenAndServe(":9999", r)

Regular expression based routing

By using regular expressions for named parameters (:paramName[pattern]), you can define routing using regular expressions.

r.Methods(http.MethodGet).Handler(`/foo/:id[^\d+$]`, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    id := goblin.GetParam(r.Context(), "id")
    fmt.Fprintf(w, "/foo/%v", id)
}))

Middleware

Supports middleware to help pre-process requests and post-process responses.

Middleware can be defined for any routing.

Middleware can also be configured globally. If a middleware is configured globally, the middleware will be applied to all routing.

More than one middleware can be configured.

Middleware must be defined as a function that returns http.

// Implement middleware as a function that returns http.Handl
func global(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "global: before\n")
		next.ServeHTTP(w, r)
		fmt.Fprintf(w, "global: after\n")
	})
}

func first(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "first: before\n")
		next.ServeHTTP(w, r)
		fmt.Fprintf(w, "first: after\n")
	})
}

func second(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "second: before\n")
		next.ServeHTTP(w, r)
		fmt.Fprintf(w, "second: after\n")
	})
}

func third(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "third: before\n")
		next.ServeHTTP(w, r)
		fmt.Fprintf(w, "third: after\n")
	})
}

r := goblin.NewRouter()

// Set middleware globally
r.UseGlobal(global)
r.Methods(http.MethodGet).Handler(`/globalmiddleware`, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "/globalmiddleware\n")
}))

// Use methods can be used to apply middleware
r.Methods(http.MethodGet).Use(first).Handler(`/middleware`, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "middleware\n")
}))

// Multiple middleware can be configured
r.Methods(http.MethodGet).Use(second, third).Handler(`/middlewares`, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "middlewares\n")
}))

http.ListenAndServe(":9999", r)

A request to /globalmiddleware gives the following results.

global: before
/globalmiddleware
global: after

A request to /middleware gives the following results.

global: before
first: before
middleware
first: after
global: after

A request to /middlewares gives the following results.

global: before
second: before
third: before
middlewares
third: after
second: after
global: after

Customizable error handlers

You can define your own error handlers.

The following two types of error handlers can be defined

  • NotFoundHandler
    • Handler that is executed when no result matching the routing is obtained
  • MethodNotAllowedHandler
    • Handler that is executed when no matching method is found
func customMethodNotFound() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "customMethodNotFound")
	})
}

func customMethodAllowed() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "customMethodNotAllowed")
	})
}

r := goblin.NewRouter()
r.NotFoundHandler = customMethodNotFound()
r.MethodNotAllowedHandler = customMethodAllowed()

http.ListenAndServe(":9999", r)

Default OPTIONS handler

You can define a default handler that will be executed when a request is made with the OPTIONS method.

func DefaultOPTIONSHandler(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusNoContent)
	})
}

r := goblin.NewRouter()
r.DefaultOPTIONSHandler = DefaultOPTIONSHandler()

http.ListenAndServe(":9999", r)

The default OPTIONS handler is useful, for example, in handling CORS OPTIONS requests (preflight requests).

Benchmark tests

We have a command to run a goblin benchmark test.

Please refer to Makefile.

Curious about benchmark comparison results with other HTTP Routers?

Please see here! bmf-san/go-router-benchmark

Design

This section describes the internal data structure of goblin.

While radix tree is often employed in performance-optimized HTTP Routers, goblin uses trie tree.

Compared to radix trees, trie tree have a disadvantage in terms of performance due to inferior memory usage. However, the simplicity of the algorithm and ease of understanding are overwhelmingly in favor of the trie tree.

HTTP Router may seem like a simple application with a simple specification, but it is surprisingly complex. You can see this by looking at the test cases. (If you have an idea for a better-looking test case implementation, please let us know.)

One advantage of using a simple algorithm is that it contributes to code maintainability. (This may sound like an excuse for the difficulty of implementing a radix tree... in fact, the difficulty of implementing an HTTP Router based on a radix tree frustrated me once...)

Using the source code of _examples as an example, I will explain the internal data structure of goblin.

The routing definitions are represented in a table as follows.

Method Path Handler Middleware
GET / RootHandler N/A
GET /foo FooHandler CORS
POST /foo FooHandler CORS
GET /foo/bar FooBarHandler N/A
GET /foo/bar/:name FooBarNameHandler N/A
POST /foo/:name FooNameHandler N/A
GET /baz BazHandler CORS

In gobin, such routing is represented as the following tree structure.

legend:<HTTP Method>,[Node]

<GET>
    ├── [/]
    |
    ├── [/foo]
    |        |
    |        └── [/bar]
    |                 |
    |                 └── [/:name]
    |
    └── [/baz]

<POST>
    └── [/foo]
             |
             └── [/:name]

The tree is constructed for each HTTP method.

Each node has handler and middleware definitions as data.

In order to simplify the explanation, data such as named routing data and global middleware data are omitted here.

Various other data is held in the internally constructed tree.

If you want to know more, use the debugger to take a peek at the internal structure.

If you have any ideas for improvements, please let us know!

Wiki

References are listed on the wiki.

Contribution

Issues and Pull Requests are always welcome.

We would be happy to receive your contributions.

Please review the following documents before making a contribution.

Sponsor

If you like it, I would be happy to have you sponsor it!

GitHub Sponsors - bmf-san

Or I would be happy to get a STAR.

It motivates me to keep up with ongoing maintenance :D

Stargazers

Stargazers repo roster for @bmf-san/goblin

Forkers

Forkers repo roster for @bmf-san/goblin

License

Based on the MIT License.

LICENSE

Author

bmf-san

goblin's People

Contributors

bmf-san avatar fox20431 avatar rinchsan avatar songmu avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

goblin's Issues

Add test case name to test cases.

Description

Add test case name to test cases.

ex.

t.Run(testname, func(t *testing.T) {
 // do something
})

I want to set the name because it is difficult to understand which test case failed.

Describe the feature you'd like to request

Describe the solution you'd like

Is there any way to support group?

Such like GIN, group can quickly create sub routers for modules with same middlewares. Is there any way to support group or just in feature?

For eg.:

r := goblin.NewRouter()
group :=r.Group("/module1")
group.Use(someMiddlwareOnlyForThisGroup)

group.Methods(http.MethodGet).Handler("/freeAPI", ...)
group.Methods(http.MethodGet).Use(Authenticator).Handler("/secretAPI", ...)

Support DisableGeneralOptionsHandler

Description

DisableGeneralOptionsHandler will be available in go1.20.

Describe the feature you'd like to request

Want to have an option that can set the DisableGeneralOptionsHandler.

Describe the solution you'd like

Update benchmark test

Description

Run benchmark tests in the latest environment

goblin: [1.0.0](https://github.com/bmf-san/goblin/releases/tag/1.1.0) → Run with latest version!
Golang version: 1.14 → Run with latest version!
Model Name: MacBook Air
Model Identifier: MacBookAir8,1
Processor Name: Dual-Core Intel Core i5
Processor Speed: 1.6 GHz
Number of Processors: 1
Total Number of Cores: 2
Memory: 16 GB

Tasks

  • Run benchmark tests with latest environment.
  • Update benchmark section in README.md

To Improve trie tree

Description

I noticed a data structure problem while considering implementing middleware for CORS.
In the current structure of the trie tree, HTTP verb matching is indispensable for routing matching.
This is inconvenient for middleware application.
There are cases where you want to apply middleware processing before determining HTTP verb matching.

Todo

Modify the data structure so that the Node has an HTTP verb.

To improve quality

Description

There are few test cases, so reconsider it for reducing bugs and improving quality.

Todo

  • To reconsider test cases.

Redesign methods for registering routes

Description

As a title.

Better

func CORS(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Max-Age", "86400")
		w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, PATCH")
		w.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, Authorization, Access-Control-Allow-Origin")
		w.Header().Set("Access-Control-Expose-Headers", "Content-Length, Pagination-Count, Pagination-Pagecount, Pagination-Page, Pagination-Limit")

		next.ServeHTTP(w, r)
	})
}
r.GET(`/`).Use(first).Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "CORS")
}))
r.OPTIONS(`/`).Use(CORS).Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    return
}))

More better

func CORS(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Max-Age", "86400")
		w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, PATCH")
		w.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, Authorization, Access-Control-Allow-Origin")
		w.Header().Set("Access-Control-Expose-Headers", "Content-Length, Pagination-Count, Pagination-Pagecount, Pagination-Page, Pagination-Limit")

		next.ServeHTTP(w, r)
	})
}
r.Methods(http.MethodGet, httpp.MethodOptions).Use(first).Handler(`/`, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "CORS")
}))

I think above pattern is more better.

References

https://github.com/gorilla/mux#handling-cors-requests

To improve regexp perfomance

Description

Go regexp lib is slow so we need to improve perfomance.
bmf-go-router uses regexp for routing pattern with regular expression.

Solution

Initializing regexp pattern is costly, maybe this will be solved by initializing regexp pattern as global variables.

Example

var re = regexp.MustCompile("[a-z]{3}")

func main() {
    fmt.Println(re.FindAllString("foobar", -1))
    // => [foo bar]
}

Fix bugs for "/" endpoint

Description

	r := goblin.NewRouter()

	r.GET(`/`, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "/")
	}))
	r.GET(`/foo/`, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "/foo/")
	}))
	r.GET(`/foo/bar/`, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "/foo/bar/")
	}))
	r.GET(`/foo/bar/:id/`, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		id := goblin.GetParam(r.Context(), "id")
		fmt.Fprintf(w, "/foo/bar/%v/", id)
	}))
        ...

This definition expects access to the following URI,

/
/foo/
/foo/bar/
/foo/bar/{id} ex. /foo/bar/1

But in fact, I don't know why I can access undefined URIs without any errors.

/hoge → No errors, and return responses for /.

0 allocs challege

Description

Improve allocation to 0 allocs in the path parameter test cases.

make test-benchmark CPU=1 COUNT=1

go test -bench=. -cpu=1 -benchmem -count=1
goos: darwin
goarch: arm64
pkg: github.com/bmf-san/goblin
BenchmarkStaticRoutesRootGoblin         75517075                15.65 ns/op            0 B/op          0 allocs/op
BenchmarkStaticRoutes1Goblin            39804074                29.65 ns/op            0 B/op          0 allocs/op
BenchmarkStaticRoutes5Goblin             9692804               124.4 ns/op             0 B/op          0 allocs/op
BenchmarkStaticRoutes10Goblin            4268844               280.4 ns/op             0 B/op          0 allocs/op
BenchmarkPathParamRoutes1ColonGoblin     6099525               199.8 ns/op           328 B/op          3 allocs/op  // here
BenchmarkPathParamRoutes5ColonGoblin     3435362               350.5 ns/op           328 B/op          3 allocs/op // here
BenchmarkPathParamRoutes10ColonGoblin    2185375               553.1 ns/op           328 B/op          3 allocs/op  // here
PASS
ok      github.com/bmf-san/goblin       10.852s

Describe the feature you'd like to request

I think it's probably due to the processing of the path parameter in the search method.
https://github.com/bmf-san/goblin/blob/master/trie.go#L250-L263

Describe the solution you'd like

Umm...

Improve error messages

Description

I feel that the error message is messy, so I want to make it feel good.

Task

Improve some error messages.

Error message improvement points

  • What kind of error is it?
  • Why did the error occur?
  • If the cause is clear, describe the cause. 

Do you have any other good ideas?

Example of the part I want to fix

goblin/trie.go

Line 127 in fce2974

return nil, fmt.Errorf("the value of %q is wrong", ptn)

goblin/trie.go

Line 148 in 78aa557

return nil, errors.New("tree is empty")

goblin/trie.go

Line 165 in fce2974

return &Result{}, errors.New("handler is not registered")

etc...

Update wiki page

Description

This diagram is out of date and will be updated.

スクリーンショット 2023-01-13 2 45 52

cf. #88

Describe the feature you'd like to request

Update diagram.

Describe the solution you'd like

Write new diagram.

About the proposal to enhance servemux being ACCEPTED

About this issue.
golang/go#61410

I can't say anything definitive because I don't know what the final implementation will be, but I think it will be easier to consider the option of using standard ServeMux instead of using third-party libraries.

I think that the reasons for using third-party libraries are good performance and richness of functions.

Regarding the future of goblin, we intend to continue maintenance for a while.
For applications that personally use goblin, I might switch to the standard ServeMux.
It's hard to say whether goblin will have any advantages over the new ServeMux, but one thing that seems likely is support for regular expressions.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.