While you're reading this, keep in mind that I'm available for hire stupid!
The Go standard library has a pretty solid package for parsing URLs and query parameters, in the form of net/url. The way the query parameter parser stores data though discards the order of parameters. Most of the time this is just fine, but recently I was implementing an API that cared a lot about the order of query parameters. Turns out parsing query parameters isn’t too hard, and I wrote my own library for it. You can find it at fknsrs.biz/p/urlqp (godoc).
The approach that I took was to store a list of key/value pairs. This way I
can maintain the order of the parameters. The data structure I use to store a
key/value pair is type Pair [2]string
, and the structure I use to store the
list is type Values []Pair
. There are a couple of convenience functions on
the Values
type - check out godoc for more details.
Here’s an example of the difference between using urlqp
and net/url
.
// We want to be able to process some query parameters in order. This API
// treats the order of query parameters significantly, using them as a set of
// operations.
func ExampleParse() {
// Add one, multiply by two, add three.
str := `add=1&multiply=2&add=3`
// We should get `5` as the result.
var expected int64 = 5
// Here we use the default `net/url` query parser. It parses the parameters
// into a `map[string][]string`, which is perfectly fine for most uses, but
// not ours.
m, _ := url.ParseQuery(str)
{
var i int64
// The order that we range over this map is intentionally undefined. That
// means we'll end up with one of two results:
//
// > add 1, add 3, multiply 2 = 8
// or
// > multiply 2, add 1, add 3 = 4
//
// This is bad for this particular use case.
for k, l := range m {
for _, v := range l {
n, _ := strconv.ParseInt(v, 10, 32)
switch k {
case "add":
i += n
case "multiply":
i *= n
}
}
}
// This is going to print `false`.
fmt.Printf("equal: %v\n", i == expected)
}
// This is using the `urlqp` parser. It preserves the order of the
// parameters as it just returns a list of key/value pairs.
q, _ := Parse(str)
{
var i int64
// Iterating through this list happens in a defined order, which is the
// order the parameters were specified in the string above.
for _, p := range q {
k, v := p[0], p[1]
n, _ := strconv.ParseInt(v, 10, 32)
switch k {
case "add":
i += n
case "multiply":
i *= n
}
}
// This will print `true`
fmt.Printf("equal: %v\n", i == expected)
}
// Output:
//
// equal: false
// equal: true
}
Below is the annotated source code of the parsing function.
// Parse tries to parse the given string into a set of values. If it fails, it
// will return an error.
func Parse(s string) (Values, error) {
// We don't need the leading question mark, and it's safe to get rid of it.
s = strings.TrimPrefix(s, "?")
// A small optimisation - an empty string means an empty set of parameters.
if s == "" {
return nil, nil
}
// Split the string on `&`, which is the standard query parameter delimiter.
// It really is this simple, since `&` has to be escaped if it's contained
// in any values, so we can't end up splitting "too much" or anything like
// that.
a := strings.Split(s, "&")
// Another small optimisation - allocate the `Values` slice with the exact
// size we need ahead of time, to avoid copying during `append()` calls.
r := make(Values, len(a))
// Iterate through the query parameters one at a time, perserving the order.
for i, p := range a {
// Split this query parameter into two parts, at the first `=`. In the
// case of a construct like `?a&b`, the length of `b` will be 1. It can't
// be zero, since even an empty string will split to a slice with one
// element.
b := strings.SplitN(p, "=", 2)
// Unescape the query parameter key. If this fails, we return an error.
k, err := url.QueryUnescape(b[0])
if err != nil {
return nil, err
}
// Set a default for the value. Empty seems reasonable.
v := ""
// If `b` has more than one element, that means the second one will be the
// parameter value, so we should grab it.
if len(b) > 1 {
// Now unescape the query parameter value. If this fails, return an
// error.
d, err := url.QueryUnescape(b[1])
if err != nil {
return nil, err
}
// Nothing failed, so we can keep the parameter value.
v = d
}
// Add this parameter pair to the set of values.
r[i] = Pair{k, v}
}
// Return the values and nil, signalling no error.
return r, nil
}