تزریق وابستگی
اگر فایل handlers.go خود را باز کنید، متوجه میشوید که تابع handler home همچنان پیامهای خطا را با استفاده از لاگر استاندارد Go مینویسد، نه لاگر ساختاریافته که اکنون میخواهیم از آن استفاده کنیم.
func home(w http.ResponseWriter, r *http.Request) { ... ts, err := template.ParseFiles(files...) if err != nil { log.Print(err.Error()) // This isn't using our new structured logger. http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } err = ts.ExecuteTemplate(w, "base", nil) if err != nil { log.Print(err.Error()) // This isn't using our new structured logger. http.Error(w, "Internal Server Error", http.StatusInternalServerError) } }
این یک سوال خوب مطرح میکند: چگونه میتوانیم لاگر ساختاریافته جدید خود را از main() در دسترس تابع home قرار دهیم؟
و این سوال بیشتر تعمیم مییابد. بیشتر برنامههای وب وابستگیهای متعددی دارند که handlerهای آنها نیاز به دسترسی دارند، مانند یک استخر اتصال پایگاه داده، handlerهای خطای متمرکز و کشهای قالب. آنچه واقعاً میخواهیم پاسخ دهیم این است: چگونه میتوانیم هر وابستگی (dependency) را در دسترس handlerهای خود قرار دهیم؟
چند روش مختلف برای انجام این کار وجود دارد، سادهترین آنها این است که وابستگیها را در متغیرهای سراسری قرار دهیم. اما به طور کلی، بهترین روش تزریق وابستگیها به handlerهای شما است. این کد شما را صریحتر، کمتر مستعد خطا و آسانتر برای تست واحد میکند تا استفاده از متغیرهای سراسری.
برای برنامههایی که همه handlerهای شما در همان پکیج هستند، مانند ما، یک روش مرتب برای تزریق وابستگیها این است که آنها را در یک ساختار application سفارشی قرار دهیم و سپس توابع handler خود را به عنوان متدهای روی application تعریف کنیم.
من نشان خواهم داد.
ابتدا فایل main.go خود را باز کنید و یک ساختار (struct) application جدید مانند این ایجاد کنید:
package main import ( "flag" "log/slog" "net/http" "os" ) // Define an application struct to hold the application-wide dependencies for the // web application. For now we'll only include the structured logger, but we'll // add more to this as the build progresses. type application struct { logger *slog.Logger } func main() { ... }
و سپس در فایل handlers.go، میخواهیم توابع handler را بهروزرسانی کنیم تا به متدهای روی ساختار application تبدیل شوند و از لاگر ساختاریافته که شامل میشود استفاده کنند.
package main import ( "fmt" "html/template" "net/http" "strconv" ) // Change the signature of the home handler so it is defined as a method against // *application. func (app *application) home(w http.ResponseWriter, r *http.Request) { w.Header().Add("Server", "Go") files := []string{ "./ui/html/base.tmpl", "./ui/html/partials/nav.tmpl", "./ui/html/pages/home.tmpl", } ts, err := template.ParseFiles(files...) if err != nil { // Because the home handler is now a method against the application // struct it can access its fields, including the structured logger. We'll // use this to create a log entry at Error level containing the error // message, also including the request method and URI as attributes to // assist with debugging. app.logger.Error(err.Error(), "method", r.Method, "uri", r.URL.RequestURI()) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } err = ts.ExecuteTemplate(w, "base", nil) if err != nil { // And we also need to update the code here to use the structured logger // too. app.logger.Error(err.Error(), "method", r.Method, "uri", r.URL.RequestURI()) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } // Change the signature of the snippetView handler so it is defined as a method // against *application. func (app *application) snippetView(w http.ResponseWriter, r *http.Request) { id, err := strconv.Atoi(r.PathValue("id")) if err != nil || id < 1 { http.NotFound(w, r) return } fmt.Fprintf(w, "Display a specific snippet with ID %d...", id) } // Change the signature of the snippetCreate handler so it is defined as a method // against *application. func (app *application) snippetCreate(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Display a form for creating a new snippet...")) } // Change the signature of the snippetCreatePost handler so it is defined as a method // against *application. func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) w.Write([]byte("Save a new snippet...")) }
و در نهایت بیایید همه چیز را در فایل main.go خود به هم متصل کنیم:
package main import ( "flag" "log/slog" "net/http" "os" ) type application struct { logger *slog.Logger } func main() { addr := flag.String("addr", ":4000", "HTTP network address") flag.Parse() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) // Initialize a new instance of our application struct, containing the // dependencies (for now, just the structured logger). app := &application{ logger: logger, } mux := http.NewServeMux() fileServer := http.FileServer(http.Dir("./ui/static/")) mux.Handle("GET /static/", http.StripPrefix("/static", fileServer)) // Swap the route declarations to use the application struct's methods as the // handler functions. 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) logger.Info("starting server", "addr", *addr) err := http.ListenAndServe(*addr, mux) logger.Error(err.Error()) os.Exit(1) }
من میفهمم که این رویکرد ممکن است کمی پیچیده به نظر برسد، به ویژه زمانی که یک جایگزین این است که به سادگی logger را یک متغیر سراسری کنیم. اما با من بمانید. با رشد برنامه و نیاز handlerهای ما به وابستگیهای بیشتر، این الگو شروع به نشان دادن ارزش خود میکند.
افزودن یک خطای عمدی
بیایید این را با افزودن سریع یک خطای عمدی به برنامه خود امتحان کنیم.
ترمینال خود را باز کنید و ui/html/pages/home.tmpl را به ui/html/pages/home.bak تغییر نام دهید. وقتی برنامه خود را اجرا میکنیم و درخواستی برای صفحه اصلی میدهیم، این باید منجر به خطا شود زیرا فایل ui/html/pages/home.tmpl دیگر وجود ندارد.
ادامه دهید و تغییر را انجام دهید:
$ cd $HOME/code/snippetbox $ mv ui/html/pages/home.tmpl ui/html/pages/home.bak
سپس برنامه را اجرا کنید و درخواستی به http://localhost:4000 بدهید. باید یک پاسخ HTTP Internal Server Error در مرورگر خود دریافت کنید و یک ورودی لاگ مربوطه در سطح Error در ترمینال خود ببینید شبیه این:
$ go run ./cmd/web time=2024-03-18T11:29:23.000+00:00 level=INFO msg="starting server" addr=:4000 time=2024-03-18T11:29:23.000+00:00 level=ERROR msg="open ./ui/html/pages/home.tmpl: no such file or directory" method=GET uri=/
این به خوبی نشان میدهد که لاگر ساختاریافته logger ما اکنون به عنوان یک وابستگی به handler home ما ارسال میشود و همانطور که انتظار میرود کار میکند.
خطای عمدی را فعلاً در جای خود بگذارید؛ در فصل بعد دوباره به آن نیاز خواهیم داشت.
اطلاعات اضافی
بستارها برای تزریق وابستگی
الگویی که برای تزریق وابستگیها استفاده میکنیم، اگر handlerهای شما در چندین پکیج پخش شده باشند کار نمیکند. در آن صورت، یک رویکرد جایگزین ایجاد یک پکیج مستقل config است که یک ساختار Application را صادر میکند و توابع handler شما روی این بسته میشوند تا یک بستار تشکیل دهند. به طور تقریبی:
// package config type Application struct { Logger *slog.Logger }
// package foo func ExampleHandler(app *config.Application) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ... ts, err := template.ParseFiles(files...) if err != nil { app.Logger.Error(err.Error(), "method", r.Method, "uri", r.URL.RequestURI()) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } ... } }
// package main func main() { app := &config.Application{ Logger: slog.New(slog.NewTextHandler(os.Stdout, nil)), } ... mux.Handle("/", foo.ExampleHandler(app)) ... }
میتوانید یک مثال کامل و ملموستر از نحوه استفاده از الگوی بستار را در این Gist پیدا کنید.