Skip to content

URL Encode / Decode — Go source

Percent-encode or decode URLs and query parameters. Choose component (encodeURIComponent) or full-URI (encodeURI) mode. 100% client-side.

This is the Go implementation — the same logic the interactive tool runs, in a shareable, citable form.

// URL encode / decode — component-level (encodeURIComponent) and full URI (encodeURI).
//
// Language: Go
// CosmoDev polyglot showcase port of the `url-encode` tool.
// Ported from src/tools/UrlEncodeTool.tsx — display source, part of CosmoDev's
// polyglot tool pages.
//
// Explicit percent-encoding matching JavaScript's encodeURIComponent/encodeURI.
// Go strings are UTF-8 byte sequences, so we walk raw bytes: each byte outside
// the safe set becomes %XX (uppercase hex). Component scope leaves
// A-Za-z0-9-_.!~*'() unescaped; full URI scope additionally leaves the
// reserved set ;,/?:@&=+$# unescaped. Decode reverses this, returning an
// error on malformed % sequences; full URI leaves encoded reserved bytes
// intact. UTF-8 validity falls out of converting the decoded byte slice back
// to a string. net/url's QueryEscape uses form-encoding (+ for space), so it
// is not a match — this is hand-rolled for exact parity.
package main

import (
	"errors"
	"fmt"
	"strings"
)

var errMalformed = errors.New("malformed URI sequence")

func isComponentSafe(c byte) bool {
	switch {
	case 'A' <= c && c <= 'Z', 'a' <= c && c <= 'z', '0' <= c && c <= '9':
		return true
	}
	return strings.IndexByte("-_.!~*'()", c) >= 0
}

func isURIExtra(c byte) bool {
	return strings.IndexByte(";,/?:@&=+$#", c) >= 0
}

const hexDigits = "0123456789ABCDEF"

// Encode percent-encodes s. When fullURI is true it matches encodeURI;
// otherwise it matches encodeURIComponent.
func Encode(s string, fullURI bool) string {
	var b strings.Builder
	for i := 0; i < len(s); i++ {
		c := s[i]
		if isComponentSafe(c) || (fullURI && isURIExtra(c)) {
			b.WriteByte(c)
			continue
		}
		b.WriteByte('%')
		b.WriteByte(hexDigits[c>>4])
		b.WriteByte(hexDigits[c&0x0f])
	}
	return b.String()
}

func unhex(c byte) (byte, bool) {
	switch {
	case '0' <= c && c <= '9':
		return c - '0', true
	case 'a' <= c && c <= 'f':
		return c - 'a' + 10, true
	case 'A' <= c && c <= 'F':
		return c - 'A' + 10, true
	}
	return 0, false
}

// unhexPair reads the two hex digits after the '%' at index i.
func unhexPair(s string, i int) (byte, error) {
	if i+2 >= len(s) {
		return 0, errMalformed
	}
	hi, ok1 := unhex(s[i+1])
	lo, ok2 := unhex(s[i+2])
	if !ok1 || !ok2 {
		return 0, errMalformed
	}
	return hi<<4 | lo, nil
}

// Decode reverses percent-encoding. When fullURI is true it matches decodeURI
// (encoded reserved characters are preserved); otherwise decodeURIComponent.
func Decode(s string, fullURI bool) (string, error) {
	var out []byte
	i := 0
	for i < len(s) {
		if s[i] != '%' {
			out = append(out, s[i])
			i++
			continue
		}
		b, err := unhexPair(s, i)
		if err != nil {
			return "", err
		}
		if b < 0x80 {
			if fullURI && strings.IndexByte(";/?:@&=+$#", b) >= 0 {
				out = append(out, s[i], s[i+1], s[i+2]) // keep encoded reserved byte
			} else {
				out = append(out, b)
			}
			i += 3
			continue
		}
		var seqLen int
		switch {
		case b&0xe0 == 0xc0:
			seqLen = 2
		case b&0xf0 == 0xe0:
			seqLen = 3
		case b&0xf8 == 0xf0:
			seqLen = 4
		default:
			return "", errMalformed
		}
		buf := make([]byte, 0, seqLen)
		buf = append(buf, b)
		for n := 1; n < seqLen; n++ {
			pi := i + 3*n
			if pi >= len(s) || s[pi] != '%' {
				return "", errMalformed
			}
			cb, err := unhexPair(s, pi)
			if err != nil {
				return "", err
			}
			if cb&0xc0 != 0x80 {
				return "", errMalformed
			}
			buf = append(buf, cb)
		}
		out = append(out, buf...)
		i += 3 * seqLen
	}
	return string(out), nil
}

func main() {
	fmt.Println(Encode("hello world & café", false))
	out, err := Decode("hello%20world%20%26%20caf%C3%A9", false)
	fmt.Println(out, err)
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →