ZhangZhihui's Blog  

Web application basics

  • The first thing we need is a handler. If you’ve previously built web applications using a MVC pattern, you can think of handlers as being a bit like controllers. They’re responsible for executing your application logic and for writing HTTP response headers and bodies.
  • The second component is a router (or servemux in Go terminology). This stores a mapping between the URL routing patterns for your application and the corresponding handlers. Usually you have one servemux for your application containing all your routes.
  • The last thing we need is a web server. One of the great things about Go is that you can establish a web server and listen for incoming requests as part of your application itself. You don’t need an external third-party server like Nginx, Apache or Caddy.

 

Network addresses

The TCP network address that you pass to http.ListenAndServe() should be in the format "host:port" . If you omit the host (like we did with ":4000" ) then the server will listen on allyour computer’s available network interfaces. Generally, you only need to specify a host in the address if your computer has multiple network interfaces and you want to listen on just one of them.

In other Go projects or documentation you might sometimes see network addresses written using named ports like ":http" or ":http-alt" instead of a number. If you use a named port then the http.ListenAndServe() function will attempt to look up the relevant port number from your /etc/services file when starting the server, returning an error if a match can’t be found.

 

Trailing slashes in route patterns

It’s important to know that Go’s servemux has different matching rules depending on whether a route pattern ends with a trailing slash or not.

Our two new route patterns — "/snippet/view" and "/snippet/create" — don’t end in a trailing slash. When a pattern doesn’t have a trailing slash, it will only be matched (and the corresponding handler called) when the request URL path exactly matches the pattern in full.

When a route pattern ends with a trailing slash — like "/" or "/static/" — it is known as a subtree path pattern. Subtree path patterns are matched (and the corresponding handler called) whenever the start of a request URL path matches the subtree path. If it helps your understanding, you can think of subtree paths as acting a bit like they have a wildcard at the end, like "/**" or "/static/**" .

This helps explain why the "/" route pattern acts like a catch-all. The pattern essentially means match a single slash, followed by anything (or nothing at all) .

 

Restricting subtree paths

To prevent subtree path patterns from acting like they have a wildcard at the end, you can append the special character sequence {$} to the end of the pattern — like "/{$}" or "/static/{$}" .

So if you have the route pattern "/{$}" , it effectively means match a single slash, followed by nothing else. It will only match requests where the URL path is exactly / .

 

Additional servemux features

There are a couple of other servemux features worth pointing out:

  • Request URL paths are automatically sanitized. If the request path contains any . or .. elements or repeated slashes, the user will automatically be redirected to an equivalent clean URL. For example, if a user makes a request to /foo/bar/..//baz they will automatically be sent a 301 Permanent Redirect to /foo/baz instead.
  • If a subtree path has been registered and a request is received for that subtree path without a trailing slash, then the user will automatically be sent a 301 Permanent Redirect to the subtree path with the slash added. For example, if you have registered the subtree path /foo/ , then any request to /foo will be redirected to /foo/ .

 

Host name matching

It’s possible to include host names in your route patterns. This can be useful when you wantto redirect all HTTP requests to a canonical URL, or if your application is acting as the back end for multiple sites or services. For example:

mux := http.NewServeMux()
mux.HandleFunc("foo.example.org/", fooHandler)
mux.HandleFunc("bar.example.org/", barHandler)
mux.HandleFunc("/baz", bazHandler)

When it comes to pattern matching, any host-specific patterns will be checked first and if there is a match the request will be dispatched to the corresponding handler. Only when there isn’t a host-specific match found will the non-host specific patterns also be checked.

 

Wildcard route patterns

Wildcard segments in a route pattern are denoted by an wildcard identifier inside {} brackets. Like this:

mux.HandleFunc("/products/{category}/item/{itemID}", exampleHandler)

In this example, the route pattern contains two wildcard segments. The first segment has the identifier category and the second has the identifier itemID .

The matching rules for route patterns containing wildcard segments are the same as we saw in the previous chapter, with the additional rule that the request path can contain any non-empty value for the wildcard segments. So, for example, the following requests would all match the route we defined above:

/products/hammocks/item/sku123456789
/products/seasonal-plants/item/pdt-1234-wxyz
/products/experimental_foods/item/quantum%20bananas

Important: When defining a route pattern, each path segment (the bit between forward slash characters) can only contain one wildcard and the wildcard needs to fill the whole path segment. Patterns like "/products/c_{category}" , /date/{y}-{m}-{d} or /{slug}.html are not valid.

Inside your handler, you can retrieve the corresponding value for a wildcard segment using its identifier and the r.PathValue() method. For example:

 

func exampleHandler(w http.ResponseWriter, r *http.Request) {
    category := r.PathValue("category")
    itemID := r.PathValue("itemID")
    ...
}

The r.PathValue() method always returns a string value, and it’s important to remember that this can be any value that the user includes in the URL — so you should validate or sanity check the value before doing anything important with it.

 

Precedence and conflicts

When defining route patterns with wildcard segments, it’s possible that some of your patterns will ‘overlap’. For example, if you define routes with the patterns "/post/edit" and "/post/{id}" they overlap because an incoming HTTP request with the path
/post/edit is a valid match for both patterns.

When route patterns overlap, Go’s servemux needs to decide which pattern takes precedent so it can dispatch the request to the appropriate handler. The rule for this is very neat and succinct: the most specific route pattern wins. Formally, Go defines a pattern as more specific than another if it matches only a subset of requests that the other pattern matches.

Continuing with the example above, the route pattern "/post/edit" only matches requests with the exact path /post/edit , whereas the pattern "/post/{id}" matches requests with the path /post/edit , /post/123 , /post/abc and many more. Therefore "/post/edit" is the more specific route pattern and will take precedent.

While we’re on this topic, there are a few other things worth mentioning:

  • A nice side-effect of the most specific pattern wins rule is that you can register patterns in any order and it won’t change how the servemux behaves.
  • There is a potential edge case where you have two overlapping route patterns but neither one is obviously more specific than the other. For example, the patterns "/post/new/{id}" and "/post/{author}/latest" overlap because they both match the request path /post/new/latest , but it’s not clear which one should take precedence. In this scenario, Go’s servemux considers the patterns to conflict, and will panic at runtime when initializing the routes.
  • Just because Go’s servemux supports overlapping routes, it doesn’t mean that you should use them! Having overlapping route patterns increases the risk of bugs and unintended behavior in your application, and if you have the freedom to design the URL structure for your application it’s generally good practice to keep overlaps to a minimum or avoid them completely.

 

Subtree path patterns with wildcards

It’s important to understand that the routing rules we described in the previous chapter still apply, even when you’re using wildcard segments. In particular, if your route pattern ends in a trailing slash and has no {$} at the end, then it is treated as a subtree path pattern and it only requires the start of a request URL path to match.

So, if you have a subtree path pattern like "/user/{id}/" in your routes (note the trailing slash), this pattern will match requests like /user/1/ , /user/2/a , /user/2/a/b/c and so on.

Again, if you don’t want that behavior, stick a {$} at the end — like "/user/{id}/{$}" .

 

Remainder wildcards

Wildcards in route patterns normally match a single, non-empty, segment of the requestpath only. But there is one special case.

If a route pattern ends with a wildcard, and this final wildcard identifier ends in ... , then the wildcard will match any and all remaining segments of a request path.

For example, if you declare a route pattern like "/post/{path...}" it will match requests like /post/a , /post/a/b , /post/a/b/c and so on — very much like a subtree path pattern does. But the difference is that you can access the entire wildcard part via the
r.PathValue() method in your handlers. In this example, you could get the wildcard value for {path...} by calling r.PathValue("path") .

 

Method-based routing

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /{$}", home)
    mux.HandleFunc("GET /snippet/view/{id}", snippetView)
    mux.HandleFunc("GET /snippet/create", snippetCreate)
    // Create the new route, which is restricted to POST requests only.
    mux.HandleFunc("POST /snippet/create", snippetCreatePost)
    log.Print("starting server on :4000")
    err := http.ListenAndServe(":4000", mux)
    log.Fatal(err)
}

Note: The HTTP methods in route patterns are case sensitive and should always be written in uppercase, followed by at least one whitespace character (both spaces and tabs are fine). You can only include one HTTP method in each route pattern.

It’s also worth mentioning that when you register a route pattern which uses the GET method, it will match both GET and HEAD requests. All other methods (like POST , PUT and DELETE ) require an exact match.

Notice that it’s totally OK to declare two (or more) separate routes that have different HTTP methods but otherwise have the same pattern, like we are doing here with "GET /snippet/create" and "POST /snippet/create" .

 

HTTP status codes

  • It’s only possible to call w.WriteHeader() once per response, and after the status code has been written it can’t be changed. If you try to call w.WriteHeader() a second time Go will log a warning message.
  • If you don’t call w.WriteHeader() explicitly, then the first call to w.Write() will automatically send a 200 status code to the user. So, if you want to send a non-200 status code, you must call w.WriteHeader() before any call to w.Write() .

 

Customizing headers

You can also customize the HTTP headers sent to a user by changing the response header map. Probably the most common thing you’ll want to do is include an additional header in the map, which you can do using the w.Header().Add() method.

Important: You must make sure that your response header map contains all the headers you want before you call w.WriteHeader() or w.Write() . Any changes you make to the response header map after calling w.WriteHeader() or w.Write() will have no effect on the headers that the user receives.

 

Writing response bodies

There are a lot of functions besides w.Write() that you can use to write a response! The key thing to understand is this… because the http.ResponseWriter value in your handlers has a Write() method, it satisfies the io.Writer interface.

That means you can use standard library functions like io.WriteString() and the fmt.Fprint*() family (all of which accept an io.Writer parameter) to write plain-text response bodies too.

// Instead of this...
w.Write([]byte("Hello world"))

// You can do this...
io.WriteString(w, "Hello world")
fmt.Fprint(w, "Hello world")

 

Content sniffing

In order to automatically set the Content-Type header, Go content sniffs the response body with the http.DetectContentType() function. If this function can’t guess the content type, Go will fall back to setting the header Content-Type: application/octet-stream instead.

The http.DetectContentType() function generally works quite well, but a common gotcha for web developers is that it can’t distinguish JSON from plain text. So, by default, JSON responses will be sent with a Content-Type: text/plain; charset=utf-8 header. You can prevent this from happening by setting the correct header manually in your handler like so:

w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"name":"Alex"}`))

 

Manipulating the header map

In this chapter we used w.Header().Add() to add a new header to the response header map. But there are also Set() , Del() , Get() and Values() methods that you can use to manipulate and read from the header map too.

// Set a new cache-control header. If an existing "Cache-Control" header exists
// it will be overwritten.
w.Header().Set("Cache-Control", "public, max-age=31536000")

// In contrast, the Add() method appends a new "Cache-Control" header and can
// be called multiple times.
w.Header().Add("Cache-Control", "public")
w.Header().Add("Cache-Control", "max-age=31536000")

// Delete all values for the "Cache-Control" header.
w.Header().Del("Cache-Control")

// Retrieve the first value for the "Cache-Control" header.
w.Header().Get("Cache-Control")

// Retrieve a slice of all values for the "Cache-Control" header.
w.Header().Values("Cache-Control")

 

Header canonicalization

When you’re using the Set() , Add() , Del() , Get() and Values() methods on the header map, the header name will always be canonicalized using the textproto.CanonicalMIMEHeaderKey() function. This converts the first letter and any letterfollowing a hyphen to upper case, and the rest of the letters to lowercase. This has the practical implication that when calling these methods the header name is case-insensitive.

If you need to avoid this canonicalization behavior, you can edit the underlying header map directly. It has the type map[string][]string behind the scenes. For example:

w.Header()["X-XSS-Protection"] = []string{"1; mode=block"}

Note: If a HTTP/2 connection is being used, Go will always automatically convert the header names and values to lowercase for you when writing the response, as per the HTTP/2 specifications.

 

Project structure and organization

  • The cmd directory will contain the application-specific code for the executable applications in the project. For now our project will have just one executable application — the web application — which will live under the cmd/web directory. You could create a CLI application under cmd/cli.
  • The internal directory will contain the ancillary non-application-specific code used in the project. We’ll use it to hold potentially reusable code like validation helpers and the SQL database models for the project.
  • The ui directory will contain the user-interface assets used by the web application. Specifically, the ui/html directory will contain HTML templates, the ui/html/partials directory will contain partial HTML templates, and the ui/static directory will contain static files (like CSS and images).

 

The internal directory

It’s important to point out that the directory name internal carries a special meaning and behavior in Go: any packages which live under this directory can only be imported by code inside the parent of the internal directory. In our case, this means that any packages which live in internal can only be imported by code inside our snippetbox project directory.Or, looking at it the other way, this means that any packages under internal cannot be imported by code outside of our project.

This is useful because it prevents other codebases from importing and relying on the (potentially unversioned and unsupported) packages in our internal directory — even if the project code is publicly available somewhere like GitHub.

 

HTML templating and inheritance

func home(w http.ResponseWriter, r *http.Request) {
    w.Header().Add("Server", "Go")

    // Use the template.ParseFiles() function to read the template file into a
    // template set. If there's an error, we log the detailed error message, use
    // the http.Error() function to send an Internal Server Error response to the
    // user, and then return from the handler so no subsequent code is executed.
    ts, err := template.ParseFiles("./ui/html/pages/home.html")
    if err != nil {
        log.Print(err.Error())
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }

    // Then we use the Execute() method on the template set to write the
    // template content as the response body. The last parameter to Execute()
    // represents any dynamic data that we want to pass in, which for now we'll
    // leave as nil.
    err = ts.Execute(w, nil)
    if err != nil {
        log.Print(err.Error())
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
    }
}

There are a couple of important things about this code to point out:

  • The file path that you pass to the template.ParseFiles() function must either be relative to your current working directory, or an absolute path. In the code above I’ve made the path relative to the root of the project directory.
  • If either the template.ParseFiles() or ts.Execute() functions return an error, we log the detailed error message and then use the http.Error() function to send a response to the user. http.Error() is a lightweight helper function which sends a plain text error message and a specific HTTP status code to the user (in our code we send the message "Internal Server Error" and the status code 500 , represented by the constant http.StatusInternalServerError ). Effectively, this means that if there is an error, the user will see the message Internal Server Error in their browser, but the detailed error message will be recorded in the application log messages.

 

{{define "base"}}
<!doctype html>
<html lang='en'>
    <head>
        <meta charset='utf-8'>
        <title>{{template "title" .}} - Snippetbox</title>
    </head>
    <body>
        <header>
            <h1><a href='/'>Snippetbox</a></h1>
        </header>
        <main>
            {{template "main" .}}
        </main>
        <footer>Powered by <a href='https://golang.org/'>Go</a></footer>
    </body>
</html>
{{end}}

We use the {{define "base"}}...{{end}} action as a wrapper to define a distinct named template called base , which contains the content we want to appear on every page.

Inside this we use the {{template "title" .}} and {{template "main" .}} actions to denote that we want to invoke other named templates (called title and main ) at a particular location in the HTML.

Note: If you’re wondering, the dot at the end of the {{template "title" .}} action represents any dynamic data that you want to pass to the invoked template. 

func home(w http.ResponseWriter, r *http.Request) {
    w.Header().Add("Server", "Go")
    // Initialize a slice containing the paths to the two files. It's important
    // to note that the file containing our base template must be the *first*
    // file in the slice.
    files := []string{
        "./ui/html/base.html",
        "./ui/html/pages/home.html",
    }

    // Use the template.ParseFiles() function to read the files and store the
    // templates in a template set. Notice that we use ... to pass the contents
    // of the files slice as variadic arguments.
    ts, err := template.ParseFiles(files...)
    if err != nil {
        log.Print(err.Error())
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }

    // Use the ExecuteTemplate() method to write the content of the "base"
    // template as the response body.
    err = ts.ExecuteTemplate(w, "base", nil)
    if err != nil {
        log.Print(err.Error())
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
    }
}

 

The block action

Go also provides a {{block}}...{{end}} action which you can use instead. This acts like the {{template}} action, except it allows you to specify some default content if the template being invoked doesn’t exist in the current template set.

In the context of a web application, this is useful when you want to provide some default content (such as a sidebar) which individual pages can override on a case-by-case basis if they need to.

Syntactically you use it like this:

{{define "base"}}
    <h1>An example template</h1>
    {{block "sidebar" .}}
        <p>My default sidebar content</p>
    {{end}}
{{end}}

But — if you want — you don’t need to include any default content between the {{block}} and {{end}} actions. In that case, the invoked template acts like it’s ‘optional’. If the template exists in the template set, then it will be rendered. But if it doesn’t, then nothing will be displayed.

 

File server features and functions

Go’s http.FileServer handler has a few really nice features that are worth mentioning:

  • It sanitizes all request paths by running them through the path.Clean() function before searching for a file. This removes any . and .. elements from the URL path, which helps to stop directory traversal attacks. This feature is particularly useful if you’re using the fileserver in conjunction with a router that doesn’t automatically sanitize URL paths.
  • Range requests are fully supported. This is great if your application is serving large files and you want to support resumable downloads. You can see this functionality in action if you use curl to request bytes 100-199 of the logo.png file, like so:
$ curl -i -H "Range: bytes=100-199" --output - http://localhost:4000/static/img/logo.png
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Length: 100
Content-Range: bytes 100-199/1075
Content-Type: image/png
Last-Modified: Wed, 18 Mar 2024 11:29:23 GMT
Date: Wed, 18 Mar 2024 11:29:23 GMT
[binary data]
  • The Last-Modified and If-Modified-Since headers are transparently supported. If a file hasn’t changed since the user last requested it, then http.FileServer will send a 304 Not Modified status code instead of the file itself. This helps reduce latency and processing overhead for both the client and server.
  • The Content-Type is automatically set from the file extension using the mime.TypeByExtension() function. You can add your own custom extensions and content types using the mime.AddExtensionType() function if necessary.

 

Serving single files

Sometimes you might want to serve a single file from within a handler. For this there’s the http.ServeFile() function, which you can use like so:

func downloadHandler(w http.ResponseWriter, r *http.Request) {
    http.ServeFile(w, r, "./ui/static/file.zip")
}

Warning: http.ServeFile() does not automatically sanitize the file path. If you’re constructing a file path from untrusted user input, to avoid directory traversal attacks you must sanitize the input with filepath.Clean() before using it.

 

Requests are handled concurrently

There is one more thing that’s really important to point out: all incoming HTTP requests are served in their own goroutine. For busy servers, this means it’s very likely that the code in or called by your handlers will be running concurrently. While this helps make Go blazingly fast, the downside is that you need to be aware of (and protect against) race conditions when accessing shared resources from your handlers.

 

Command-line flags

addr := flag.String("addr", ":4000", "HTTP network address")

 

zzh@ZZHPC:/zdata/Github/snippetbox$ go run ./cmd/web -addr=:80
2024/09/02 10:02:32 starting server on :80
2024/09/02 10:02:32 listen tcp :80: bind: permission denied
exit status 1
zzh@ZZHPC:/zdata/Github/snippetbox$ go run ./cmd/web -addr=":80"
2024/09/02 10:02:50 starting server on :80
2024/09/02 10:02:50 listen tcp :80: bind: permission denied
exit status 1
zzh@ZZHPC:/zdata/Github/snippetbox$ go run ./cmd/web -addr=:5000
2024/09/02 10:03:15 starting server on :5000

Note: Ports 0-1023 are restricted and (typically) can only be used by services which have root privileges. If you try to use one of these ports you should get a bind: permission denied error message on start-up.

In the code above we’ve used the flag.String() function to define the command-line flag. This has the benefit of converting whatever value the user provides at runtime to a string type. If the value can’t be converted to a string then the application will print an error message and exit.

Go also has a range of other functions including flag.Int() , flag.Bool() , flag.Float64() and flag.Duration() for defining flags. These work in exactly the same way as flag.String() , except they automatically convert the command-line flag value to the
appropriate type.

 

Automated help

Another great feature is that you can use the -help flag to list all the available command-line flags for an application and their accompanying help text.

zzh@ZZHPC:/zdata/Github/snippetbox$ go run ./cmd/web -help
Usage of /tmp/go-build2756680308/b001/exe/web:
  -addr string
        HTTP network address (default ":4000")

 

Environment variables

addr := os.Getenv("SNIPPETBOX_ADDR")

But this has some drawbacks compared to using command-line flags. You can’t specify a default setting (the return value from os.Getenv() is the empty string if the environment variable doesn’t exist), you don’t get the -help functionality that you do with command-line flags, and the return value from os.Getenv() is always a string — you don’t get automatic type conversions like you do with flag.Int() , flag.Bool() and the other command line flag functions.

 

Boolean flags

For flags defined with flag.Bool() , omitting a value when starting the application is the same as writing -flag=true . The following two commands are equivalent:

$ go run ./example -flag=true
$ go run ./example -flag

You must explicitly use -flag=false if you want to set a boolean flag value to false.

 

Pre-existing variables

It’s possible to parse command-line flag values into the memory addresses of pre-existing variables, using flag.StringVar() , flag.IntVar() , flag.BoolVar() , and similar functions for other types.

These functions are particularly useful if you want to store all your configuration settings in a single struct.

type config struct {
    addr      string
    staticDir string
}

func main() {
    var cfg config
    flag.StringVar(&cfg.addr, "addr", ":4000", "HTTP network address")
    flag.StringVar(&cfg.staticDir, "static-dir", "./ui/static", "Path to static assets")
    flag.Parse()
}

 

Creating a structured logger

The code for creating a structured logger with the log/slog package can be a little bit confusing the first time you see it.

The key thing to understand is that all structured loggers have a structured logging handler associated with them (not to be confused with a HTTP handler), and it’s actually this handler that controls how log entries are formatted and where they are written to.

The code for creating a logger looks like this:

loggerHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{...})
logger := slog.New(loggerHandler)

In the first line of code we first use the slog.NewTextHandler() function to create the structured logging handler. This function accepts two arguments:

  • The first argument is the write destination for the log entries. In the example above we’ve set it to os.Stdout , which means it will write log entries to the standard out stream.
  • The second argument is a pointer to a slog.HandlerOptions struct , which you can use to customize the behavior of the handler. We’ll take a look at some of the available customizations at the end of this chapter. If you’re happy with the defaults and don’t want to change anything, you can pass nil as the second argument instead.

Then in the second line of code, we actually create the structured logger by passing the handler to the slog.New() function.

In practice, it’s more common to do all this in a single line of code:

logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{...}))

 

Using a structured logger

Once you’ve created a structured logger, you can then write a log entry at a specific severity level by calling the Debug() , Info() , Warn() or Error() methods on the logger. As an example, the following line of code:

logger.Info("request received")

 

time=2024-03-18T11:29:23.000+00:00 level=INFO msg="request received"

The Debug() , Info() , Warn() or Error() methods are variadic methods which accept an arbitrary number of additional attributes ( key-value pairs). Like so:

logger.Info("request received", "method", "GET", "path", "/")

In this example, we’ve added two extra attributes to the log entry: the key "method" and value "GET" , and the key "path" and value "/" . Attribute keys must always be strings, but the values can be of any type. In this example, the log entry will look like this:

time=2024-03-18T11:29:23.000+00:00 level=INFO msg="request received" method=GET path=/

Note: If your attribute keys, values, or log message contain " or = characters or any whitespace, they will be wrapped in double quotes in the log output. We can see this behavior in the example above, where the log message msg="request received" is quoted.

 

Safer attributes

Let’s say that you accidentally write some code where you forget to include either the key or value for an attribute. For example:

logger.Info("starting server", "addr") // Oops, the value for "addr" is missing

When this happens, the log entry will still be written but the attribute will have the key !BADKEY , like so:

To avoid this happening and catch any problems at compile-time, you can use the slog.Any() function to create an attribute pair instead:

logger.Info("starting server", slog.Any("addr", ":4000"))

Or you can go even further and introduce some additional type safety by using the slog.String() , slog.Int() , slog.Bool() , slog.Time() and slog.Duration() functions to create attributes with a specific type of value.

logger.Info("starting server", slog.String("addr", ":4000"))

Whether you want to use these functions or not is up to you. The log/slog package is relatively new to Go (introduced in Go 1.21), and there isn’t much in the way of established best-practices or conventions around using it yet. But the trade-off is straightforward… using functions like slog.String() to create attributes is more verbose, but safer in sense that it reduces the risk of bugs in your application.

 

Minimum log level

As we’ve mentioned a couple of times, the log/slog package supports four severity levels: Debug , Info , Warn and Error in that order. Debug is the least severe level, and Error is the most severe.

By default, the minimum log level for a structured logger is Info . That means that any log entries with a severity less than Info — i.e. Debug level entries — will be silently discarded. 

You can use the slog.HandlerOptions struct to override this and set the minimum level to Debug (or any other level) if you want:

logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelDebug,
}))

 

Caller location

You can also customize the handler so that it includes the filename and line number of the calling source code in the log entries, like so:

logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
    AddSource: true,
}))

The log entries will look similar to this, with the caller location recorded under the source key:

time=2024-03-18T11:29:23.000+00:00 level=INFO source=/home/alex/code/snippetbox/cmd/web/main.go:32 msg="starting server" addr

 

Decoupled logging

In this chapter we’ve set up our structured logger to write entries to os.Stdout — the standard out stream.

The big benefit of writing log entries to os.Stdout is that your application and logging are decoupled. Your application itself isn’t concerned with the routing or storage of the logs, and that can make it easier to manage the logs differently depending on the environment.

During development, it’s easy to view the log output because the standard out stream is displayed in the terminal.

In staging or production environments, you can redirect the stream to a final destination for viewing and archival. This destination could be on-disk files, or a logging service such as Splunk. Either way, the final destination of the logs can be managed by your execution environment independently of the application.

For example, we could redirect the standard out stream to a on-disk file when starting the application like so:

$ go run ./cmd/web >>/tmp/web.log

 

Concurrent logging

Custom loggers created by slog.New() are concurrency-safe. You can share a single logger and use it across multiple goroutines and in your HTTP handlers without needing to worry about race conditions.

That said, if you have multiple structured loggers writing to the same destination then you need to be careful and ensure that the destination’s underlying Write() method is also safe for concurrent use.

 

Dependency injection

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 structured logger that we now want to be using.

This raises a good question: how can we make our new structured 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.

First open your main.go file and create a new application struct like so:

// Define an application struct to hold the application-wide dependencies for the 
// web application. For now we'll only include the structured logger.
type application struct {
    logger *slog.Logger
}

func main() {
    ...
}

And then in the handlers.go file, we want to update the handler functions so that they become methods against the application struct and use the structured logger that it contains.

func (app *application) home(w http.ResponseWriter, r *http.Request) {
    w.Header().Add("Server", "Go")

    files := []string{
        "./ui/html/base.html",
        "./ui/html/partials/nav.html",
        "./ui/html/pages/home.html",
    }

    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.
        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 {
        app.logger.Error(err.Error(), "method", r.Method, "uri", r.URL.RequestURI())
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
    }
}

And finally let’s wire things together in our main.go file:

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.
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))

    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)
}

 

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 standalone config package which exports an Application struct, and have your handler functions close over this to form a closure. Very roughly:

// 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))
    ...
}

 

Centralized error handling

Let’s neaten up our application by moving some of the error handling code into helper methods. This will help separate our concerns and stop us repeating code as we progress through the build.

Go ahead and add a new helpers.go file under the cmd/web directory:

package main

import "net/http"

// The serverError helper writes a log entry at Error level (including the request
// method and URI as attributes), then sends a generic 500 Internal Server Error
// response to the user.
func (app *application) serverError(w http.ResponseWriter, r *http.Request, err error) {
    var (
        method = r.Method
        uri = r.URL.RequestURI()
    )

    app.logger.Error(err.Error(), "method", method, "uri", uri)
    http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}

// The clientError helper sends a specific status code and corresponding description 
// to the user. We'll use this later in the book to send responses like 400 "Bad 
// Request" when there's a problem with the request that the user sent.
func (app *application) clientError(w http.ResponseWriter, status int) {
    http.Error(w, http.StatusText(status), status)
}

Now that’s done, head back to your handlers.go file and update it to use the new serverError() helper:

...
    ts, err := template.ParseFiles(files...)
    if err != nil {
        app.serverError(w, r, err)  // Use the serverError() helper.
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }

    err = ts.ExecuteTemplate(w, "base", nil)
    if err != nil {
        app.serverError(w, r, err)
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
    }
...

 

Stack traces

You can use the debug.Stack() function to get a stack trace outlining the execution path of the application for the current goroutine. Including this as an attribute in your log entries can be helpful for debugging errors.

If you want, you could update the serverError() method so that it includes a stack trace in the log entries like so:

func (app *application) serverError(w http.ResponseWriter, r *http.Request, err error) {
    var (
        method = r.Method
        uri    = r.URL.RequestURI()
        // Use debug.Stack() to get the stack trace. This returns a byte slice, which
        // we need to convert to a string so that it's readable in the log entry.
        trace = string(debug.Stack())
    )
    // Include the trace in the log entry.
    app.logger.Error(err.Error(), "method", method, "uri", uri, "trace", trace)
    http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}

The log entry output would then look something like this (line breaks added for readability):

time=2024-03-18T11:29:23.000+00:00 level=ERROR msg="open ./ui/html/pages/home.html:
no such file or directory" method=GET uri=/ trace="goroutine 6 [running]:\nruntime/
debug.Stack()\n\t/usr/local/go/src/runtime/debug/stack.go:24 +0x5e\nmain.(*applicat
ion).serverError(0xc00006c048, {0x8221b0, 0xc0000f40e0}, 0x3?, {0x820600, 0xc0000ab
5c0})\n\t/home/alex/code/snippetbox/cmd/web/helpers.go:14 +0x74\nmain.(*application
).home(0x10?, {0x8221b0?, 0xc0000f40e0}, 0xc0000fe000)\n\t/home/alex/code/snippetbo
x/cmd/web/handlers.go:24 +0x16a\nnet/http.HandlerFunc.ServeHTTP(0x4459e0?, {0x8221b
0?, 0xc0000f40e0?}, 0x6cc57a?)\n\t/usr/local/go/src/net/http/server.go:2136 +0x29\n
net/http.(*ServeMux).ServeHTTP(0xa7fde0?, {0x8221b0, 0xc0000f40e0}, 0xc0000fe000)\n
\t/usr/local/go/src/net/http/server.go:2514 +0x142\nnet/http.serverHandler.ServeHTT
P({0xc0000aaf00?}, {0x8221b0?, 0xc0000f40e0?}, 0x6?)\n\t/usr/local/go/src/net/http/
server.go:2938 +0x8e\nnet/http.(*conn).serve(0xc0000c0120, {0x8229e0, 0xc0000aae10})
\n\t/usr/local/go/src/net/http/server.go:2009 +0x5f4\ncreated by net/http.(*Server).
Serve in goroutine 1\n\t/usr/local/go/src/net/http/server.go:3086 +0x5cb\n"

 

Isolating the application routes

While we’re refactoring our code there’s one more change worth making. Our main() function is beginning to get a bit crowded, so to keep it clear and focused I’d like to move the route declarations for the application into a standalone routes.go file, like
so:

package main

import "net/http"

// The routes() method returns a servemux containing our application routes.
func (app *application) routes() *http.ServeMux {
    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)

    return mux
}

 

// application struct holds the application-wide dependencies for the web application.
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))

    app := &application{
        logger: logger,
    }

    logger.Info("starting server", "addr", *addr)

    err := http.ListenAndServe(*addr, app.routes())
    logger.Error(err.Error())
    os.Exit(1)
}

This is quite a bit neater. The routes for our application are now isolated and encapsulated in the app.routes() method, and the responsibilities of our main() function are limited to:

  • Parsing the runtime configuration settings for the application;
  • Establishing the dependencies for the handlers; and
  • Running the HTTP server.

 

posted on 2024-09-01 18:58  ZhangZhihuiAAA  阅读(4)  评论(0编辑  收藏  举报