package listing
import "encoding/base64"
func Encode(name string) string {
return base64.RawURLEncoding.EncodeToString([]byte(name))
}
func Decode(cursor string) (string, error) {
b, err := base64.RawURLEncoding.DecodeString(cursor)
if err != nil {
return "", err
}
return string(b), nil
}
package listing
import (
"os"
"sort"
"time"
)
type FileInfo struct {
Name string `json:"name"`
Size int64 `json:"size"`
IsDir bool `json:"is_dir"`
ModTime time.Time `json:"mod_time"`
}
type Page struct {
Files []FileInfo `json:"files"`
NextCursor string `json:"next_cursor"`
}
func List(dir, cursor string, limit int) (*Page, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
start := 0
if cursor != "" {
after, err := Decode(cursor)
if err != nil {
return nil, err
}
start = sort.Search(len(entries), func(i int) bool {
return entries[i].Name() > after
})
}
page := &Page{Files: []FileInfo{}}
for i := start; i < len(entries) && len(page.Files) < limit; i++ {
info, err := entries[i].Info()
if err != nil {
continue
}
page.Files = append(page.Files, FileInfo{
Name: entries[i].Name(),
Size: info.Size(),
IsDir: entries[i].IsDir(),
ModTime: info.ModTime(),
})
}
if last := start + len(page.Files); last < len(entries) && len(page.Files) > 0 {
page.NextCursor = Encode(page.Files[len(page.Files)-1].Name)
}
return page, nil
}
package listing
import (
"encoding/json"
"net/http"
"strconv"
)
const maxLimit = 100
type ListHandler struct {
Root string
}
func (h *ListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
limit := 20
if raw := q.Get("limit"); raw != "" {
if n, err := strconv.Atoi(raw); err == nil && n > 0 {
limit = n
}
}
if limit > maxLimit {
limit = maxLimit
}
page, err := List(h.Root, q.Get("cursor"), limit)
if err != nil {
if _, decErr := Decode(q.Get("cursor")); decErr != nil && q.Get("cursor") != "" {
http.Error(w, "invalid cursor", http.StatusBadRequest)
return
}
http.Error(w, "could not read directory", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(page); err != nil {
http.Error(w, "encoding failed", http.StatusInternalServerError)
}
}
This snippet shows how a directory listing endpoint is built in Go using cursor-based pagination rather than offset pagination. Offset pagination re-scans and skips rows, which is both slow and unstable when the underlying set changes between requests; a cursor encodes the exact position to resume from, so each page starts precisely where the last one ended. For a filesystem listing, the natural stable ordering is by name, so the cursor is simply the last filename returned, base64-encoded to keep it opaque and URL-safe.
The cursor.go file isolates the encoding concern. Encode wraps a filename in base64.RawURLEncoding, and Decode reverses it, returning an error on malformed input. Treating the cursor as opaque means clients never construct it themselves; they only echo back the next_cursor the server handed them, which lets the server change its internal format later without breaking callers.
The lister.go file does the actual work in List. It reads the directory with os.ReadDir, which returns entries already sorted by name — a convenient guarantee that makes the name cursor valid. When a cursor is present it decodes it and uses sort.Search to binary-search for the first entry strictly greater than the cursor value, skipping everything already seen. It then collects up to limit entries into FileInfo records, calling entry.Info() to pull size and mod time. If more entries remain past the page, it sets NextCursor from the last item's name; an empty NextCursor signals the end.
The handler.go file wires this into net/http. ListHandler parses limit and cursor from the query string, clamps limit to a sane maximum to prevent a client from requesting an unbounded page, and translates a bad cursor into a 400 rather than a 500. Success responses are marshaled with json.NewEncoder. The design is fully stateless — no server-side session holds pagination state — so it scales horizontally and survives restarts, at the cost of not supporting arbitrary jumps to page N.
Related snips
package api
import (
"net/http"
"runtime/debug"
)
Expose build metadata for debugging deploys
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
package files
import (
"context"
"time"
Presigned S3 upload URLs (AWS SDK v2)
package deps
import (
"net"
"net/http"
"time"
HTTP client tuned for production: timeouts, transport, and connection reuse
package api
import (
"io"
"net/http"
"os"
Safe multipart uploads using temp files (bounded memory)
Share this code
Here's the card — post it anywhere.