Let's Go میدل‌ور › بازیابی پانیک
قبلی · فهرست · بعدی
فصل ۶.۴.

بازیابی پانیک

در یک برنامه Go ساده، وقتی کد شما پانیک می‌کند، باعث خاتمه فوری برنامه می‌شود.

اما برنامه وب ما کمی پیچیده‌تر است. سرور HTTP Go فرض می‌کند که تأثیر هر پانیک به گوروتین سرو کردن درخواست HTTP فعال محدود است (یادتان باشد، هر درخواست در گوروتین خودش پردازش می‌شود).

به طور خاص، پس از یک پانیک، سرور ما یک ردیابی پشته را در لاگ خطای سرور ثبت می‌کند (که بعداً در کتاب درباره آن صحبت خواهیم کرد)، پشته را برای گوروتین تأثیرگرفته باز می‌کند (هر تابع deferred را در مسیر فراخوانی می‌کند) و اتصال HTTP زیرین را می‌بندد. اما برنامه را خاتمه نمی‌دهد، بنابراین پانیک داخل handler نباید کل سرور را از کار بیندازد.

اما اگر پانیکی در یکی از handlerهای ما رخ دهد، کاربر چه خواهد دید؟

بیایید نگاهی بیندازیم و یک پانیک عمدی در handler home خود معرفی کنیم.

File: cmd/web/handlers.go
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 خود را باز کنید و کد زیر را اضافه کنید:

File: cmd/web/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)
    })
}

دو جزئیات در مورد این کد وجود دارد که ارزش توضیح دارد:

حالا بیایید این را در فایل routes.go به کار ببریم، به طوری که اولین چیز در زنجیره ما برای اجرا باشد (تا پانیک‌ها را در تمام میدل‌ورها و handlerهای بعدی پوشش دهد).

File: cmd/web/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("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 خود برگردید و پانیک عمدی را از کد حذف کنید.

File: cmd/web/handlers.go
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"))
}