Serving static files
Now let’s improve the look and feel of the home page by adding some static CSS and image files to our project, along with a tiny bit of JavaScript to highlight the active navigation item.
If you’re following along, you can grab the necessary files and extract them into the ui/static
folder that we made earlier with the following commands:
$ cd $HOME/code/snippetbox $ curl https://www.alexedwards.net/static/sb-v2.tar.gz | tar -xvz -C ./ui/static/
The contents of your ui/static
directory should now look like this:

The http.Fileserver handler
Go’s net/http
package ships with a built-in http.FileServer
handler which you can use to serve files over HTTP from a specific directory. Let’s add a new route to our application so that all GET
requests which begin with "/static/"
are handled using this, like so:
Route pattern | Handler | Action |
---|---|---|
GET / | home | Display the home page |
GET /snippet/view/{id} | snippetView | Display a specific snippet |
GET /snippet/create | snippetCreate | Display a form for creating a new snippet |
POST /snippet/create | snippetCreatePost | Save a new snippet |
GET /static/ | http.FileServer | Serve a specific static file |
To create a new http.FileServer
handler, we need to use the http.FileServer()
function like this:
fileServer := http.FileServer(http.Dir("./ui/static/"))
When this handler receives a request for a file, it will remove the leading slash from the request URL path and then search the ./ui/static
directory for the corresponding file to send to the user.
So, for this to work correctly, we must strip the leading "/static"
from the URL path before passing it to http.FileServer
. Otherwise it will be looking for a file which doesn’t exist and the user will receive a 404 page not found
response. Fortunately Go includes a http.StripPrefix()
helper specifically for this task.
Open your main.go
file and add the following code, so that the file ends up looking like this:
package main import ( "log" "net/http" ) func main() { mux := http.NewServeMux() // Create a file server which serves files out of the "./ui/static" directory. // Note that the path given to the http.Dir function is relative to the project // directory root. fileServer := http.FileServer(http.Dir("./ui/static/")) // Use the mux.Handle() function to register the file server as the handler for // all URL paths that start with "/static/". For matching paths, we strip the // "/static" prefix before the request reaches the file server. mux.Handle("GET /static/", http.StripPrefix("/static", fileServer)) // Register the other application routes as normal.. mux.HandleFunc("GET /{$}", home) mux.HandleFunc("GET /snippet/view/{id}", snippetView) mux.HandleFunc("GET /snippet/create", snippetCreate) mux.HandleFunc("POST /snippet/create", snippetCreatePost) log.Print("starting server on :4000") err := http.ListenAndServe(":4000", mux) log.Fatal(err) }
Once that’s complete, restart the application and open http://localhost:4000/static/
in your browser. You should see a navigable directory listing of the ui/static
folder which looks like this:

Feel free to have a play around and browse through the directory listing to view individual files. For example, if you navigate to http://localhost:4000/static/css/main.css
you should see the CSS file appear in your browser like so:

Using the static files
With the file server working properly, we can now update the ui/html/base.tmpl
file to make use of the static files:
{{define "base"}} <!doctype html> <html lang='en'> <head> <meta charset='utf-8'> <title>{{template "title" .}} - Snippetbox</title> <!-- Link to the CSS stylesheet and favicon --> <link rel='stylesheet' href='/static/css/main.css'> <link rel='shortcut icon' href='/static/img/favicon.ico' type='image/x-icon'> <!-- Also link to some fonts hosted by Google --> <link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Ubuntu+Mono:400,700'> </head> <body> <header> <h1><a href='/'>Snippetbox</a></h1> </header> {{template "nav" .}} <main> {{template "main" .}} </main> <footer>Powered by <a href='https://golang.org/'>Go</a></footer> <!-- And include the JavaScript file --> <script src='/static/js/main.js' type='text/javascript'></script> </body> </html> {{end}}
Make sure you save the changes, then restart the server and visit http://localhost:4000
. Your home page should now look like this:

Additional information
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
andIf-Modified-Since
headers are transparently supported. If a file hasn’t changed since the user last requested it, thenhttp.FileServer
will send a304 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 themime.TypeByExtension()
function. You can add your own custom extensions and content types using themime.AddExtensionType()
function if necessary.
Performance
In this chapter we set up the file server so that it serves files out of the ./ui/static
directory on your hard disk.
But it’s important to note that http.FileServer
probably won’t be reading these files from disk once the application is up-and-running. Both Windows and Unix-based operating systems cache recently-used files in RAM, so (for frequently-served files at least) it’s likely that http.FileServer
will be serving them from RAM rather than making the relatively slow round-trip to your hard disk.
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") }
Disabling directory listings
If you want to disable directory listings there are a few different approaches you can take.
The simplest way? Add a blank index.html
file to the specific directory that you want to disable listings for. This will then be served instead of the directory listing, and the user will get a 200 OK
response with no body. If you want to do this for all directories under ./ui/static
you can use the command:
$ find ./ui/static -type d -exec touch {}/index.html \;
A more complicated (but arguably better) solution is to create a custom implementation of http.FileSystem
, and have it return an os.ErrNotExist
error for any directories. A full explanation and sample code can be found in this blog post.