Middleware dưới dạng Decorator
Thuộc phần Lập trình hướng đối tượng trong hành trình GO của Coddy. Bài học 97 trên 107.
Middleware là một trong những ứng dụng thực tiễn nhất của mẫu Decorator trong Go. Trong phát triển web, các hàm middleware bao bọc HTTP handler để bổ sung những chức năng dùng chung như ghi nhật ký, xác thực hoặc đo thời gian mà không sửa đổi handler gốc.
Điểm mấu chốt là middleware nhận một handler và trả về handler mới có thêm hành vi. Điều này tuân theo chính xác mẫu Decorator: đầu vào và đầu ra có cùng giao diện, nhưng chức năng được mở rộng:
type Handler func(request string) string
func LoggingMiddleware(next Handler) Handler {
return func(request string) string {
result := next(request)
return "[LOG] " + result
}
}
func AuthMiddleware(next Handler) Handler {
return func(request string) string {
return "[AUTH] " + next(request)
}
}Mỗi middleware bọc trình xử lý tiếp theo trong chuỗi, bổ sung hành vi trước hoặc sau khi gọi trình xử lý đó. Bạn có thể xếp chồng nhiều middleware bằng cách kết hợp chúng:
func MainHandler(request string) string {
return "Response to: " + request
}
handler := LoggingMiddleware(AuthMiddleware(MainHandler))
fmt.Println(handler("GET /users"))
// [LOG] [AUTH] Phản hồi cho: GET /usersThứ tự bọc quyết định thứ tự thực thi: middleware ngoài cùng chạy trước. Mẫu này được dùng rộng rãi trong các framework web Go để xử lý yêu cầu theo quy trình, giúp bạn dễ dàng thêm hoặc gỡ bỏ chức năng mà không cần sửa logic cốt lõi của handler.
Thử thách
DễHãy xây dựng một quy trình xử lý yêu cầu bằng mẫu Middleware as Decorator! Bạn sẽ tạo một hệ thống trong đó các hàm middleware bọc các trình xử lý để bổ sung tính năng như đo thời gian, xác thực và định dạng, mà không sửa đổi logic cốt lõi của trình xử lý.
Bạn sẽ sắp xếp code trong ba tệp:
handler.go: Định nghĩa kiểu trình xử lý và trình xử lý cơ sở để xử lý các yêu cầu.Tạo kiểu
Handlerdưới dạng một hàm nhận chuỗi yêu cầu và trả về chuỗi phản hồi:type Handler func(request string) stringViết hàm
BaseHandlertrả vềProcessed: [request]cho mọi yêu cầu mà hàm nhận được.middleware.go: Tạo các hàm middleware để bọc trình xử lý và bổ sung hành vi.Tạo ba hàm middleware, mỗi hàm nhận một
Handlervà trả về mộtHandlermới:TimingMiddleware: thêm thông tin đo thời gian vào phản hồi, trả về[TIMING] [response]ValidationMiddleware: thêm tiền tố xác thực, trả về[VALID] [response]UppercaseMiddleware: chuyển toàn bộ phản hồi thành chữ hoa
Mỗi middleware cần gọi trình xử lý được bọc và bổ sung cho kết quả của nó.
main.go: Tạo quy trình xử lý bằng cách kết hợp các middleware.Đọc số lớp middleware cần áp dụng. Sau đó, với mỗi lớp, đọc loại middleware (
timing,validationhoặcuppercase) và bọc trình xử lý hiện tại bằng middleware đó. Áp dụng theo thứ tự đã đọc: middleware được đọc đầu tiên sẽ trở thành lớp bọc ngoài cùng.Sau khi tạo quy trình, đọc chuỗi yêu cầu, xử lý yêu cầu bằng trình xử lý đã kết hợp và in ra kết quả.
Đầu vào sẽ có định dạng như sau:
- Dòng 1: Số lớp middleware
- Các dòng tiếp theo: Mỗi dòng ghi một loại middleware (
timing,validationhoặcuppercase) - Dòng cuối: Yêu cầu cần xử lý
Ví dụ, với đầu vào:
2
timing
validation
GET /usersĐầu ra cần là:
[TIMING] [VALID] Processed: GET /usersVới đầu vào:
3
uppercase
timing
validation
POST /dataĐầu ra cần là:
[TIMING] [VALID] PROCESSED: POST /DATAVới đầu vào:
1
uppercase
hello worldĐầu ra cần là:
PROCESSED: HELLO WORLDVới đầu vào:
0
simple requestĐầu ra cần là:
Processed: simple requestLưu ý thứ tự bọc middleware ảnh hưởng đến đầu ra như thế nào: middleware được áp dụng đầu tiên (ở lớp ngoài cùng) chạy trước và nhận kết quả cuối cùng từ tất cả middleware bên trong. Trong ví dụ thứ hai, middleware viết hoa chạy cuối cùng (ở lớp trong cùng), vì vậy nó chuyển phản hồi cơ sở thành chữ hoa trước khi các tiền tố đo thời gian và xác thực được thêm vào!
Tự làm thử
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Đọc số lớp middleware
nLine, _ := reader.ReadString('\n')
n, _ := strconv.Atoi(strings.TrimSpace(nLine))
// Đọc các loại middleware
middlewareTypes := make([]string, n)
for i := 0; i < n; i++ {
line, _ := reader.ReadString('\n')
middlewareTypes[i] = strings.TrimSpace(line)
}
// Đọc yêu cầu
request, _ := reader.ReadString('\n')
request = strings.TrimSpace(request)
// TODO: Bắt đầu với BaseHandler
// TODO: Tạo pipeline bằng cách bọc handler bằng middleware
// Áp dụng middleware theo thứ tự: middleware đọc đầu tiên sẽ là lớp bọc ngoài cùng
// Gợi ý: Duyệt middlewareTypes theo thứ tự NGƯỢC lại để làm được điều này
// Middleware áp dụng cuối cùng sẽ nằm ngoài cùng (chạy đầu tiên)
// TODO: Xử lý yêu cầu qua handler đã kết hợp và in ra kết quả
fmt.Println("TODO: implement pipeline")
}
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Lập trình hướng đối tượng
1Nền tảng OOP trong Go
Tệp bên ngoàiWorkspace và module trong GoPackage và importTên được export và không được exportGiới thiệu về OOP trong GoStruct như lớpĐịnh nghĩa phương thức cho structReceiver kiểu con trỏ và kiểu giá trịKhởi tạo structHàm khởi tạoÔn tập: máy tính đơn giản4Interface
Giới thiệu về interfaceTriển khai ngầm địnhInterface như một hợp đồngInterface rỗng (any)Khẳng định kiểuswitch kiểuKết hợp interfaceInterface Stringer và ErrorÔn tập: Máy tính hình học7Tính đóng gói
Trường được xuất và không được xuấtĐóng gói ở cấp packagePhương thức getter và setterẨn thông tin trong GoÔn tập: hồ sơ học sinh10Generics (Go 1.18 trở lên)
Giới thiệu về GenericsTham số kiểuRàng buộc kiểuStruct tổng quátGiải pháp thay thế cho phương thức tổng quátÔn tập: tập hợp tổng quát13Các mẫu thiết kế phần 1
Giới thiệu về mẫu thiết kếMẫu SingletonMẫu FactoryMẫu Abstract FactoryMẫu ObserverMẫu Strategy2Tìm hiểu chuyên sâu về kiểu dữ liệu và struct
Kiểu cơ bản và kiểu tổng hợpĐịnh nghĩa kiểu dữ liệu tùy chỉnhThẻ structStruct ẩn danhStruct lồng nhauGiá trị zero và giá trị mặc địnhÔn tập: sổ liên lạc5Ưu tiên kết hợp hơn kế thừa
Vì sao Go không có kế thừaNhập môn nhúng structTự động nâng phương thứcNhúng nhiều structNhúng và kết tậpChe khuất phương thức được nhúngÔn tập: hệ phân cấp nhân viên8Xử lý lỗi và lập trình hướng đối tượng
Giao diện errorCác kiểu lỗi tùy chỉnhBọc lỗi với fmt.ErrorfLỗi sentinelerrors.Is() và errors.As()Panic, defer và recoverÔn tập: trình phân tích tệp11Thư viện chuẩn và OOP
io.Reader và io.Writersort.Interfaceinterface fmt.Stringerencoding/json với structinterface http.HandlerÔn tập: mô hình REST API14Mẫu thiết kế phần 2
Mẫu CommandMẫu AdapterMẫu DecoratorMẫu Template MethodMẫu StateMẫu CompositeMiddleware dưới dạng Decorator3Con trỏ và bộ nhớ
Cơ bản về con trỏ trong GoCon trỏ đến structTruyền theo giá trị và tham chiếuHàm new()Thu gom rác trong GoÔn tập: xây dựng danh sách liên kết6Đa hình trong Go
Đa hình qua interfaceĐịnh kiểu vịt trong GoQuy tắc thỏa mãn interfaceCác tập hợp đa hìnhTiêm phụ thuộcÔn tập: bộ xử lý thanh toán9Lập trình đồng thời và lập trình hướng đối tượng
Cơ bản về goroutineChannel và giao tiếpChannel có bộ đệm và không có bộ đệmCâu lệnh selectsync.Mutex và sync.RWMutexsync.WaitGroupThiết kế struct an toàn luồngÔn tập: worker poolTự luyện tập: Trình biên dịch Go online