بازیابی پانیک
در یک برنامه Go ساده، وقتی کد شما پانیک میکند، باعث خاتمه فوری برنامه میشود.
اما برنامه وب ما کمی پیچیدهتر است. سرور HTTP Go فرض میکند که تأثیر هر پانیک به گوروتین سرو کردن درخواست HTTP فعال محدود است (یادتان باشد، هر درخواست در گوروتین خودش پردازش میشود).
به طور خاص، پس از یک پانیک، سرور ما یک ردیابی پشته را در لاگ خطای سرور ثبت میکند (که بعداً در کتاب درباره آن صحبت خواهیم کرد)، پشته را برای گوروتین تأثیرگرفته باز میکند (هر تابع deferred را در مسیر فراخوانی میکند) و اتصال HTTP زیرین را میبندد. اما برنامه را خاتمه نمیدهد، بنابراین پانیک داخل handler نباید کل سرور را از کار بیندازد.
اما اگر پانیکی در یکی از handlerهای ما رخ دهد، کاربر چه خواهد دید؟
بیایید نگاهی بیندازیم و یک پانیک عمدی در handler home خود معرفی کنیم.
package main ... func (app *application) home(w http.ResponseWriter, r *http.Request) { panic("oops! something went wrong") // Deliberate panic 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() ما را فراخوانی میکند. برای انجام این کار، میتوانیم از این واقعیت استفاده کنیم که توابع deferred همیشه هنگام باز شدن پشته پس از یک پانیک فراخوانی میشوند.
فایل middleware.go خود را باز کنید و کد زیر را اضافه کنید:
package main import ( "fmt" // New import "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 there has... if err := recover(); err != nil { // Set a "Connection: close" header on the response. w.Header().Set("Connection", "close") // Call the app.serverError helper method to return a 500 // Internal Server response. app.serverError(w, r, fmt.Errorf("%s", err)) } }() next.ServeHTTP(w, r) }) }
دو جزئیات در مورد این کد وجود دارد که ارزش توضیح دارد:
تنظیم هدر
Connection: Closeروی پاسخ به عنوان یک محرک عمل میکند تا سرور HTTP Go به طور خودکار اتصال فعلی را پس از ارسال پاسخ ببندد. همچنین به کاربر اطلاع میدهد که اتصال بسته خواهد شد. توجه: اگر پروتکل مورد استفاده HTTP/2 باشد، Go به طور خودکار هدرConnection: Closeرا از پاسخ حذف میکند (تا ناقص نباشد) و یک فریمGOAWAYارسال میکند.مقدار برگردانده شده توسط تابع داخلی
recover()نوعanyدارد و نوع زیرین آن میتواندstring،errorیا چیز دیگری باشد — هر چیزی که بهpanic()به عنوان پارامتر داده شده باشد. در مورد ما، رشته"oops! something went wrong"است. در کد بالا، این را با استفاده از تابعfmt.Errorf()برای ایجاد یک شیءerrorجدید حاوی نمایش متنی پیشفرض مقدارany، به یکerrorنرمال میکنیم و سپس اینerrorرا به متد کمکیapp.serverError()میدهیم.
حالا بیایید این را در فایل routes.go به کار ببریم، به طوری که اولین چیز در زنجیره ما برای اجرا باشد (تا پانیکها را در تمام میدلورها و handlerهای بعدی پوشش دهد).
package main import "net/http" func (app *application) routes() http.Handler { mux := http.NewServeMux() fileServer := http.FileServer(http.Dir("./ui/static/")) mux.Handle("GET /static/", http.StripPrefix("/static", fileServer)) mux.HandleFunc("GET /{$}", app.home) mux.HandleFunc("GET /snippet/view/{id}", app.snippetView) mux.HandleFunc("GET /snippet/create", app.snippetCreate) mux.HandleFunc("POST /snippet/create", app.snippetCreatePost) // Wrap the existing chain with the recoverPanic middleware. return app.recoverPanic(app.logRequest(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
قبل از ادامه، به handler 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) } ...
اطلاعات اضافی
بازیابی پانیک در گوروتینهای پسزمینه
مهم است بدانید که میدلور ما فقط پانیکهایی را بازیابی میکند که در همان گوروتین که میدلور recoverPanic() را اجرا کرده است رخ میدهند.
اگر، به عنوان مثال، یک handler دارید که گوروتین دیگری را راهاندازی میکند (مثلاً برای انجام برخی پردازشهای پسزمینه)، آنگاه هر پانیکی که در گوروتین دوم رخ دهد بازیابی نخواهد شد — نه توسط میدلور recoverPanic()… و نه توسط بازیابی پانیک داخلی سرور HTTP Go. آنها باعث خروج برنامه و پایین آمدن سرور میشوند.
بنابراین، اگر از داخل برنامه وب خود گوروتینهای اضافی راهاندازی میکنید و احتمال پانیک وجود دارد، باید مطمئن شوید که پانیکها را از داخل آنها نیز بازیابی میکنید. به عنوان مثال:
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")) }