URL encoding in Go
Go’s standard library net/url package gets URL encoding right — clear naming, well-separated functions for different parts of a URL, and no historical quirks to work around. This article covers the four main functions, when to use each, and a few idiomatic patterns.
The four functions
import "net/url"
// Encode a query value (space becomes +)
encoded := url.QueryEscape("Hello, World!")
// "Hello%2C+World%21"
// Encode a path segment (space becomes %20)
encoded := url.PathEscape("Hello, World!")
// "Hello%2C%20World%21"
// Decode a query value
decoded, err := url.QueryUnescape("Hello%2C+World%21")
// "Hello, World!", nil
// Decode a path segment
decoded, err := url.PathUnescape("Hello%2C%20World%21")
// "Hello, World!", nil
Unlike Java, Go gives you separate functions for path and query encoding. The split mirrors how URL parts have different reserved-character rules:
- Query:
+means space (form-encoded convention) - Path:
+is literal; spaces are always%20
url.Values: building query strings
For more than one query parameter, use url.Values:
v := url.Values{}
v.Set("q", "Hello, World!")
v.Set("lang", "en")
v.Add("tag", "foo")
v.Add("tag", "bar") // multiple values per key
query := v.Encode()
// "lang=en&q=Hello%2C+World%21&tag=foo&tag=bar"
Note: v.Encode() sorts keys alphabetically. If you need a specific order (for signing, e.g., AWS), build the string manually or use an ordered map.
Three relevant methods on url.Values:
Set(key, value)— replaces any existing value for the keyAdd(key, value)— appends a value, allowing duplicate keysGet(key)— returns the first value, or empty string if not present
url.URL: the structured type
For working with whole URLs:
u, err := url.Parse("https://example.com/search?q=test&page=2#section")
if err != nil { return err }
u.Scheme // "https"
u.Host // "example.com"
u.Path // "/search"
u.RawQuery // "q=test&page=2"
u.Fragment // "section"
And to rebuild it after modifying:
u.Path = "/new-path"
v := u.Query()
v.Set("page", "3")
u.RawQuery = v.Encode()
fmt.Println(u.String())
// "https://example.com/new-path?page=3&q=test#section"
u.Query() parses RawQuery into a url.Values; u.String() rebuilds the URL with all parts encoded correctly.
Real-world patterns
Building a search URL with type safety
func searchURL(query string, page int) string {
u := &url.URL{
Scheme: "https",
Host: "api.example.com",
Path: "/search",
}
v := url.Values{}
v.Set("q", query)
v.Set("page", strconv.Itoa(page))
u.RawQuery = v.Encode()
return u.String()
}
Building the URL by setting fields on url.URL is more robust than string concatenation — Go encodes each part appropriately during u.String().
Adding a parameter to an existing URL
func addParam(rawURL, key, value string) (string, error) {
u, err := url.Parse(rawURL)
if err != nil { return "", err }
v := u.Query()
v.Set(key, value)
u.RawQuery = v.Encode()
return u.String(), nil
}
// addParam("https://example.com/page?ref=ad", "utm_source", "twitter")
// → "https://example.com/page?ref=ad&utm_source=twitter"
Encoding a path with a slash in the data
productName := "A/B Testing"
encoded := url.PathEscape(productName)
// "A%2FB%20Testing"
fullPath := "/products/" + encoded
What url.URL handles automatically
If you build a URL using the structured type, Go handles encoding correctly:
u := &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/products/My Item", // unencoded — Go will encode on output
}
fmt.Println(u.String())
// "https://example.com/products/My%20Item"
The Path field stores the unencoded form. String() encodes it appropriately when producing the final URL.
For cases where you need precise control over what gets encoded (e.g., the path already contains %XX sequences you want preserved), set RawPath instead.
Going beyond the standard library
For most use cases, net/url is enough. Common reasons to use a third-party library:
- URL signing for AWS, GCS, or other cloud APIs — use the vendor SDK
- OAuth signature base strings — require very specific encoding rules (always %20, always upper-case hex). Hand-write or use a library that explicitly handles it.
- Pretty URL builders — some libraries (like
github.com/google/go-querystring) let you tag struct fields and have them automatically encoded
Common Go URL encoding mistakes
Mistake 1: Mixing PathEscape and QueryEscape
Using PathEscape on a query value works but produces %20 instead of +. Mostly harmless, but produces URLs that look different from what a browser would send.
Using QueryEscape on a path segment is worse — the + ends up as literal plus in the path, which probably isn’t what you want.
Mistake 2: Manually concatenating without escaping
// Wrong
url := "https://api.example.com/search?q=" + userInput
// Right
v := url.Values{"q": {userInput}}
url := "https://api.example.com/search?" + v.Encode()
Mistake 3: Ignoring the second return from QueryUnescape
// Wrong — silently ignores invalid input
decoded, _ := url.QueryUnescape(suspicious)
// Right — check the error
decoded, err := url.QueryUnescape(suspicious)
if err != nil {
return nil, fmt.Errorf("bad URL encoding: %w", err)
}
Quick reference
| Goal | Function |
|---|---|
| Encode a query value | url.QueryEscape(s) |
| Encode a path segment | url.PathEscape(s) |
| Decode a query value | url.QueryUnescape(s) |
| Decode a path segment | url.PathUnescape(s) |
| Build query from map | url.Values{}.Encode() |
| Build whole URL | &url.URL{...}.String() |
Go’s split between QueryEscape and PathEscape is exactly right — once you internalize that distinction, every URL encoding decision in Go is unambiguous.
Found this useful? Try the URL decoder, the URL encoder, or browse all tools.