Skip to Content
Course content

84: Building a URL Shortener Service in Go

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I worked with a developer who built a "quick and dirty" URL shortener for a promotional campaign. He used a simple auto-incrementing integer from a SQL database as the short code. It worked perfectly for the first ten minutes. Then, a curious user noticed their link ended in /102, tried /101, and realized they could just iterate backwards to scrape every single private link the company had generated for its VIP clients. It was a classic mistake: predictability is the enemy of a good shortener.

To do this right, we need a way to map a long, cumbersome URL to a short, unique string that isn't easily guessable. While you could use a UUID, those are far too long to be "short." Instead, the industry standard is usually to take a unique numeric ID and encode it using Base62. This uses characters 0-9, a-z, and A-Z, giving us 62 possible characters for each slot in our string. It keeps the URLs tight while allowing for billions of combinations.

Encoding IDs into Base62

In Go, the cleanest way to handle this is to define our alphabet as a constant string and use a loop to convert our database ID into that alphabet. I usually prefer this over using a library because the logic is straightforward and gives you total control over the character set.

package main

import "fmt"

const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

func encode(number uint64) string {
	if number == 0 {
		return string(alphabet[0])
	}

	var result []byte
	base := uint64(len(alphabet))

	for number > 0 {
		result = append(result, alphabet[number%base])
		number /= base
	}

	// We reverse the slice because we calculated the remainders 
	// from least significant to most significant.
	for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
		result[i], result[j] = result[j], result[i]
	}

	return string(result)
}

Notice that I used a []byte slice here rather than concatenating strings. In Go, strings are immutable, so adding to a string in a loop creates a new string every single time. Using a slice is significantly more efficient for the garbage collector.

Wiring the Redirection Logic

Once we have our encoding logic, we need to wrap it in an HTTP server. You'll need two main endpoints: one to take a long URL and return a short one, and another to take that short code and redirect the user to the original destination. For this example, I'm using a simple map to store the URLs in memory. In a production environment, you'd obviously swap this for Redis or Postgres, but the interface remains the same.

package main

import (
	"net/http"
	"sync"
)

type Shortener struct {
	mu    sync.RWMutex
	store map[string]string
	idCounter uint64
}

func (s *Shortener) HandleShorten(w http.ResponseWriter, r *http.Request) {
	url := r.URL.Query().Get("url")
	if url == "" {
		http.Error(w, "Missing url parameter", http.StatusBadRequest)
		return
	}

	s.mu.Lock()
	s.idCounter++
	id := s.idCounter
	s.mu.Unlock()

	shortCode := encode(id)

	s.mu.Lock()
	s.store[shortCode] = url
	s.mu.Unlock()

	fmt.Fprintf(w, "Shortened URL: %s", shortCode)
}

func (s *Shortener) HandleRedirect(w http.ResponseWriter, r *http.Request) {
	code := r.URL.Path[1:] // Strip the leading "/"

	s.mu.RLock()
	longURL, exists := s.store[code]
	s.mu.RUnlock()

	if !exists {
		http.NotFound(w, r)
		return
	}

	http.Redirect(w, r, longURL, http.StatusMovedPermanently)
}

I added a sync.RWMutex here because Go maps are not thread-safe. Since a URL shortener is read-heavy (way more people click links than create them), a RWMutex allows multiple readers to access the map simultaneously while ensuring that only one writer can modify the store at a time. If you forget this, your server will panic the moment two people try to shorten a link at the same millisecond.




📋 Practical Task

Build a Persistent Base62 URL Shortener with Custom Alias Support

Your task is to extend the provided shortener logic. Instead of relying solely on an auto-incrementing ID, modify the service so that users can optionally provide their own "custom alias" (e.g., /my-portfolio instead of /aB3x).

Requirements:

  • Modify the HandleShorten function to check for an optional alias query parameter.
  • If an alias is provided, verify that it doesn't already exist in the store before saving it.
  • If no alias is provided, fall back to the Base62 encoding logic.
  • Implement a "Delete" endpoint that removes a short code from the store using a DELETE request.
  • Ensure all map access is protected by the sync.RWMutex to prevent race conditions.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.