/ 5 min read
How To Handle Query Parameters Using The Go Standard Library
Introduction
In the last post, we handled basic routing and made our own router that let us more easily define nested and complex routes. Today, we’ll be learning about handling query paramters in query strings, which are those weird key-value pairs after a question mark in a URL.
First, let’s see what a URL with a query string looks like:
http://localhost:8080/userpost?user=1154
This URL has two parts:
- Endpoint:
http://localhost:8080/user/allposts
- Query String:
user=1154
The ?
acts as a delimiter to let the multiplexer
know where the endpoint ends and the query string begins.
The query string itself is composed of a key-value pair: the parameter
, and it’s argument
You could define an endpoint that requires some state to be passed in for the request to be handled correctly. Let’s try making such an endpoint in Go.
Setup
We’ll be using the router we made last time, and our main.go
looks like this:
package main
import ( "bamboo/basic-routing/pkgs/router" "fmt" "log" "net/http")
func main() { base := router.NewRouter() users := router.NewRouter()
base.HandleSubroute("/users", users)
// Handles /users users.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Base Users") })
// Handles /users/first users.HandleFunc("/first", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "First") })
log.Fatal(http.ListenAndServe(":8080", base))}
And the router, defined in pkgs/router/router.go
looks like:
package router
import ( "net/http")
type Router struct { *http.ServeMux}
func NewRouter() Router { return Router{http.NewServeMux()}}
func (router *Router) HandleSubroute(route string, handler http.Handler) { router.Handle(route+"/", http.StripPrefix(route, handler))}
Query Strings
Now, query strings typically reside within the URL itself. Looking at the URL
field in http.Request
, we find the function Query
.
Looking at the source documentation, this returns a value of type url.Values
, which is simply:
type Values map[string][]string
Okay, great! so it’s just a map of a string to a list of strings
Let’s iterate over it, and print things out to see if we can get the values out.
We define a new route on users
to test out our query strings: users/query
users.HandleFunc("/query", func(w http.ResponseWriter, r *http.Request) { for key, value := range r.URL.Query() { fmt.Printf("%s => %s\n", key, value) }})
And if we make a request to http://localhost:8080/users/query?first_key=hello_world
, we see this in our console:
first_key => [hello_world]
Awesome, so it’s parsing the keys as expected. But why is it storing them as a map of string to a list of strings, instead of a map of string to string?
That’s because we can provide multiple values to a key by defining it again in the query string. For example, if we were to make a request like so:
/users/query?key=hello&key=world
; we would get this in our console:
key => [hello world]
Notice how our list now has two values in it. It combines the duplicate key-values into an array. that we can use.
Working With Query Strings
Now, we won’t always be ranging over the url.Values
to get our required fields.
We’ll need to get specific fields that have been passed in and process them as required.
We can do that by using the Get
method defined on url.Values
. Let’s modify our /users/query
endpoint to handle a user
parameter that logs the incoming user to the console
users.HandleFunc("/query", func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() userId := query.Get("user") fmt.Printf("user id is %s\n", userId)})
Let’s make a request:
iwr http://localhost:8080/users/query?user=1154
And this is what our console gets:
user id is 1154
And it’s as simple as that! We’re able to handle our query strings.
Handling multiple values
If, by chance, we decide we want to use the multiple-value feature of query strings, we’ll notice we won’t be able to get more than one value out of a field.
Looking at the source code of the Get
function, we see why that is:
func (v Values) Get(key string) string { vs := v[key] if len(vs) == 0 { return "" } return vs[0]}
It seems like it’s hard coded to return only the first value out.
To get multiple values out, we can write our own function to handle that for us.
In main.go
:
func getAllFromQuery(query url.Values, field string) []string { vals, ok := query[field] if !ok { return []string{} } return vals}
Now, we can rewrite our query to handle, let’s say, multiple post
fields.
users.HandleFunc("/query", func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() posts := getAllFromQuery(query, "post") fmt.Printf("posts are %s\n", posts)})
And now, making a request
iwr "http://localhost:8080/users/query?post=454&post=4477"
Logs this to the console:
posts are [454 4477]
Form Data
Of course, query strings and query paramters aren’t always passed in the URL of the request.
When the request has the POST
, PUT
, DELETE
etc. methods, the query string is sent as part of the Request body, and is typically accompanied with the Content-Type: application/x-www-form-urlencoded
header.
Go provides us with a way to handle all these different kinds of query strings all with one function. Where earlier we were using the URL.Query
method, we can use the ParseForm
method on http.Request
to first parse the query string, and the Form
field, also on http.Request
to get the Values
out.
users.HandleFunc("/query", func(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { w.WriteHeader(http.StatusBadRequest) fmt.Fprintf(w, "Invalid query string") return } query := r.Form posts := getAllFromQuery(query, "post") fmt.Printf("posts are %s\n", posts)})
And now you know how to handle query strings!