0
0
Lập trình
Admin Team
Admin Teamtechmely

Xây Dựng Router Cho Web Server Bằng Go

Đăng vào 7 tháng trước

• 9 phút đọc

Giới thiệu

Trong các phần trước, chúng ta đã xây dựng một web server cơ bản bằng Go với các route được định nghĩa bằng cách sử dụng các câu lệnh switch. Mặc dù cách làm này hoạt động, nhưng nó có thể khiến cho việc mở rộng và bảo trì trở nên phức tạp, đặc biệt khi bạn cần thêm nhiều endpoint mới. Để giải quyết vấn đề này, trong bài viết này, chúng ta sẽ xây dựng một router hoàn chỉnh, một thành phần riêng biệt để xử lý tất cả logic định tuyến, tương tự như cách mà Express.js thực hiện với app.get()app.post().

Những gì chúng ta đã xây dựng cho đến nay

Từ các chương trước, chúng ta đã có:

  • ✅ Một server HTTP cơ bản lắng nghe các yêu cầu
  • ✅ Định tuyến dựa trên đường dẫn sử dụng câu lệnh switch
  • ❌ Các câu lệnh switch lồng nhau khó bảo trì
  • ❌ Không có cách nào sạch để đăng ký các route một cách động

Vấn đề với cách tiếp cận hiện tại

Hãy xem xét cách mà phương pháp switch lồng nhau dẫn đến các vấn đề:

go Copy
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    path := r.URL.Path
    log.Printf("%s %s", r.Method, path)
    w.Header().Set("Content-Type", "text/plain; charset=utf-8")
    w.WriteHeader(http.StatusOK)

    switch r.Method {
    case http.MethodGet:
        GET(path, w)
    case http.MethodPost:
        POST(path, w)
    default:
        not_Supported(w)
    }
}

func GET(path string, w http.ResponseWriter) {
    switch path {
    case "/hello":
        hello(w)
    case "/goodbye":
        goodbye(w)
    case "/time":
        getTime(w)
    default:
        welcome(w)
    }
}

func POST(path string, w http.ResponseWriter) {
    switch path {
    case "/hello":
        post_hello(w)
    case "/messages":
        messages(w)
    case "/time":
        post_getTime(w)
    default:
        post_welcome(w)
    }
}

Một số vấn đề của cách tiếp cận này:

  1. Logic rời rạc: Định nghĩa route được phân tán ở nhiều hàm khác nhau.
  2. Khó mở rộng: Thêm các phương thức HTTP mới yêu cầu tạo thêm hàm mới.
  3. Không có đăng ký động: Không thể thêm các route một cách lập trình.
  4. Tổ chức kém: Không giống như app.get('/users', handler) trong Express.

Hiểu điều mà các framework đang làm

Khi sử dụng Express.js, bạn có thể viết:

go Copy
app.get('/users', getUsersHandler)
app.post('/users', createUserHandler)  
app.put('/users/:id', updateUserHandler)

Hay với Gin trong Go:

go Copy
r.GET("/users", getUsersHandler)
r.POST("/users", createUserHandler)
r.PUT("/users/:id", updateUserHandler)

Vậy điều gì đang diễn ra bên dưới? Framework lưu trữ các route này trong một cấu trúc dữ liệu và so khớp các yêu cầu đến với chúng. Hãy cùng xây dựng một phiên bản riêng của chúng ta.

Thiết kế Router

Chúng ta cần một router có khả năng:

  1. Đăng ký các route với phương thức + đường dẫn + hàm xử lý.
  2. So khớp các yêu cầu đến với hàm xử lý đúng.
  3. Thực thi hàm xử lý đã so khớp hoặc trả về lỗi 404.

Thiết kế cấu trúc dữ liệu

Chúng ta cần lưu trữ các route theo phương thức HTTP và đường dẫn:

go Copy
routes = {
    "GET": {
        "/hello": helloHandler,
        "/users": getUsersHandler
    },
    "POST": {
        "/users": createUserHandler,
        "/messages": messagesHandler
    }
}

Xây dựng thành phần Router

Tạo một file mới router.go:

go Copy
package main

import (
    "fmt"
    "log"
    "net/http"
)

type HandlerFunc func(w http.ResponseWriter, r *http.Request)

type Router struct {
    routes map[string]map[string]HandlerFunc
}

func NewRouter() *Router {
    return &Router{
        routes: make(map[string]map[string]HandlerFunc),
    }
}

Các quyết định thiết kế chính:

  1. Kiểu HandlerFunc: Điều này phù hợp với chữ ký mà các hàm xử lý cần - có quyền truy cập cả vào phản hồi và yêu cầu.
  2. Bản đồ lồng nhau: map[method]map[path]handler cho phép tra cứu nhanh O(1).
  3. Con trỏ receiver: Chúng ta sẽ sửa đổi trạng thái của router khi thêm route.

Thêm các phương thức đăng ký route

Bây giờ hãy thêm các phương thức cho phép chúng ta đăng ký các route (giống như app.get() của Express):

go Copy
func (r *Router) addRoute(method string, path string, handler HandlerFunc) {
    if r.routes[method] == nil {
        r.routes[method] = make(map[string]HandlerFunc)
    }

    r.routes[method][path] = handler
}

func (r *Router) GET(path string, handler HandlerFunc) {
    r.addRoute(http.MethodGet, path, handler)
}

func (r *Router) POST(path string, handler HandlerFunc) {
    r.addRoute(http.MethodPost, path, handler)
}

func (r *Router) PUT(path string, handler HandlerFunc) {
    r.addRoute(http.MethodPut, path, handler)
}

func (r *Router) PATCH(path string, handler HandlerFunc) {
    r.addRoute(http.MethodPatch, path, handler)
}

func (r *Router) DELETE(path string, handler HandlerFunc) {
    r.addRoute(http.MethodDelete, path, handler)
}

Bây giờ chúng ta có thể đăng ký các route giống như trong các framework:

go Copy
router.GET("/hello", helloHandler)
router.POST("/users", createUserHandler)

Thực hiện giải quyết route

Router cần phải so khớp các yêu cầu đến với các hàm xử lý:

go Copy
func (r *Router) resolveRoute(req *http.Request) (HandlerFunc, error) {
    method := req.Method
    path := req.URL.Path

    log.Printf("%s %s", method, path)

    methodRoutes, ok := r.routes[method]
    if !ok {
        return nil, fmt.Errorf("phương thức không được hỗ trợ")
    }

    handler, ok := methodRoutes[path]
    if !ok {
        return nil, fmt.Errorf("đường dẫn không tồn tại")
    }

    return handler, nil
}

Làm cho Router xử lý các yêu cầu HTTP

Router của chúng ta cần phải thực hiện interface http.Handler để có thể xử lý các yêu cầu:

go Copy
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    handler, err := r.resolveRoute(req)

    if err != nil {
        w.WriteHeader(http.StatusNotFound)
        w.Write([]byte("route không tìm thấy"))
        return
    }

    handler(w, req)
}

Điều này thật tuyệt - router của chúng ta tìm hàm xử lý đúng và gọi nó, hoặc trả về 404 nếu không tìm thấy.

Cập nhật Server để sử dụng Router

Bây giờ chúng ta cần tích hợp router vào server. Cập nhật server.go:

go Copy
package main

import (
    "log"
    "net/http"
)

type Server struct {
    Addr   string
    Router *Router
    server *http.Server
}

func NewServer(addr string) *Server {
    router := NewRouter()

    server := &Server{
        Addr:   addr,
        Router: router,
    }

    return server
}

func (s *Server) Start() {
    s.server = &http.Server{
        Addr:    s.Addr,
        Handler: s,
    }
    log.Println("Server bắt đầu tại", s.Addr)
    err := s.server.ListenAndServe()
    if err != nil {
        log.Fatal(err)
    }
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    s.Router.ServeHTTP(w, r)
}

Những gì đã thay đổi?

  • Server bây giờ có một trường Router.
  • NewServer tạo một instance của router.
  • ServeHTTP ủy quyền cho router thay vì xử lý định tuyến trực tiếp.

Tạo các hàm xử lý

Chúng ta cần cập nhật các hàm xử lý để phù hợp với chữ ký mới của HandlerFunc:

go Copy
func welcome(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Chào mừng đến với Webserver Go của chúng ta!"))
}

func hello(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Xin chào, Thế giới!"))
}

func goodbye(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Tạm biệt, hẹn gặp lại!"))
}

func getTime(w http.ResponseWriter, r *http.Request) {
    message := fmt.Sprintf("Thời gian hiện tại: %s", time.Now().Format("2006-01-02 15:04:05"))
    w.Write([]byte(message))
}

func post_hello(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Xin chào qua POST!"))
}

func messages(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Bạn đã gửi một tin nhắn mới"))
}

func post_getTime(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Yêu cầu POST nhận được trên /time"))
}

func post_welcome(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Chào mừng đến với Webserver Go từ POST!"))
}

Lưu ý: Chúng ta đã thêm tham số *http.Request vào mỗi hàm xử lý. Bây giờ các hàm xử lý có quyền truy cập vào toàn bộ thông tin yêu cầu.

Đăng ký các Route

Cập nhật main.go để đăng ký các route bằng API router mới:

go Copy
package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {
    server := NewServer(":3000")
    setupRoutes(server)

    server.Start()
}

func setupRoutes(s *Server) {
    s.Router.GET("/", welcome)
    s.Router.GET("/hello", hello)
    s.Router.GET("/goodbye", goodbye)
    s.Router.GET("/time", getTime)
    s.Router.POST("/", post_welcome)
    s.Router.POST("/hello", post_hello)
    s.Router.POST("/messages", messages)
    s.Router.POST("/time", post_getTime)
}

Hãy xem nó thật sạch sẽ! Nó đọc giống như các định nghĩa route trong Express hoặc Gin.

Kiểm tra Router mới của chúng ta

Hãy kiểm tra server dựa trên router:

bash Copy
go run .

Thử nghiệm với các route và phương thức khác nhau:

bash Copy
# Các route GET
curl http://localhost:3000/
curl http://localhost:3000/hello
curl http://localhost:3000/goodbye
curl http://localhost:3000/time

# Các route POST
curl -X POST http://localhost:3000/
curl -X POST http://localhost:3000/hello
curl -X POST http://localhost:3000/messages

# Kiểm tra 404
curl http://localhost:3000/nonexistent
curl -X DELETE http://localhost:3000/hello

Hành vi mong đợi:

  • Các route GET và POST hoạt động như mong đợi.
  • Các đường dẫn không xác định trả về "route không tìm thấy".
  • Các phương thức không hỗ trợ trả về "route không tìm thấy".

Những gì chúng ta đã đạt được

Chúng ta đã có:

  • Đăng ký route sạch sẽ (như Express/Gin)
  • Tách biệt mối quan tâm (logic định tuyến tách biệt khỏi logic server)
  • Nhận thức phương thức HTTP (GET và POST hành xử khác nhau)
  • Xử lý lỗi đúng cách (404 cho các route không xác định)
  • Kiến trúc có thể mở rộng (dễ dàng thêm các route mới)

Giới hạn hiện tại

Router của chúng ta đã tốt hơn, nhưng vẫn còn một số giới hạn:

1. Không có Route Động

Chúng ta không thể xử lý /users/123 hoặc /products/456 - mỗi route phải được định nghĩa chính xác.

2. Không có Hỗ Trợ Middleware

Các API thực tế cần ghi log, xác thực, CORS, v.v. Chúng ta không có cách nào để thêm những vấn đề chéo này.

3. Phản hồi lỗi cơ bản

Phản hồi 404 của chúng ta chỉ là văn bản thuần túy - các API thực tế trả về phản hồi lỗi JSON.

Điều gì tiếp theo?

Trong Chương 4, chúng ta sẽ giải quyết giới hạn lớn nhất - các route động. Chúng ta sẽ cho phép các route như /users/:id/products/:category/:id, và học cách trích xuất các tham số đó trong các hàm xử lý của chúng ta. Điều này sẽ đưa chúng ta gần hơn đến định tuyến sẵn sàng cho sản xuất có thể xử lý các yêu cầu API thực tế.


Thử thách: Hãy thử thêm một số route mới sử dụng các phương thức HTTP khác nhau. Thêm một route DELETE cho /users và một route PUT cho /messages. Xem thử nó dễ dàng như thế nào so với cách tiếp cận bằng câu lệnh switch!

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào