wikigo is a , type-safe Go SDK for the Wikimedia REST API. It wraps the API with proper typing, context propagation, error wrapping, robust rate limiting, and backoff retry logic.
- Functional Options: Modular and backward-compatible client initialization (e.g. custom subdomains, timeouts, user-agents, and retries).
- Context-Aware:
context.Contextis passed as the first parameter to all public API calls to support cancellations, timeouts, and deadlines. - Resource-Scoped Sub-Clients: Intuitive structure matching the API resource layout (
client.Pages,client.Search,client.Revisions,client.Media). - Custom Transport (
retryTransport&cacheTransport): Transparent layers handling:- Token Bucket Rate Limiting: Smooth request pacing using
golang.org/x/time/rate. - Exponential Backoff & Jitter: Random ±20% jitter to prevent thundering herd problems.
- Selective Retries: Retries only transient server errors (
HTTP 503) and rate-limit blocks (HTTP 429), respecting theRetry-Afterheader. - In-Memory Caching: Short-circuits successful
GETrequests (status 200 OK) using cloned response headers and fresh body readers, with a configurable capacity cap and pseudo-random eviction.
- Token Bucket Rate Limiting: Smooth request pacing using
- Dedicated Errors Package: Explicit error handling with status code preservation (
WikiErrorstruct) and sentinel errors for easy comparisons (errors.Is). - No External Web Frameworks: Built purely on top of Go's standard library and the official x/time package.
go get github.com/saluja-ji/wikigo├── errors/
│ └── errors.go # Sentinel errors and WikiError definition
├── models/
│ └── models.go # Strongly typed JSON representation structs
├── client.go # Core client and sub-client definitions
├── options.go # Configuration options (Functional Options)
├── transport.go # Custom retry, rate limiting, and cache http.RoundTripper
├── pages.go # Pages sub-client implementations
├── search.go # Search sub-client implementations
├── revisions.go # Revisions sub-client implementations
└── media.go # Media sub-client implementations
Here is a quick example of constructing the client and utilizing its sub-clients:
package main
import (
"context"
"errors"
"fmt"
"time"
"github.com/saluja-ji/wikigo"
wikierrors "github.com/saluja-ji/wikigo/errors"
"golang.org/x/time/rate"
)
func main() {
// 1. Initialize client using Functional Options (with optional caching)
client := wikigo.NewClient(
wikigo.WithLanguage("en"),
wikigo.WithProject("wikipedia"),
wikigo.WithTimeout(10*time.Second),
wikigo.WithRateLimit(rate.Limit(10), 10), // Limit to 10 req/sec
wikigo.WithMaxRetries(3),
wikigo.WithCache(5*time.Minute, 100), // Cache GET requests for 5 min, max 100 entries
)
ctx := context.Background()
// 2. Fetch page summary details (cached if requested again)
summary, err := client.Pages.GetSummary(ctx, "Earth")
if err != nil {
handleError(err)
return
}
fmt.Printf("Summary of %s: %s\n", summary.DisplayTitle, summary.Description)
// 3. Search for pages matching a query
searchResp, err := client.Search.Pages(ctx, "Go (programming language)", 3)
if err != nil {
handleError(err)
return
}
for _, page := range searchResp.Pages {
fmt.Printf("Search Match: %s (ID: %d)\n", page.Title, page.ID)
}
}
func handleError(err error) {
var wikiErr *wikierrors.WikiError
if errors.As(err, &wikiErr) {
fmt.Printf("API Error: [HTTP %d] %v\n", wikiErr.StatusCode, wikiErr.Err)
} else {
fmt.Printf("Standard Error: %v\n", err)
}
}Get(ctx, title): Retrieves basic page metadata (*models.Page) from/page/{title}/bareusing Core REST API.GetSummary(ctx, title): Retrieves a quick, preview-friendly summary (*models.Summary) from/page/summary/{title}using the Legacy/Summary API.
Pages(ctx, query, limit): Queries matching articles (*models.SearchResponse) containing title or content terms.
List(ctx, title, limit, olderThan): Fetches history edits list (*models.RevisionList). Does not auto-paginate. You must pass the returnedContinuetoken cursor back into consecutive list requests to retrieve older segments.
GetFile(ctx, title): Retrieves file detail metadata (*models.File) from/file/{title}for media files (the title must contain theFile:prefix).
The SDK supports transparent, thread-safe in-memory caching of successful GET requests via the functional option WithCache(ttl, maxEntries).
When enabled, the cacheTransport sits at the very outer edge of the http.Client.Transport chain. It short-circuits identical GET requests (matching URL strings) within the specified Time-To-Live (TTL).
- Safety and Clones: To prevent race conditions and cross-request side effects, header fields are copied via
resp.Header.Clone()and the response body is served through a freshio.NopCloser(bytes.NewReader(bodyBytes))reader on every cache hit. - Eviction Strategy: To manage memory usage, the cache capacity is capped. If the cache reaches the limit, a pseudo-random entry is evicted using a single-iteration
for...breakloop before saving the new entry.
A token bucket rate limiter is embedded directly inside retryTransport. The client blocks appropriately using Wait() before a request goes out:
if err := t.limiter.Wait(req.Context()); err != nil {
return nil, fmt.Errorf("rate limiter wait: %w", err)
}This is fully transparent to callers, ensuring you stay within Wikimedia limits without doing any custom interval spacing.
If a request encounters a rate limit (HTTP 429) or a service outage (HTTP 503), the client automatically starts a retry cycle:
- It parses and respects the
Retry-Afterheader if it's sent back as part of a429(supports both integer seconds and HTTP dates). - If
Retry-Afteris missing, it calculates a backoff with exponential scaling:$$\text{backoff} = 1\text{s} \times 2^{\text{attempt}}$$ - Applies a
$\pm20%$ randomized jitter to the backoff to prevent thundering herd locks. - Permanent errors like
404 Not Found,400 Bad Request, or401 Unauthorizedreturn immediately without any retry loops.
Any failure >= 400 returns a wrapped WikiError struct:
type WikiError struct {
StatusCode int // Preserves original HTTP status code (e.g. 404)
Err error // Standard Sentinel Error
Message string // Descriptive payload from the API body
}You can inspect errors directly using errors.Is to check for specific error states:
errors.Is(err, wikierrors.ErrNotFound)errors.Is(err, wikierrors.ErrRateLimited)errors.Is(err, wikierrors.ErrUnauthorized)
All unit tests compile and run deterministically against mock servers. No live requests are ever executed during the tests.
To run the unit tests:
go test -v ./...