Error handling
Gin handleri hech narsa qaytarmaydi: func(c *gin.Context). Shu sababli xato yuz berganda javobni ko‘pincha o‘sha joyning o‘zida yozish vasvasasi paydo bo‘ladi:
book, err := svc.Get(ctx, id)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()}) // ichki xato matni klientga ketdi!
return
}
Bunday kod o‘nlab handlerda takrorlanadi. Har birida format bir oz farq qiladi, ba’zilarida esa ichki xato matni to‘g‘ridan-to‘g‘ri klientga yuboriladi.
Request timing va error handler middleware darsida net/http uchun bu muammo HandlerE turi bilan hal qilingan edi: handler xatoni qaytaradi, uni esa bitta joy qayta ishlaydi. Gin’da buning uchun tayyor mexanizm bor: c.Error() va c.Errors.
c.Error va c.Errors
c.Error(err) xatoni joriy Contextning xatolar ro‘yxatiga qo‘shadi. U javob yozmaydi va zanjirni to‘xtatmaydi:
func getBook(c *gin.Context) {
book, err := svc.Get(c.Request.Context(), id)
if err != nil {
c.Error(err) // xatoni ro‘yxatga qo‘shish
return // handlerdan chiqish
}
c.JSON(http.StatusOK, book)
}
Xatolar ro‘yxati c.Errors maydonida saqlanadi. Zanjirdagi keyingi middleware’lar uni c.Next()dan keyin o‘qiy oladi. Aynan shu imkoniyat xato middleware’ining asosini tashkil qiladi.
Xato turi: AppError
Klientga ko‘rsatiladigan xatolar uchun alohida tur yaratamiz. Bu Xatoliklarni standart formatda qaytarish darsidagi yondashuvning Gin’dagi ko‘rinishi:
type AppError struct {
Status int
Code string
Message string
Fields map[string]string
Err error // ichki sabab, klientga ko‘rsatilmaydi
}
func (e *AppError) Error() string {
if e.Err != nil {
return e.Code + ": " + e.Err.Error()
}
return e.Code
}
func (e *AppError) Unwrap() error { return e.Err }
func NotFound(what string) *AppError {
return &AppError{Status: http.StatusNotFound, Code: "not_found", Message: what + " topilmadi"}
}
func Forbidden() *AppError {
return &AppError{Status: http.StatusForbidden, Code: "forbidden", Message: "bu amal uchun ruxsat yo‘q"}
}
Xato middleware’i
func ErrorHandler(logger *slog.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
c.Next() // avval handlerlar ishlaydi
if len(c.Errors) == 0 || c.Writer.Written() {
return // xato yo‘q yoki javob allaqachon yozilgan
}
err := c.Errors.Last().Err
var appErr *AppError
if !errors.As(err, &appErr) {
appErr = &AppError{
Status: http.StatusInternalServerError,
Code: "internal",
Message: "Ichki server xatosi",
Err: err,
}
}
if appErr.Status >= 500 {
logger.Error("server xatosi", "err", err, "path", c.Request.URL.Path)
}
body := gin.H{"code": appErr.Code, "message": appErr.Message}
if len(appErr.Fields) > 0 {
body["fields"] = appErr.Fields
}
c.JSON(appErr.Status, gin.H{"error": body})
}
}
Middleware qanday ishlaydi:
c.Next()barcha keyingi handlerlarni bajaradi.- Keyin
c.Errorstekshiriladi. Xato bo‘lmasa yoki handler javobni allaqachon yozgan bo‘lsa, hech narsa qilinmaydi. - Oxirgi xato olinadi va
*AppErrorbo‘lsa, uning status kodi va xabari ishlatiladi. - Boshqa har qanday xato
500ga aylantiriladi, to‘liq matni esa faqat logga yoziladi.
Bu middleware zanjirda handlerlardan oldin ro‘yxatdan o‘tkazilishi kerak. Shunda uning c.Next()dan keyingi qismi handlerlar ishini tugatgach bajariladi.
c.Writer.Written() tekshiruvi muhim. Handler xatoni qo‘shib, javobni ham yozgan bo‘lishi mumkin. Bunday holatda ikkinchi marta javob yozishga urinilmaydi.
Binding xatolarini ham shu yo‘ldan o‘tkazish
Request binding darsidagi validatsiya xatolarini ham AppErrorga aylantirib, c.Error orqali qaytarish mumkin:
func bindErr(err error) *AppError {
var verrs validator.ValidationErrors
if errors.As(err, &verrs) {
fields := make(map[string]string, len(verrs))
for _, fe := range verrs {
fields[fe.Field()] = fe.Tag()
}
return &AppError{Status: http.StatusUnprocessableEntity, Code: "validation_failed",
Message: "Ma’lumotlar noto‘g‘ri", Fields: fields}
}
return &AppError{Status: http.StatusBadRequest, Code: "bad_request", Message: "So‘rov formati noto‘g‘ri"}
}
func createBook(c *gin.Context) {
var req CreateBookRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.Error(bindErr(err))
return
}
// ...
}
Endi barcha xatolar — binding, domen va ichki xatolar — bitta middleware orqali bir xil formatda qaytadi.
Domen xatolarini AppErrorga moslash
Service qatlami AppError haqida bilmasligi kerak. U ErrNotFound, ErrForbidden kabi domen xatolarini qaytaradi. Ularni HTTP xatolariga moslashtirish handlerda yoki alohida funksiyada bajariladi:
func mapDomain(err error) error {
switch {
case errors.Is(err, book.ErrNotFound):
return NotFound("kitob")
case errors.Is(err, book.ErrForbidden):
return Forbidden()
default:
return err // noma’lum — middleware 500 qiladi
}
}
book, err := svc.Get(ctx, id)
if err != nil {
c.Error(mapDomain(err))
return
}
panic uchun: CustomRecovery
gin.Recovery() panicni ushlaydi va 500 qaytaradi, lekin tanasiz. JSON javob kerak bo‘lsa, gin.CustomRecovery ishlatiladi:
r.Use(gin.CustomRecovery(func(c *gin.Context, rec any) {
logger.Error("panic", "err", rec, "path", c.Request.URL.Path)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": gin.H{"code": "internal", "message": "Ichki server xatosi"},
})
}))
CustomRecovery stek izini standart Gin loggeriga o‘zi yozadi. Funksiya ichida esa klientga javob yoziladi va xato o‘z loggerimizga yuboriladi.
404 va 405 javoblari
Gin standart holatda mavjud bo‘lmagan marshrut uchun 404 page not found matnini qaytaradi. JSON javob uchun NoRoute va NoMethod ishlatiladi:
r.HandleMethodNotAllowed = true
r.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{"error": gin.H{"code": "not_found", "message": "endpoint topilmadi"}})
})
r.NoMethod(func(c *gin.Context) {
c.JSON(http.StatusMethodNotAllowed, gin.H{"error": gin.H{"code": "method_not_allowed", "message": "metod ruxsat etilmagan"}})
})
NoMethod faqat HandleMethodNotAllowed = true bo‘lganda ishlaydi. Gin arxitekturasi darsida aytilganidek, bu sozlama standart holatda o‘chiq.
To‘liq misol
| main.go | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | |
Sinab ko‘ramiz:
curl localhost:8080/books/1
curl localhost:8080/books/abc
curl localhost:8080/books/2
curl localhost:8080/books/3
curl localhost:8080/books/9
curl -X POST localhost:8080/books -d '{"year":5}'
curl localhost:8080/crash
curl localhost:8080/authors
curl -X DELETE localhost:8080/books/1
{"data":{"id":1,"title":"O‘tkan kunlar"}}
{"error":{"code":"bad_request","message":"id son bo‘lishi kerak"}}
{"error":{"code":"forbidden","message":"bu amal uchun ruxsat yo‘q"}}
{"error":{"code":"internal","message":"Ichki server xatosi"}}
{"error":{"code":"not_found","message":"kitob topilmadi"}}
{"error":{"code":"validation_failed","fields":{"Title":"required","Year":"gte"},"message":"Ma’lumotlar noto‘g‘ri"}}
{"error":{"code":"internal","message":"Ichki server xatosi"}}
{"error":{"code":"not_found","message":"endpoint topilmadi"}}
{"error":{"code":"method_not_allowed","message":"metod ruxsat etilmagan"}}
Server logida faqat 500 xatolari bor. Ular orasida CustomRecovery yozgan stek izi ham chiqadi, bu yerda u tushirib qoldirilgan:
{"level":"ERROR","msg":"server xatosi","err":"kitobni o‘qish: pq: connection refused","path":"/books/3"}
{"level":"ERROR","msg":"panic","err":"assignment to entry in nil map","path":"/crash"}
- Barcha javoblar bir xil
{"error": {...}}formatida. - Database xatosi klientga faqat umumiy xabar sifatida yetib bordi. To‘liq matni esa logda saqlandi.
panicushlandi va server ishlashda davom etdi.- Validatsiya xatosidagi maydon nomlari Go nomlari ko‘rinishida chiqdi. Ularni JSON nomlariga aylantirish uchun Request binding darsidagi
RegisterTagNameFuncqo‘shiladi.
Xulosa
- Handler xatoni
c.Error(err)bilan qo‘shadi vareturnqiladi. Javobni esa xato middleware’i yozadi. - Xato middleware’i
c.Next()dan keyinc.Errorsni tekshiradi va uni zanjir boshida ro‘yxatdan o‘tkazish kerak. *AppErrorklientga ko‘rsatiladi, qolgan xatolar500sifatida yashiriladi va logga yoziladi.- Domen xatolari HTTP xatolariga handler qatlamida moslashtiriladi.
gin.CustomRecoverypanicuchun JSON javob qaytaradi.NoRoutevaNoMethod404hamda405javoblarini yagona formatga keltiradi.NoMethoduchunHandleMethodNotAllowed = truekerak.
Keyingi darsda Gin ilovasini production uchun sozlash va tezligini o‘lchashni ko‘ramiz.