Ở bài viết trước chúng ta đã tìm hiểu về package net/http
trong thư viện standard của Go Lang và sử dụng net/http
để viết một web api đơn giản. Tuy nhiên, ta có thể thấy tính năng router của ServerMux
của net/http
khá đơn giản, chỉ hỗ trợ những route cố định và người lập trình phải tự xử lý những trường hợp phức tạp hơn. Với những API đơn giản như trong ví dụ của bài viết trước thì điều này không thành vấn đề, tuy nhiên sẽ là khá khó khăn với những API phức tạp với số lượng route lớn. Bài viết lần này trên nền net/http
chúng ta sẽ tự thử xây dựng 1 router sử dụng Regex để việc routing được flexible và dễ dàng hơn.
Mục tiêu
Regex router mà ta sẽ tạo ra sẽ giúp chúng ta thực hiện việc routing 1 cách đơn giản như đoạn code ở phía dưới. Chúng ta có thể dùng Regex để định nghĩa cho route của mình 1 cách flexible với các route param nằm trong capture group của Regex, HTTP method của route cũng được định nghĩa bằng cách sử dụng những hàm được cung cấp bởi router. Ngoài ra để tương thích với net\http
các hàm xử lý vẫn giữ nguyên đặc tính của interface http.HandlerFunc
là type HandlerFunc func(ResponseWriter, *Request)
// main.go
func main() {
rt := NewRouter()
rt.Get("/", HelloServer)
rt.Get("/posts", GetPosts)
rt.Get("/posts/([0-9]+)", GetPost)
rt.Post("/posts", CreatePost)
rt.Put("/posts/([0-9]+)", UpdatePost)
rt.Delete("/posts/([0-9]+)", DeletePost)
log.Fatal(http.ListenAndServe(":8080", rt))
}
func HelloServer(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!")
}
Cấu trúc dữ liệu
Trước tiên ta cần định nghĩa các cấu trúc dữ liệu cần thiết cho Router của mình. Trong file router.go
, ta sẽ định nghĩa những struct như ở phía dưới
// router.go
type regexRoute struct {
method string
regex *regexp.Regexp
handler http.HandlerFunc
}
type Router struct {
handlersTable map[string]regexRoute
}
regexRoute
sẽ là struct lưu trữ định nghĩa từng route còn Router
sẽ chứa toàn bộ các route của router trong 1 một regex table dưới định dạng m
Để khởi tạo router thì ta sẽ định nghĩa hàm sau
// router.go
func NewRouter() *Router {
rt := &Router{make(map[string]regexRoute)}
return rt
}
Tạo route mới
Tiếp theo ta cần viết các hàm để lưu route vào các cấu trúc dữ liệu đã định nghĩa ở trên. Ta sẽ viết các hàm Get
, Post
, Put
, Delete
tương ứng với các HTTP method
// router.go
func (rt *Router) Get(path string, h http.HandlerFunc) {ap
rt.addRoute("GET", path, h)
}
func (rt *Router) Post(path string, h http.HandlerFunc) {
rt.addRoute("POST", path, h)
}
func (rt *Router) Put(path string, h http.HandlerFunc) {
rt.addRoute("PUT", path, h)
}
func (rt *Router) Delete(path string, h http.HandlerFunc) {
rt.addRoute("DELETE", path, h)
}
Hàm addRoute
với nhiệm vụ chính là tạo regex từ định nghĩa route, tạo struct regexRoute
và add vào trong regex table của router
// router.go
func (rt *Router) addRoute(method string, path string, h http.HandlerFunc) {
if _, ok := rt.handlersTable[path]; ok {
return
}
exactPath := "^" + path + "$"
rgRoute := regexRoute{method, regexp.MustCompile(exactPath), h}
rt.handlersTable[path] = rgRoute
}
Routing
Nhiệm vụ tiếp theo của chúng ta là viết hàm để xử lý routing request đến những định nghĩa route đã lưu trong Router. Tất cả logic cho phần này sẽ nằm ở trong hàm dưới
// router.go
func (rt *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
for _, rgRoute := range rt.handlersTable {
if r.Method != rgRoute.method {
continue
}
matches := rgRoute.regex.FindStringSubmatch(r.URL.Path)
if len(matches) <= 0 {
continue
}
ctx := context.WithValue(r.Context(), pathParamKey, matches[1:])
rgRoute.handler(w, r.WithContext(ctx))
return
}
http.NotFound(w, r)
}
Ở đây type Router
implement interface http.Handler
của Go để có thể gắn router vào server HTTP được khởi tạo bởi net\http
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Dùng với server http của net\http
// main.go
func main() {
rt := NewRouter()
...
http.ListenAndServe(":8080", rt)
}
Hàm ServeHTTP
sẽ được gọi với từng request gửi đến server HTTP. Trong hàm này, ta sẽ lấy path gọi đến của request, rồi loop qua các route đã được lưu trong router, sử dụng hàm FindStringSubmatch
của regex để check xem request path có match với route nào không, nếu có thì sẽ thực hiện gọi hàm handler tương ứng.
Ngoài ra để có thể lưu được route param, ta cũng sử dụng package context
của Go để lưu giá trị param match được từ kết quả của FindStringSubmatch
(cách dùng context
xin dành cho một bài viết khác)
// router.go
type pathParamCtxKey string
const pathParamKey = pathParamCtxKey("pathkey")
func (rt *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
...
ctx := context.WithValue(r.Context(), pathParamKey, matches[1:])
rgRoute.handler(w, r.WithContext(ctx))
...
}
Để có thể đọc được giá trị path param từ context trong hàm handler cho route ta cũng viết thêm 1 hàm như sau
// router.go
func GetPathParam(r *http.Request, idx int) string {
params := r.Context().Value(pathParamKey).([]string)
return params[idx]
}
Hàm GetPathParam
này sẽ được sử dụng để lấy path param trong các hàm handler cho route như sau
// main.go
func DeletePost(w http.ResponseWriter, r *http.Request) {
param := GetPathParam(r, 0)
fmt.Fprintf(w, "DELETE /posts/%s is called\n", param)
}
Kết quả
Như vậy không tới 100 dòng code chúng ta đã có một router nho nhỏ thực hiện route matching sử dụng regex của riêng mình. Chạy thử với code trong file main.go
như ở dưới ta có thể thấy kết quả mong muốn mỗi khi truy cập các route đã được định nghĩa
// main.go
func main() {
rt := NewRouter()
rt.Get("/", HelloServer)
rt.Get("/posts", GetPosts)
rt.Get("/posts/([0-9]+)", GetPost)
rt.Post("/posts", CreatePost)
rt.Put("/posts/([0-9]+)", UpdatePost)
rt.Delete("/posts/([0-9]+)", DeletePost)
log.Fatal(http.ListenAndServe(":8080", rt))
}
func HelloServer(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!")
}
func GetPosts(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "GET /posts is called\n")
}
func GetPost(w http.ResponseWriter, r *http.Request) {
param := GetPathParam(r, 0)
fmt.Fprintf(w, "GET /posts/%s is called\n", param)
}
func CreatePost(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "POST /posts is called\n")
}
func UpdatePost(w http.ResponseWriter, r *http.Request) {
param := GetPathParam(r, 0)
fmt.Fprintf(w, "POST /posts/%s is called\n", param)
}
func DeletePost(w http.ResponseWriter, r *http.Request) {
param := GetPathParam(r, 0)
fmt.Fprintf(w, "DELETE /posts/%s is called\n", param)
}
Kết luận
Không cần thiết phải sử dụng framework nào, chỉ sử dụng net\http
chúng ta có thể tự xây dựng 1 router phù hợp cho các API không quá phức tạp. Tất nhiên còn nhiều vấn đề về performance, lẫn tính năng cần cải thiện như việc thay vì sử dụng map và regex ta có thể sử dụng cấu trúc Tree để lưu các route, hay xử lý path param một cách đẹp hơn...
Cũng đã có rất nhiều router đã được opensoure như Gin, httprouter, Chi với đầy đủ tính năng và hiệu năng cũng rất tốt, tuy nhiên việc tự viết một router cũng là một công việc rất thú vị và là một cơ hội học hỏi cho bất cứ lập trình viên nào