Step 2 — The store, in memory
Somewhere to keep links, so you can build the API before adding a database.
- Build an in-memory link store behind a small set of methods.
- Explain why an RWMutex suits a store that is read far more than written.
- Return a sentinel error such as
ErrNotFoundfrom the store.
Every command for this step
mkdir -p internal/model internal/store internal/service internal/transport/webtouch internal/model/link.go internal/store/memory.go# after writing themgo build ./...go vet ./...# a quick test of the storetouch internal/store/memory_test.gogo test ./internal/store/ -run TestMemory -vWhy memory first
You could add Postgres now. Doing it later is better: an in-memory store takes ten minutes, has no setup, and lets you get the whole API working before you introduce anything that can fail for reasons unrelated to your code.
It also forces a useful shape. If the rest of your program only talks to the store through a few methods, swapping them for SQL later touches only the store package. Step 5 does that.
Two new folders
File 1 — internal/model/link.go
The type first. The struct tags decide what the JSON looks like, so this file also defines your API’s response shape.
package modelimport ( "errors" "time")// Link is the only type this service has. It lives in model/ because it is// what the application is ABOUT — no HTTP, no SQL, just the shape.type Link struct { Code string `json:"code"` LongURL string `json:"long_url"` Clicks int64 `json:"clicks"` CreatedAt time.Time `json:"created_at"`}// The errors this service can return. The transport layer turns these into// status codes, so it never needs to know how links are stored.var ( ErrNotFound = errors.New("link not found") ErrCodeTaken = errors.New("code already in use") ErrValidation = errors.New("invalid input"))File 2 — internal/store/memory.go
The store keeps links in a map. The mutex is not optional: an HTTP server handles each request in its own goroutine, so two requests can write to that map at the same moment. Go usually detects that and crashes the process.
package storeimport ( "context" "sync" "github.com/you/shortener/internal/model")// Memory keeps links in a map. Everything is lost on restart, which is fine// until step 5 swaps this for Postgres.type Memory struct { mu sync.RWMutex // guards the map below links map[string]*model.Link // keyed by code}func NewMemory() *Memory { return &Memory{links: map[string]*model.Link{}}}func (s *Memory) Save(ctx context.Context, l *model.Link) error { s.mu.Lock() // a write: exclusive defer s.mu.Unlock() if _, exists := s.links[l.Code]; exists { return model.ErrCodeTaken } s.links[l.Code] = l return nil}func (s *Memory) ByCode(ctx context.Context, code string) (*model.Link, error) { s.mu.RLock() // a read: many at once are fine defer s.mu.RUnlock() l, ok := s.links[code] if !ok { return nil, model.ErrNotFound // absent is not the same as an empty link } return l, nil}Why RWMutex here
A mutex lets one goroutine at a time into a section of code. An RWMutex is a mutex that also allows many readers at once, as long as nobody is writing.
That fits a URL shortener: every redirect is a read, and creating a link is comparatively rare. When reads heavily outnumber writes, RWMutex lets them run in parallel instead of queueing. For a map lookup this short the gain is small, so measure before you rely on it.
recover cannot catch it.- Build against memory first. A database is a step you take once the API already works.
- Keep the type and its errors in
model, and the code that stores it instore. - Every request runs in its own goroutine, so a shared map needs a lock.
RWMutexwhen reads heavily outnumber writes, which is the case here.
ErrNotFound.- 1Add a
Delete(code string) errormethod that returnsErrNotFoundwhen the code is not there. - 2Remove the mutex, then run
go test -racewith two goroutines writing at once. Read the report. - 3Change
ByCodeto return(Link, bool)instead of an error, then decide which version you prefer and why.