Parsing form data
Thanks to the work we did previously in the advanced routing section, any POST /snippets/create
requests are already being dispatched to our snippetCreatePost
handler. We’ll now update this handler to process and use the form data when it’s submitted.
At a high-level we can break this down into two distinct steps.
First, we need to use the
r.ParseForm()
method to parse the request body. This checks that the request body is well-formed, and then stores the form data in the request’sr.PostForm
map. If there are any errors encountered when parsing the body (like there is no body, or it’s too large to process) then it will return an error. Ther.ParseForm()
method is also idempotent; it can safely be called multiple times on the same request without any side-effects.We can then get to the form data contained in
r.PostForm
by using ther.PostForm.Get()
method. For example, we can retrieve the value of thetitle
field withr.PostForm.Get("title")
. If there is no matching field name in the form this will return the empty string""
, similar to the way that query string parameters worked earlier in the book.
Open your cmd/web/handlers.go
file and update it to include the following code:
package main ... func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) { // First we call r.ParseForm() which adds any data in POST request bodies // to the r.PostForm map. This also works in the same way for PUT and PATCH // requests. If there are any errors, we use our app.ClientError() helper to // send a 400 Bad Request response to the user. err := r.ParseForm() if err != nil { app.clientError(w, http.StatusBadRequest) return } // Use the r.PostForm.Get() method to retrieve the title and content // from the r.PostForm map. title := r.PostForm.Get("title") content := r.PostForm.Get("content") // The r.PostForm.Get() method always returns the form data as a *string*. // However, we're expecting our expires value to be a number, and want to // represent it in our Go code as an integer. So we need to manually covert // the form data to an integer using strconv.Atoi(), and we send a 400 Bad // Request response if the conversion fails. expires, err := strconv.Atoi(r.PostForm.Get("expires")) if err != nil { app.clientError(w, http.StatusBadRequest) return } id, err := app.snippets.Insert(title, content, expires) if err != nil { app.serverError(w, err) return } http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther) }
Alright, let’s give this a try! Restart the application and try filling in the form with the title and content of a snippet, a bit like this:

And then submit the form. If everything has worked, you should be redirected to a page displaying your new snippet like so:

Additional information
The r.Form map
In our code above, we accessed the form values via the r.PostForm
map. But an alternative approach is to use the (subtly different) r.Form
map.
The r.PostForm
map is populated only for POST
, PATCH
and PUT
requests, and contains the form data from the request body.
In contrast, the r.Form
map is populated for all requests (irrespective of their HTTP method), and contains the form data from any request body and any query string parameters. So, if our form was submitted to /snippet/create?foo=bar
, we could also get the value of the foo
parameter by calling r.Form.Get("foo")
. Note that in the event of a conflict, the request body value will take precedent over the query string parameter.
Using the r.Form
map can be useful if your application sends data in a HTML form and in the URL, or you have an application that is agnostic about how parameters are passed. But in our case those things aren’t applicable. We expect our form data to be sent in the request body only, so it’s sensible for us to access it via r.PostForm
.
The FormValue and PostFormValue methods
The net/http
package also provides the methods r.FormValue()
and r.PostFormValue()
. These are essentially shortcut functions that call r.ParseForm()
for you, and then fetch the appropriate field value from r.Form
or r.PostForm
respectively.
I recommend avoiding these shortcuts because they silently ignore any errors returned by r.ParseForm()
. That’s not ideal — it means our application could be encountering errors and failing for users, but there’s no feedback mechanism to let them know.
Multiple-value fields
Strictly speaking, the r.PostForm.Get()
method that we’ve used above only returns the first value for a specific form field. This means you can’t use it with form fields which potentially send multiple values, such as a group of checkboxes.
<input type="checkbox" name="items" value="foo"> Foo <input type="checkbox" name="items" value="bar"> Bar <input type="checkbox" name="items" value="baz"> Baz
In this case you’ll need to work with the r.PostForm
map directly. The underlying type of the r.PostForm
map is url.Values
, which in turn has the underlying type map[string][]string
. So, for fields with multiple values you can loop over the underlying map to access them like so:
for i, item := range r.PostForm["items"] { fmt.Fprintf(w, "%d: Item %s\n", i, item) }
Limiting form size
Unless you’re sending multipart data (i.e. your form has the enctype="multipart/form-data"
attribute) then POST
, PUT
and PATCH
request bodies are limited to 10MB. If this is exceeded then r.ParseForm()
will return an error.
If you want to change this limit you can use the http.MaxBytesReader()
function like so:
// Limit the request body size to 4096 bytes r.Body = http.MaxBytesReader(w, r.Body, 4096) err := r.ParseForm() if err != nil { http.Error(w, "Bad Request", http.StatusBadRequest) return }
With this code only the first 4096 bytes of the request body will be read during r.ParseForm()
. Trying to read beyond this limit will cause the MaxBytesReader
to return an error, which will subsequently be surfaced by r.ParseForm()
.
Additionally — if the limit is hit — MaxBytesReader
sets a flag on http.ResponseWriter
which instructs the server to close the underlying TCP connection.