Back

/ 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:

  1. Endpoint: http://localhost:8080/user/allposts
  2. 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:

main.go
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:

pkgs/router/router.go
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:

Terminal window
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:

/net/url/url.go
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

Terminal window
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!