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.HandlerFunctype 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

Tham khảo