Golang Query Parameter Library
Apr 28, 2016
4 minutes read

While you're reading this, keep in mind that I'm available for hire! If you've got a JavaScript project getting out of hand, or a Golang program that's more "stop" than "go," feel free to get in touch with me. I might be able to help you. You can find my resume here.

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
}

Back to posts