skip to content

What do url.URL.Path, url.URL.RawPath and the EscapedPath method each hold after url.Parse?

level: middleimportance: should knowfreq 42%

answer

  1. one field is meaning, one is wire text
  2. a slash inside a segment is the whole problem
  3. the second field is only a hint, often empty
  4. the method reconciles the two and can ignore one
  5. decode last, split first

basics

~20 s

Path holds the decoded path. RawPath holds the original escaped path, but only when it differs from the default encoding of Path. EscapedPath returns RawPath when it is a valid encoding of Path, otherwise it re-encodes Path itself.

solid answer

~50 s

`Path` is the path after percent-decoding, so `/a%2Fb` becomes `/a/b` and you can no longer tell an escaped slash from a segment separator. `RawPath` is an optional hint: `url.Parse` fills it only when the original text is not what Go would produce by encoding `Path` itself, which is exactly the case where information would otherwise be lost. `EscapedPath()` is the accessor that reconciles them — it returns `RawPath` when `RawPath` is a valid escaping of `Path`, and otherwise computes an encoding from `Path`, which is why mutating `Path` and leaving a stale `RawPath` behind is safe. `u.String()` renders the path through `EscapedPath()`. The practical rule: work on `Path` when you want meaning, on `EscapedPath()` when you care about the exact bytes, and use `url.PathEscape` per segment when you build a path that must contain a literal slash.

code

go · 9 lines
go
u, _ := url.Parse("https://api.example.test/files/a%2Fb/meta")
fmt.Println(u.Path)
// /files/a/b/meta
fmt.Println(u.RawPath)
// /files/a%2Fb/meta
fmt.Println(u.EscapedPath())
// /files/a%2Fb/meta
fmt.Println(u.String())
// https://api.example.test/files/a%2Fb/meta

go deeper

for a junior

Recall that a URL path exists in a decoded form and an escaped form, and that Go exposes both. Know that url.PathEscape is what you call on one segment you are inserting into a path.

for a middle

Explain the mechanics: which field is decoded, why the escaped field is stored only sometimes, and how the accessor method decides between them. Be able to say what %2F becomes after parsing.

for a senior

Show where the distinction changes behaviour in production: routing, caching, comparisons and signatures that decide with the decoded path accept URLs their author never meant to allow. Demonstrate the split-then-unescape ordering.

for a principal

Own the convention: pick one representation for identifiers crossing service boundaries — usually opaque, already-safe keys rather than anything needing an escaped slash — so that no downstream team has to get this pair right to stay correct.

## Two representations of the same path A URL path exists in two forms at once. There is the text that travels on the wire — `/files/a%2Fb/meta` — and there is what it *means* after percent-decoding — a three-segment path whose middle segment is the two characters `a/b`. `net/url` keeps both, because neither one alone is sufficient. - **`Path`** is the decoded form. `url.Parse("https://api.example.test/files/a%2Fb/meta")` gives `Path == "/files/a/b/meta"`. Notice what just happened: the escaped slash and a real separator now look identical. From `Path` alone you cannot recover the original structure. - **`RawPath`** is the escaped form, stored **only when it is needed**. Go computes what it would produce by encoding `Path`; if that matches the original text, `RawPath` stays empty because it would carry no extra information. If it differs — as with `%2F`, which Go would not emit on its own — `RawPath` keeps the original: `"/files/a%2Fb/meta"`. For the overwhelming majority of URLs, `RawPath` is empty, and code that assumes it is always populated breaks on the common case. - **`EscapedPath()`** is the method that gives you the wire form regardless. Its rule is: return `RawPath` if `RawPath` is a valid escaping of `Path`; otherwise ignore it and compute an escaping from `Path`. The word *valid* is doing real work — `RawPath` is a hint, and the moment it stops agreeing with `Path` it is discarded. ## Why the hint is discarded rather than trusted That rule is what makes the pair safe to mutate. If you assign `u.Path = "/other"` and forget that `RawPath` still holds the old escaped path, `EscapedPath()` notices that `RawPath` no longer decodes to `Path` and falls back to encoding `Path`. You get `/other`, not a stale value. The cost of that safety is the mirror image: you cannot introduce an escaped slash by writing `Path`. Setting `u.Path = "/files/a/b/meta"` will always render as three separators, because that is what the string means. To put a literal slash *inside* a segment you must go through the escaped form. ## Building a path that contains a slash The robust way is to escape each segment and let `url.Parse` fill both fields consistently: ```go raw := "/files/" + url.PathEscape("a/b") + "/meta" // /files/a%2Fb/meta u, _ := url.Parse("https://api.example.test" + raw) ``` `url.PathEscape` escapes a single path segment. It escapes `/` (to `%2F`), and it writes a space as `%20` rather than the `+` that `url.QueryEscape` produces — a `+` in a path is a literal plus character, since `url.PathUnescape` deliberately does not decode it. Escaping a *whole* path with `PathEscape` is a classic mistake: it would escape the separators too, collapsing the entire path into one segment. ## What renders on the wire `u.String()` renders the path with `EscapedPath()`. So a URL you parsed with `%2F` in it round-trips through `String()` unchanged — the parser kept `RawPath` precisely so it could. The query is handled differently: `String()` writes `RawQuery` verbatim without re-escaping. ## Where this actually bites Whenever a path segment carries user data — an identifier, a filename, a display name, a resource key from another system — that data may contain a slash, a percent sign, a space or non-ASCII runes. Three failure shapes recur. **Splitting on the wrong field.** `strings.Split(u.Path, "/")` on `/files/a/b/meta` yields four segments where the sender meant three. If segment boundaries matter, split `EscapedPath()` and decode each piece with `url.PathUnescape` afterwards — decode last, split first. **Double encoding.** Escaping something that is already escaped turns `a%2Fb` into `a%252Fb`, and the receiver decodes it once to the literal text `a%2Fb`. This is the classic symptom of a helper that escapes on the way in and a caller that escaped already. **Comparing paths for equality.** `/a%2Fb` and `/a/b` have equal `Path` values but are different URLs. If your comparison decides authorisation, caching or signature validity, compare the escaped form, not the decoded one. ## The mental model Use `Path` when you want to know what the URL *means* — it is decoded, human-readable and what most handler code should read. Use `EscapedPath()` when you need to know what the URL *says*, byte for byte. Treat `RawPath` as an implementation detail of that relationship: read it to understand behaviour, but write to it only if you know exactly why the default encoding of `Path` will not do.

  • When does url.Parse leave RawPath empty?
    Whenever the default encoding of `Path` reproduces the original text — which is most URLs. `RawPath` exists only to record an escaping Go would not have chosen itself, such as `%2F` for a slash inside a segment. Code that reads `RawPath` directly and expects it to be populated will see an empty string on ordinary paths.
  • Which of the two does u.String() use when rendering the path?
    `EscapedPath()`. That means a URL parsed with `%2F` in it renders back unchanged, because `RawPath` was preserved and is still a valid escaping of `Path`. The query is handled differently: `String()` writes `RawQuery` out verbatim rather than re-escaping it.
  • A path segment must carry a display name with spaces and non-ASCII characters. Which escape do you use?
    `url.PathEscape`, applied to that one segment. It writes a space as `%20` and percent-encodes the UTF-8 bytes of non-ASCII runes. `url.QueryEscape` would write a space as `+`, and since `url.PathUnescape` never decodes `+`, the receiver would see a literal plus in the name.
  • How do you recover the original segments when one of them may contain an escaped slash?
    Split `EscapedPath()` on `/` first, then run `url.PathUnescape` on each piece. Splitting `Path` cannot work, because by then the escaped slash has already become an ordinary separator and the segment count is wrong.

Path is the meaning and RawPath is the wording. Go keeps the original wording only when the same meaning could have been written more than one way.

saying these in an interview costs you the question

  • Says Path holds the raw text exactly as received
  • Assumes RawPath is always populated after parsing
  • Treats %2F and / as equivalent once parsed
  • Escapes a whole path with url.PathEscape, separators included
  • Splits u.Path to recover segments that may contain escaped slashes
  • Escapes a value that was already escaped, producing %252F