بازیابی پنیک (Panic Recovery)
در این بخش، یک میانافزار بازیابی (Recovery Middleware) ایجاد خواهیم کرد که پنیکهای زمان اجرا (Runtime Panics) را مدیریت میکند و آنها را به پاسخ خطای 500 (500 Error Response) تبدیل میکند.
در یک برنامه ساده Go، وقتی کد شما پنیک میکند منجر به خاتمه فوری برنامه میشود.
اما برنامه وب ما کمی پیچیدهتر است. سرور HTTP گو فرض میکند که تأثیر هر پنیک به گوروتین خدمتدهنده به درخواست HTTP فعال محدود میشود (به یاد داشته باشید، هر درخواست در گوروتین خودش مدیریت میشود).
به طور خاص، پس از یک پنیک، سرور ما یک ردیابی پشته را در لاگ خطای سرور ثبت میکند (که ما بعداً در کتاب در مورد آن صحبت خواهیم کرد)، پشته را برای گوروتین تحت تأثیر باز میکند (با فراخوانی هر تابع معوق در طول مسیر) و اتصال HTTP زیرین را میبندد. اما برنامه را متوقف نمیکند، بنابراین مهم است که هر پنیک در هندلرهای شما باعث از کار افتادن سرور نمیشود.
اما اگر یک پنیک در یکی از هندلرهای ما رخ دهد، کاربر چه چیزی خواهد دید؟
بیایید نگاهی بیندازیم و یک پنیک عمدی در هندلر home خود ایجاد کنیم.
package main ... func (app *application) home(w http.ResponseWriter, r *http.Request) { snippets, err := app.snippets.Latest() if err != nil { app.serverError(w, r, err) return } data := app.newTemplateData(r) data.Snippets = snippets app.render(w, r, http.StatusOK, "home.tmpl", data) } ...
برنامه خود را مجدداً راهاندازی کنید...
$ go run ./cmd/web time=2024-03-18T11:29:23.000+00:00 level=INFO msg="starting server" addr=:4000
... و یک درخواست HTTP برای صفحه اصلی از یک پنجره ترمینال دوم ارسال کنید:
$ curl -i http://localhost:4000 curl: (52) Empty reply from server
متأسفانه، به دلیل بسته شدن اتصال HTTP زیرین پس از پنیک توسط Go، تنها چیزی که دریافت میکنیم یک پاسخ خالی است.
این تجربه خوبی برای کاربر نیست. ارسال یک پاسخ HTTP مناسب با وضعیت 500 Internal Server Error مناسبتر و معنادارتر خواهد بود.
یک روش تمیز برای انجام این کار، ایجاد میانافزاری است که پنیک را بازیابی میکند و متد کمکی app.serverError() ما را فراخوانی میکند. برای انجام این کار، میتوانیم از این واقعیت استفاده کنیم که توابع معوق همیشه هنگام باز شدن پشته پس از یک پنیک فراخوانی میشوند.
فایل middleware.go خود را باز کنید و کد زیر را اضافه کنید:
package main import ( "fmt" "net/http" ) func (app *application) recoverPanic(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Create a deferred function (which will always be run in the event // of a panic as Go unwinds the stack). defer func() { // Use the builtin recover function to check if there has been a // panic or not. if err := recover(); err != nil { // If there was a panic, set a "Connection: close" header on the // response. This acts as a trigger to make Go's HTTP server // automatically close the current connection after a response has been // sent. w.Header().Set("Connection", "close") // The value returned by recover() has the type any, so we use // fmt.Errorf() to normalize it into an error and log it using our // custom Logger type. err = fmt.Errorf("%v", err) app.logger.Error(err.Error()) app.serverError(w, r, err) } }() next.ServeHTTP(w, r) }) }
دو نکته در مورد این کد وجود دارد که ارزش توضیح دارند:
تنظیم هدر
Connection: Closeدر پاسخ به عنوان یک محرک عمل میکند تا سرور HTTP گو به طور خودکار اتصال فعلی را پس از ارسال پاسخ ببندد. همچنین به کاربر اطلاع میدهد که اتصال بسته خواهد شد. نکته: اگر پروتکل مورد استفاده HTTP/2 باشد، گو به طور خودکار هدرConnection: Closeرا از پاسخ حذف میکند (تا نادرست نباشد) و یک فریمGOAWAYارسال میکند.مقدار برگردانده شده توسط تابع داخلی
recover()دارای نوعanyاست، و نوع زیرین آن میتواندstring،error، یا چیز دیگری باشد — هر پارامتری که بهpanic()ارسال شده است. در مورد ما، این رشته"oops! something went wrong"است. در کد بالا، ما این را با استفاده از تابعfmt.Errorf()برای ایجاد یک شیءerrorجدید حاوی نمایش متنی پیشفرض مقدارanyنرمال میکنیم، و سپس اینerrorرا به متد کمکیapp.serverError()ارسال میکنیم.
حالا بیایید این را در فایل routes.go به کار ببریم، به طوری که اولین چیز در زنجیره ما برای اجرا باشد (تا پنیکها را در تمام میانافزارها و هندلرهای بعدی پوشش دهد).
package main import "net/http" func (app *application) routes() http.Handler { mux := http.NewServeMux() fileServer := http.FileServer(http.Dir("./ui/static/")) mux.Handle("/static/", http.StripPrefix("/static", fileServer)) mux.HandleFunc("/", app.home) mux.HandleFunc("/snippet/view/", app.snippetView) mux.HandleFunc("/snippet/create", app.snippetCreate) return app.recoverPanic(app.logRequest(app.commonHeaders(mux))) }
اگر برنامه را مجدداً راهاندازی کنید و اکنون درخواستی برای صفحه اصلی ارسال کنید، باید یک پاسخ 500 Internal Server Error به خوبی شکل گرفته را پس از پنیک ببینید، از جمله هدر Connection: close که در موردش صحبت کردیم.
$ go run ./cmd/web time=2024-03-18T11:29:23.000+00:00 level=INFO msg="starting server" addr=:4000
$ curl -i http://localhost:4000 HTTP/1.1 500 Internal Server Error Connection: close Content-Security-Policy: default-src 'self'; style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com Content-Type: text/plain; charset=utf-8 Referrer-Policy: origin-when-cross-origin Server: Go X-Content-Type-Options: nosniff X-Frame-Options: deny X-Xss-Protection: 0 Date: Wed, 18 Mar 2024 11:29:23 GMT Content-Length: 22 Internal Server Error
قبل از ادامه، به هندلر home خود برگردید و پنیک عمدی را از کد حذف کنید.
اطلاعات تکمیلی
بازیابی پنیک در گوروتینهای پسزمینه
مهم است که متوجه شوید میانافزار ما فقط پنیکهایی را که در همان گوروتینی که میانافزار recoverPanic() را اجرا کرده رخ میدهند، بازیابی میکند.
به عنوان مثال، اگر هندلری دارید که یک گوروتین دیگر راهاندازی میکند (مثلاً برای انجام برخی پردازشهای پسزمینه)، هر پنیکی که در گوروتین دوم رخ دهد بازیابی نخواهد شد — نه توسط میانافزار recoverPanic()... و نه توسط بازیابی پنیک داخلی سرور HTTP گو. آنها باعث خروج برنامه شما و از کار افتادن سرور خواهند شد.
بنابراین، اگر گوروتینهای اضافی را از درون برنامه وب خود راهاندازی میکنید و احتمال پنیک وجود دارد، باید مطمئن شوید که هر پنیکی را از درون آنها نیز بازیابی میکنید. به عنوان مثال:
func (app *application) myHandler(w http.ResponseWriter, r *http.Request) { ... // Spin up a new goroutine to do some background processing. go func() { defer func() { if err := recover(); err != nil { app.logger.Error(fmt.Sprint(err)) } }() doSomeBackgroundProcessing() }() w.Write([]byte("OK")) }
واژهنامه اصطلاحات فنی
| اصطلاح فارسی | معادل انگلیسی | توضیح |
|---|---|---|
| میانافزار بازیابی | Recovery Middleware | میانافزاری که پنیکها را مدیریت میکند |
| پنیکهای زمان اجرا | Runtime Panics | خطاهای غیرمنتظره در زمان اجرای برنامه |
| پاسخ خطای 500 | 500 Error Response | پاسخ خطای سرور داخلی |
| زنجیره میانافزار | Middleware Chain | ترکیب چندین میانافزار به صورت پشت سر هم |
| تابع معوق | Deferred Function | تابعی که در پایان اجرای بلوک فراخوانی میشود |
| بازیابی | Recover | تابع داخلی Go برای مدیریت پنیکها |
| هدر اتصال | Connection Header | هدر HTTP برای کنترل وضعیت اتصال |
| ثبت خطا | Error Logging | ذخیره اطلاعات خطاها در گزارشها |
| مدیریت خطا | Error Handling | نحوه برخورد با خطاها در برنامه |
| خطای سرور | Server Error | خطایی که در سمت سرور رخ میدهد |