Standard library · Encoding and data

encoding/url

Taking a web address apart, building and editing its parameters, and resolving relative addresses.

import encoding.url

This package works with whole web addresses. Parse splits a URL such as https://user@shop.example.co.uk:8443/path?page=2#top into a URL struct with the fields scheme, userinfo, host, port, path, raw_query and fragment, and String puts it back together. Around that come helpers to edit query parameters, resolve relative links with Join, and find the registrable domain of a host.

Use it whenever you have a full address. For the a=1&b=2 part on its own, such as a form body, use encoding/query; for percent-encoding a single string, encoding.URLEncode in encoding; to fetch the address, net/http.

Raw and decoded parts

The fields hold the text exactly as it appeared, percent escapes included, so String(Parse(x)) normally gives back x. Read the human form with DecodedPath, DecodedUserinfo or Query. When you set a field yourself, put encoded text in it: escape path segments with EncodePathSegment and build queries with SetQuery.

What trips people up

  • Parse never fails. It does no validation: a port that is not a number becomes 0, and text without :// (including mailto:sara@example.com) has no scheme or host and lands whole in path. Check scheme and host yourself before trusting an address from a user.
  • Nothing is lowercased. HTTPS://Shop.Example.COM keeps its case in scheme and host, and Domain returns Example.COM. Lowercase with str.Lower before you compare hosts or domains.
  • Editing one parameter rewrites the whole query. RemoveParam, SetParam and KeepParams decode the query and encode it again: %20 becomes +, characters such as * get escaped, a bare flag becomes flag=, and repeated keys are grouped together. The meaning stays the same; the text may not.
  • Join with only ?query or #fragment loses the file name of the base, unlike a browser. See the links guide.

Guide: strip tracking parameters

Links copied from email and social media carry utm_ and click id parameters that only serve analytics. Before you store or compare links, parse them, drop those parameters with RemoveParam, clear the fragment and print the result with String. The parameters that stay keep their order.

encoding-url-guide-clean.salam
import encoding.url

func clean(raw: str): str:
    mut u := url.Parse(raw)
    names := url.ParamNames(u)
    each n in names:
        if n.starts_with("utm_") or n == "fbclid" or n == "gclid":
            _ := url.RemoveParam(u, n)
        end
    end
    u.fragment = ""
    ret url.String(u)
end

func main:
    println clean("https://shop.example/item/42?color=red&utm_source=news&utm_medium=email#reviews")
    println clean("https://blog.example/post?fbclid=IwAR0abc")
    println clean("https://example.com/search?q=salam%20lang&utm_campaign=fall&page=2&debug")
end
Outputhttps://shop.example/item/42?color=red https://blog.example/post https://example.com/search?q=salam+lang&page=2&debug=

ParamNames returns a copy of the names, so removing parameters inside the loop is safe. The fields of URL are plain strings you can set directly, as u.fragment = "" does. The last line shows the rewrite described above: salam%20lang comes out as salam+lang and the bare debug as debug=. Both mean the same to a server, but if you use the cleaned link as a cache or database key, clean every link the same way so equal links give equal text.

A crawler or a link checker finds relative links like ../api/json.html in a page and needs the full address. Join resolves a reference against the page's own URL the way a browser does: ./ and ../ move through the folders, a leading / starts at the site root, //host keeps the scheme, and a full URL replaces everything.

encoding-url-guide-links.salam
import encoding.url

func main:
    page := url.Parse("https://docs.example.com/guide/start/index.html?lang=en")
    hrefs := ["install.html", "./setup/linux.html", "../api/json.html", "/blog/", "https://other.example/x", "//cdn.example.com/app.js"]
    each h in hrefs:
        println h, "->", url.String(url.Join(page, h))
    end
end
Outputinstall.html -> https://docs.example.com/guide/start/install.html ./setup/linux.html -> https://docs.example.com/guide/start/setup/linux.html ../api/json.html -> https://docs.example.com/guide/api/json.html /blog/ -> https://docs.example.com/blog/ https://other.example/x -> https://other.example/x //cdn.example.com/app.js -> https://cdn.example.com/app.js

A reference that is only a query (?lang=fa) or only a fragment (#top) does not keep the file name of the base yet, so resolve those yourself by setting raw_query or fragment on a copy of the base.

Guide: is it the same site?

To allow redirects only within your own site, or to group links by owner, compare the registrable domain instead of the host name. Domain is the part a person registers, such as example.co.uk; PublicSuffix is the part under which anyone can register; Subdomain is what is left on the left. An address made of an IP has no domain, so compare the host name there. Host names are not case sensitive but the package keeps their case, so the program lowercases both addresses before parsing them.

encoding-url-guide-site.salam
import encoding.url
import str

func same_site(a: str, b: str): bool:
    ua := url.Parse(str.Lower(a))
    ub := url.Parse(str.Lower(b))
    if url.IsIP(ua) or url.IsIP(ub):
        ret url.Hostname(ua) == url.Hostname(ub)
    end
    ret url.Domain(ua) == url.Domain(ub)
end

func main:
    u := url.Parse("https://api.eu.shop.example.co.uk:8443/v1/orders")
    println "host:", url.Hostname(u), "port:", u.port
    println "domain:", url.Domain(u), "suffix:", url.PublicSuffix(u), "sub:", url.Subdomain(u)

    println same_site("https://www.example.com/a", "https://login.example.com/b")
    println same_site("https://WWW.Example.com/", "https://example.com/cart")
    println same_site("https://example.com/", "https://example.org/")
    println same_site("http://10.0.0.5:8080/", "http://10.0.0.5/")
end
Outputhost: api.eu.shop.example.co.uk port: 8443 domain: example.co.uk suffix: co.uk sub: api.eu.shop true true false true

The package knows the common public suffixes such as com, co.uk and com.au, not the complete Public Suffix List, so hosting platforms that give every customer a subdomain may count as one site. Check those hosts by name when it matters.

Guide: build a URL from user input

When parts of the address come from users, escape each path segment with EncodePathSegment, which also escapes / so a file name cannot add folders. Put the parameters in a QueryMap and attach it with SetQuery; Add allows the same key more than once.

encoding-url-guide-build.salam
import encoding.url
import encoding.query

func main:
    folder := "My Files"
    name := "report 2026/Q3.pdf"
    mut u := url.Parse("https://files.example.com")
    u.path = "/users/sara/" + url.EncodePathSegment(folder) + "/" + url.EncodePathSegment(name)

    mut q := query.NewQueryMap()
    query.Set(q, "download", "1")
    query.Add(q, "tag", "finance")
    query.Add(q, "tag", "سه‌ماهه")
    url.SetQuery(u, q)

    full := url.String(u)
    println full

    back := url.Parse(full)
    println "path:", url.DecodedPath(back)
    println "tags:", query.GetAll(back.raw_query, "tag")
end
Outputhttps://files.example.com/users/sara/My%20Files/report%202026%2FQ3.pdf?download=1&tag=finance&tag=%D8%B3%D9%87%E2%80%8C%D9%85%D8%A7%D9%87%D9%87 path: /users/sara/My Files/report 2026/Q3.pdf tags: ["finance", "سه‌ماهه"]

DecodedPath gives the readable path but turns %2F back into /, so it cannot tell a slash inside a name from a folder boundary. Split u.path at / first and decode each segment when you need the names exactly.

Types

URL

struct URL
A parsed web address.
NameTypeDescription
schemestr = ""The scheme, such as https.
userinfostr = ""The user name and password before the @, still encoded.
hoststr = ""The host name or numeric address.
portint = 0The port, or zero when none was given.
pathstr = ""The path, still encoded.
raw_querystr = ""The query after the ?, still encoded.
fragmentstr = ""The part after the #.
encoding-url-Parse.salam
import encoding.url
import encoding.query

func main:
    mut u := url.Parse("https://user%20a:pw@shop.example.co.uk:8443/books/%D8%B3%D9%84%D8%A7%D9%85?page=2&sort=new#top")
    println u.scheme, u.host, u.port, u.path, u.raw_query, u.fragment
    println url.Hostname(u), url.HostPort(u), url.IsIP(u)
    println url.Labels(u), url.PublicSuffix(u), url.Domain(u), url.Subdomain(u)
    println url.DecodedPath(u), url.DecodedUserinfo(u)
    println url.ParamNames(u), query.Size(url.Query(u))
    url.SetParam(u, "page", "3")
    println url.RemoveParam(u, "sort"), url.String(u)
    mut keep := Vector {} as Vector<str>
    keep.push("page")
    url.KeepParams(u, keep)
    mut q := query.NewQueryMap()
    query.Add(q, "q", "hello world")
    url.SetQuery(u, q)
    println url.String(u)
    url.ClearQuery(u)
    println url.String(u)
    println url.String(url.Join(u, "../img/a.png")), url.EncodePathSegment("a b/c")
    println url.IsIP(url.Parse("http://127.0.0.1/"))
end
Outputhttps shop.example.co.uk 8443 /books/%D8%B3%D9%84%D8%A7%D9%85 page=2&sort=new top shop.example.co.uk shop.example.co.uk:8443 false ["shop", "example", "co", "uk"] co.uk example.co.uk shop /books/سلام user a:pw ["page", "sort"] 2 true https://user%20a:pw@shop.example.co.uk:8443/books/%D8%B3%D9%84%D8%A7%D9%85?page=3#top https://user%20a:pw@shop.example.co.uk:8443/books/%D8%B3%D9%84%D8%A7%D9%85?q=hello+world#top https://user%20a:pw@shop.example.co.uk:8443/books/%D8%B3%D9%84%D8%A7%D9%85#top https://user%20a:pw@shop.example.co.uk:8443/img/a.png a%20b%2Fc true

Functions

Hostname

func Hostname(u: URL): str
The host name without the port.

The example for this item is shown with the ones above: encoding-url-Parse.salam

IsIP

func IsIP(u: URL): bool
True when the host is four dot-separated groups of digits or contains : (an IPv6 address). The digits are not range checked.

The example for this item is shown with the ones above: encoding-url-Parse.salam

Labels

func Labels(u: URL): Vector<str>
The parts of the host name, separated by dots.

The example for this item is shown with the ones above: encoding-url-Parse.salam

PublicSuffix

func PublicSuffix(u: URL): str
The public suffix of the domain, such as co.uk or ir.

The example for this item is shown with the ones above: encoding-url-Parse.salam

Domain

func Domain(u: URL): str
The registered domain: one label before the public suffix, such as example.co.uk. An IP address or a one-label host such as localhost is returned unchanged. Case is kept.

The example for this item is shown with the ones above: encoding-url-Parse.salam

Subdomain

func Subdomain(u: URL): str
Whatever comes before the registered domain, such as shop.

The example for this item is shown with the ones above: encoding-url-Parse.salam

RemoveParam

func RemoveParam(u &: URL, key: str): bool
Removes every value of a parameter and says whether it was there. Like the other parameter functions it re-encodes the rest of the query.

The example for this item is shown with the ones above: encoding-url-Parse.salam

SetParam

func SetParam(u &: URL, key: str, val: str)
Sets a parameter to one value, replacing any values it had; a new name goes at the end.

The example for this item is shown with the ones above: encoding-url-Parse.salam

KeepParams

func KeepParams(u &: URL, keep: Vector<str>)
Keeps only the parameters you name, for stripping tracking parameters.

The example for this item is shown with the ones above: encoding-url-Parse.salam

ClearQuery

func ClearQuery(u &: URL)
Removes the whole query.

The example for this item is shown with the ones above: encoding-url-Parse.salam

ParamNames

func ParamNames(u: URL): Vector<str>
The names of the parameters, in order.

The example for this item is shown with the ones above: encoding-url-Parse.salam

Parse

func Parse(raw: str): URL
Takes an address apart into its pieces. It never fails and does no validation: only text with :// (or a leading //) gets a scheme and host, and a port that is not a number becomes 0.

The example for this item is shown with the ones above: encoding-url-Parse.salam

HostPort

func HostPort(u: URL): str
The host and port together, such as shop.example.co.uk:8443.

The example for this item is shown with the ones above: encoding-url-Parse.salam

String

func String(u: URL): str
Turns the address back into text.

The example for this item is shown with the ones above: encoding-url-Parse.salam

DecodedPath

func DecodedPath(u: URL): str
The path with its percent encoding undone.

The example for this item is shown with the ones above: encoding-url-Parse.salam

DecodedUserinfo

func DecodedUserinfo(u: URL): str
The user information, decoded.

The example for this item is shown with the ones above: encoding-url-Parse.salam

EncodePathSegment

func EncodePathSegment(s: str): str
Encodes one path segment; / is encoded too.

The example for this item is shown with the ones above: encoding-url-Parse.salam

Query

func Query(u: URL): query.QueryMap
Turns the address's query into a QueryMap.

The example for this item is shown with the ones above: encoding-url-Parse.salam

SetQuery

func SetQuery(u &: URL, q: query.QueryMap)
Replaces the address's query with the given map.

The example for this item is shown with the ones above: encoding-url-Parse.salam

Join

func Join(base: URL, ref: str): URL
Resolves a relative address against a base one, the way a browser does; .. and . in the path are worked out. A reference of only ?query or #fragment drops the base file name.

The example for this item is shown with the ones above: encoding-url-Parse.salam