1 The Problem
We want a URL shortener: give it a long link, it returns a short code; give back the code, it returns the original link. It teaches two-way lookups (code↔URL), generating unique keys, and persisting a small store — the core of any link service.
2 How to Think About It
Think about the mapping, before any code:
3 The Build — explained part by part
Here is the complete shortener. Each part is explained below.
package main
import (
"crypto/rand"
"encoding/json"
"fmt"
"math/big"
"os"
)
const (
file = "links.json"
codeChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
codeLength = 6
)
func load() map[string]string {
data, err := os.ReadFile(file)
if err != nil {
return map[string]string{}
}
links := map[string]string{}
if err := json.Unmarshal(data, &links); err != nil {
return map[string]string{}
}
return links
}
func save(links map[string]string) {
data, _ := json.Marshal(links)
os.WriteFile(file, data, 0644)
}
// makeCode builds a random 6-character code from letters and digits, using
// crypto/rand — the secure choice, same lesson as the password generator.
func makeCode() string {
b := make([]byte, codeLength)
for i := range b {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(codeChars))))
b[i] = codeChars[n.Int64()]
}
return string(b)
}
// shorten makes a new code, retrying on a clash, and stores the mapping.
func shorten(links map[string]string, url string) string {
code := makeCode()
for {
if _, exists := links[code]; !exists {
break
}
code = makeCode()
}
links[code] = url
return code
}
// expand looks up a code. The second return value is false for an unknown
// code — Go's comma-ok idiom, used instead of Python's links.get(code).
func expand(links map[string]string, code string) (string, bool) {
url, ok := links[code]
return url, ok
}
func main() {
links := load()
code := shorten(links, "https://example.com/a/very/long/link")
save(links)
fmt.Printf("Short code: %s\n", code)
if url, ok := expand(links, code); ok {
fmt.Printf("Expands to: %s\n", url)
}
}
makeCode() — build a random 6-character code from letters and digits using
crypto/rand — the same secure-randomness lesson as the password generator, applied to a different problem.for { if _, exists := links[code]; !exists { break }; code = makeCode() } — the safety step: if the random code happens to already exist, generate another and check again. This guarantees every code is unique before it is ever stored.
url, ok := links[code] — Go’s comma-ok idiom for map lookups. Reading a missing key from a Go map never panics — it just returns the zero value (
"" for a string) — but that is ambiguous if an empty string is ever a valid value, so ok tells you for certain whether the key was actually present. This is Go’s answer to Python’s links.get(code) returning None.
url := links[code] and treating an empty result as “not found” — a genuinely empty stored value would look identical to a missing key.url, ok := links[code] and check ok.shorten does.save(links) before adding the new mapping to the map — order matters, since maps in Go are reference types but the write still has to happen first.main above.4 Test & Prove Each Part
We test the round-trip (shorten then expand) and that codes are unique and the right length.
package main
import "testing"
func TestRoundTrip(t *testing.T) {
links := map[string]string{}
code := shorten(links, "https://example.com")
got, ok := expand(links, code)
if !ok {
t.Fatalf("expand(%q) not found; want it present", code)
}
if got != "https://example.com" {
t.Errorf("expand(%q) = %q; want %q", code, got, "https://example.com")
}
}
func TestCodeLength(t *testing.T) {
if got := len(makeCode()); got != 6 {
t.Errorf("len(makeCode()) = %d; want 6", got)
}
}
func TestUnknownCode(t *testing.T) {
_, ok := expand(map[string]string{}, "missing")
if ok {
t.Errorf("expand on empty map returned ok=true; want false")
}
}
Run with go test -v ./.... The round-trip test is the most important: it proves the two halves (shorten and expand) fit together. Testing a there-and-back journey is a powerful way to verify any mapping, in any language.
5 The Interface
What it expects
shorten(links, "https://example.com/very/long")What it returns
Short code: T0Gl9y
Expands to: https://example.com/very/long6 Run It & Automate It
Save the code as shortener.go and run it with go run shortener.go — Go compiles and executes in one step, no separate build needed while you are experimenting.
go run shortener.goShortens a link and expands it back. Links are saved in
links.json, next to the program.A CI tool like Jenkins runs go test automatically whenever the code changes — every line below has a plain explanation.
Short code: T0Gl9y
Expands to: https://example.com/a/very/long/linkexpand’s result without looking at the second (ok) return value. Always destructure both: url, ok := expand(...).save(links) after shortening, before the program exits.// Jenkinsfile — runs the tests automatically every time the code changes.
pipeline {
agent any // run on any available machine
stages {
stage('Get the code') {
steps { checkout scm } // download the latest code
}
stage('Set up Go') {
steps {
sh 'go version' // confirm Go is installed
sh 'test -f go.mod || go mod init url_shortener' // create a module if none exists
}
}
stage('Run the tests') {
steps {
sh 'go vet ./...' // catch obvious mistakes before running
sh 'go test -v ./...' // run every test, show each result
}
}
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
- Custom codes. Let users pick their own code if it is free, checking with the comma-ok idiom. (Teaches: checking availability.)
- Click counts. Store a small struct
{URL string; Clicks int}as the map value instead of a bare string. (Teaches: richer values.) - Make it a web service. Combine with the REST API project so
GET /{code}redirects. (Teaches: connecting projects.)
.get(). This key-to-value mapping is the basis of caches, stores, and link services. Related: Maps, JSON & Encoding.