Requirement Scenario

When a backend service calls a third-party API, a common requirement is to build a URL query string. net/url is available in the go standard package to solve this problem, url.Values is essentially a map[string][] string, and provides a series of methods (Add, Del, Set) to manipulate the parameters, and eventually converts the map to a URL query string via the Encode() method.

However, there is some repetitive work involved, such as.

  1. type conversion, to int, bool, etc. to string
  2. to determine whether the field is empty or zero value of the processing logic

For this problem, google open source go-querystring can solve this kind of repetitive work elegantly and concisely.

Usage

The entire go-querystring library exposes only one method, func Values(v interface{}) (url.Values, error), which takes a structure and returns a populated url.Values.

By default, the key value in the URL query string is the field name of the structure. If the field does not need to be encoded, you can write url:"-" and for scenarios where null values need to be ignored, add omitempty, for example as follows.

1
2
3
4
5
6
type GetPodsReq struct {
    ClusterID int64  `form:"cluster_id" url:"cluster_id,omitempty"`
    Nodenames string `form:"nodenames" url:"nodenames,omitempty"`
    Selector  string `form:"selector" url:"selector,omitempty"`
    Hostnames string `form:"hostnames" url:"hostnames,omitempty"`
}

Converting a structure to a query string is very simple, requiring only a Values method call to convert the structure to url.Values, and then constructing the query string from the Encode method of `url.

1
2
3
4
5
v, err := query.Values(req)
if err != nil {
  return nil, err
}
url := fmt.Sprintf("%s/%s?%s", c.Domain, PathGetPods, v.Encode())

Using go-querystring to rework old code

In the previous project code, I used a lot of if-determinations to add query parameters one by one, as many times as there are parameters. And I used some other methods of net/url to finally encode the URL of the HTTP request.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
queryParams := url.Values{}

if req.ClusterID != 0 {
  queryParams.Add("cluster_id", fmt.Sprintf("%d", req.ClusterID))
}
if req.Selector != "" {
  queryParams.Add("selector", req.Selector)
}
if req.Nodenames != "" {
  queryParams.Add("nodenames", req.Nodenames)
}
if req.Hostnames != "" {
  queryParams.Add("hostnames", req.Hostnames)
}

casterURL, err := url.Parse(fmt.Sprintf("%s/%s", c.Domain, PathGetPods))
if err != nil {
  return nil, err
}
casterURL.RawQuery = queryParams.Encode()

c.get(ctx, casterURL.String(), resp)

With the go-querystring package, the code can be made very clean.

1
2
3
4
5
6
7
v, err := query.Values(req)
if err != nil {
  return nil, err
}

url := fmt.Sprintf("%s/%s?%s", c.Domain, PathGetPods, v.Encode())
c.get(ctx, url, resp)

Summary

go-querystring is a very simple and problem-oriented library that does one thing: transform custom structs to url.Values. The problem is very focused, and the solution is very extreme, which is what every open source project needs to learn and learn from.