Article · Apr 16, 2026 · 6 min read

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:

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:

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:

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 valueurl.QueryEscape(s)
Encode a path segmenturl.PathEscape(s)
Decode a query valueurl.QueryUnescape(s)
Decode a path segmenturl.PathUnescape(s)
Build query from mapurl.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.

More reading

From the blog.