Parsing form data
Thanks to the work we did previously in the foundations 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""
.
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, r, 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 PostFormValue method
The net/http
package also provides the r.PostFormValue()
method, which is essentially a shortcut function that calls r.ParseForm()
for you and then fetches the appropriate field value from r.PostForm
.
I recommend avoiding this shortcut because it silently ignores any errors returned by r.ParseForm()
. If you use it, it means that the parsing step could be encountering errors and failing for users, but there’s no feedback mechanism to let them (or you) know about the problem.
Multiple-value fields
Strictly speaking, the r.PostForm.Get()
method that we’ve used in this chapter 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
By default, forms submitted with a POST
method have a size limit of 10MB of data. The exception to this is if your form has the enctype="multipart/form-data"
attribute and is sending multipart data, in which case there is no default limit.
If you want to change the 10MB 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.
Query string parameters
If you have a form that submits data using the HTTP method GET
, rather than POST
, the form data will be included as URL query string parameters. For example, if you have a HTML form that looks like this:
<form action='/foo/bar' method='GET'> <input type='text' name='title'> <input type='text' name='content'> <input type='submit' value='Submit'> </form>
When the form is submitted, it will send a GET
request with a URL that looks like this: /foo/bar?title=value&content=value
.
You can retrieve the values for the query string parameters in your handlers via the r.URL.Query().Get()
method. This will always return a string value for a parameter, or the empty string ""
if no matching parameter exists. For example:
func exampleHandler(w http.ResponseWriter, r *http.Request) { title := r.URL.Query().Get("title") content := r.URL.Query().Get("content") ... }
The r.Form map
An alternative way to access query string parameters is via the r.Form
map. This is similar to the r.PostForm
map that we’ve used in this chapter, except that it contains the form data from any POST
request body and any query string parameters.
Let’s say that you have some code in your handler that looks like this:
err := r.ParseForm() if err != nil { http.Error(w, "Bad Request", http.StatusBadRequest) return } title := r.Form.Get("title")
In this code, the line r.Form.Get("title")
will return the title
value from the POST
request body or from a query string parameter with the name title
. In the event of a conflict, the request body value will take precedent over the query string parameter.
Using r.Form
can be very helpful if you want your application to be agnostic about how data values are passed to it. But outside of that scenario, r.Form
doesn’t offer any benefits and it is clearer and more explicit to read data from the POST
request body via r.PostForm
or from query string parameters via r.URL.Query().Get()
.