Dependency injection
There’s one more problem with our logging that we need to address. If you open up your handlers.go
file you’ll notice that the home
handler function is still writing error messages using Go’s standard logger, not the errorLog
logger that we want to be using.
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 error logger. http.Error(w, "Internal Server Error", 500) return } err = ts.ExecuteTemplate(w, "base", nil) if err != nil { log.Print(err.Error()) // This isn't using our new error logger. http.Error(w, "Internal Server Error", 500) } }
This raises a good question: how can we make our new errorLog
logger available to our home
function from main()
?
And this question generalizes further. Most web applications will have multiple dependencies that their handlers need to access, such as a database connection pool, centralized error handlers, and template caches. What we really want to answer is: how can we make any dependency available to our handlers?
There are a few different ways to do this, the simplest being to just put the dependencies in global variables. But in general, it is good practice to inject dependencies into your handlers. It makes your code more explicit, less error-prone and easier to unit test than if you use global variables.
For applications where all your handlers are in the same package, like ours, a neat way to inject dependencies is to put them into a custom application
struct, and then define your handler functions as methods against application
.
I’ll demonstrate.
Open your main.go
file and create a new application
struct like so:
package main import ( "flag" "log" "net/http" "os" ) // Define an application struct to hold the application-wide dependencies for the // web application. For now we'll only include fields for the two custom loggers, but // we'll add more to it as the build progresses. type application struct { errorLog *log.Logger infoLog *log.Logger } func main() { ... }
And then in the handlers.go
file update your handler functions so that they become methods against the application
struct…
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) { if r.URL.Path != "/" { http.NotFound(w, r) return } 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 function is now a method against application // it can access its fields, including the error logger. We'll write the log // message to this instead of the standard logger. app.errorLog.Print(err.Error()) http.Error(w, "Internal Server Error", 500) return } err = ts.ExecuteTemplate(w, "base", nil) if err != nil { // Also update the code here to use the error logger from the application // struct. app.errorLog.Print(err.Error()) http.Error(w, "Internal Server Error", 500) } } // 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.URL.Query().Get("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) { if r.Method != http.MethodPost { w.Header().Set("Allow", http.MethodPost) http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) return } w.Write([]byte("Create a new snippet...")) }
And finally let’s wire things together in our main.go
file:
package main import ( "flag" "log" "net/http" "os" ) type application struct { errorLog *log.Logger infoLog *log.Logger } func main() { addr := flag.String("addr", ":4000", "HTTP network address") flag.Parse() infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime) errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile) // Initialize a new instance of our application struct, containing the // dependencies. app := &application{ errorLog: errorLog, infoLog: infoLog, } // Swap the route declarations to use the application struct's methods as the // handler functions. 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) srv := &http.Server{ Addr: *addr, ErrorLog: errorLog, Handler: mux, } infoLog.Printf("Starting server on %s", *addr) err := srv.ListenAndServe() errorLog.Fatal(err) }
I understand that this approach might feel a bit complicated and convoluted, especially when an alternative is to simply make the infoLog
and errorLog
loggers global variables. But stick with me. As the application grows, and our handlers start to need more dependencies, this pattern will begin to show its worth.
Adding a deliberate error
Let’s try this out by quickly adding a deliberate error to our application.
Open your terminal and rename the ui/html/pages/home.tmpl
to ui/html/pages/home.bak
. When we run our application and make a request for the home page, this now should result in an error because the ui/html/pages/home.tmpl
no longer exists.
Go ahead and make the change:
$ cd $HOME/code/snippetbox $ mv ui/html/pages/home.tmpl ui/html/pages/home.bak
Then run the application and make a request to http://localhost:4000
. You should get an Internal Server Error
HTTP response in your browser, and see a corresponding error message in your terminal similar to this:
$ go run ./cmd/web INFO 2022/01/29 16:12:36 Starting server on :4000 ERROR 2022/01/29 16:12:40 handlers.go:29: open ./ui/html/pages/home.tmpl: no such file or directory
Notice how the log message is now prefixed with ERROR
and originated from line 25
of the handlers.go
file? This demonstrates nicely that our custom errorLog
logger is being passed through to our home
handler as a dependency, and is working as expected.
Leave the deliberate error in place for now; we’ll need it again in the next chapter.
Additional information
Closures for dependency injection
The pattern that we’re using to inject dependencies won’t work if your handlers are spread across multiple packages. In that case, an alternative approach is to create a config
package exporting an Application
struct and have your handler functions close over this to form a closure. Very roughly:
func main() { app := &config.Application{ ErrorLog: log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile) } mux.Handle("/", examplePackage.ExampleHandler(app)) }
func ExampleHandler(app *config.Application) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ... ts, err := template.ParseFiles(files...) if err != nil { app.ErrorLog.Print(err.Error()) http.Error(w, "Internal Server Error", 500) return } ... } }
You can find a complete and more concrete example of how to use the closure pattern in this Gist.