Vi Router π
Published at November 16, 2023
βThis project draws inspiration from gorilla/mux, and I appreciate the ease of working with the library.However, its performance falls short of my expectations. After conducting research, my goal is to develop a high-performance HTTP router that not only outperforms but also retains the convenient API of mux, enhanced with additional support for regex. I welcome any feedback as this will be my first open source projects.β
Installation
go get -u github.com/diontr00/vi
TestRegex
If you want to make sure whether the regex matcher work as you expected , you can use TestMatcher function in the init function
func init() {
// Register helper for ip address
RegisterHelper(
"ip",
`((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|
[1-9][0-9]|[0-9]).){3}(25[0-5]|
2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])`
)
// Register helper for phone number
RegisterHelper(
"phone",
`([+]?[s0-9]+)?(d{3}|[(]?[0-9]+[)])
?([-]?[s]?[0-9])+`
)
// Expect match
errs := TestMatcher(
true,
"/user/{id:[0-9]+}/:ip/:phone?",
map[vi.TestUrl]vi.TestResult{
"/user/101/192.168.0.1/999-9999999":
{
"id": "101",
"ip": "192.168.0.1",
"phone": "999-9999999"
},
"/user/102/192.168.0.2/+48(12)504-203-260":
{
"id": "102",
"ip": "192.168.0.2",
"phone": "+48(12)504-203-260"
},
"/user/103/192.168.0.3":
{
"id": "103",
"ip": "192.168.0.3"
},
"/user/104/192.168.0.4/555-5555-555":
{
"id": "104",
"ip": "192.168.0.4",
"phone": "555-5555-555"
},
})
for i := range errs {
fmt.Println(errs[i])
}
// Expect not match
errs := TestMatcher(
false,
....)
}
Simple Usage
package main
import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/diontr00/vi"
)
const customNotFoundMsg = "Custom Not Found Message"
func main() {
customNotFoundHandler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(customNotFoundMsg))
}
mux := vi.New(&vi.Config{
Banner: false,
NotFoundHandler: customNotFoundHandler,
})
vi.RegisterHelper("ip", `d{1,3}.d{1,3}.d{1,3}.d{1,3}`)
mux.Get("/location/:ip", func(w http.ResponseWriter, r *http.Request) {
ip := vi.GetParam("ip")
msg := fmt.Sprintf("You have searched for the IP address %s \n", ip)
w.Write([]byte(msg))
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
}
done := make(chan os.Signal, 1)
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
log.Print("Server Started")
<-done
log.Print("Server Stopped")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer func() {
// extra handling here
cancel()
}()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("Server Shutdown Failed: %+v", err)
}
log.Print("Server Exited Properly")
}
Serving Static Files
This receipt will serve any βuserimageβ.png file under userfile static folder. Note that prefix is use to append to asset path, so userimage.png eventually become /static/userfile/userimage.png
Other properties:
- Root: Can be either http.Dir or embed with go embed fs
- MaxAge: Can be set in term of second to control the cache header
- Next: Skip function , will skip when query param ignore is true in this scenario
- NotFoundFile: if not found , it will be serve
func main() {
vi := vi.New()
vi.Static("/{userimage:[0-9a-zA-Z_.]+}.png" ,
&StaticConfig{
Root : http.Dir("static"),
Prefix: "userfile",
Index : "index.html",
MaxAge: 2,
Next : func(w http.ResponseWriter , r *http.Request) bool {
return r.URL.Query().Get("ignore") == "true"
},
NotFoundFile : "error/notfound.html"
} )
http.ListenAndServe(":8080", vi)
}
Matching Rule
- Named parameter
- Syntax: :name
- Example: /student/:name
- Explain: Match name as word
- Name with regex pattern
- Syntax: {name:regex-pattern}
- Example: /student/{id:[0-9]+}
- Explain: Match id as number
- Helper pattern
- :id : short for /student/{id:[0-9]+}
- :name : short for /{name:[0-9a-zA-Z]+}
- Register pattern
- :ip : RegisterPattern("ip-regex")
....
Benchmark
Run benchmark and test with ginkgo: