Introduction
Go is a natural fit for domain intelligence services: concurrent workers, predictable timeouts, and small static binaries that run in CI or Kubernetes. Security scanners, portfolio monitors and signup-risk services all need structured WHOIS data — registrar, dates, EPP status, nameservers — without maintaining hundreds of registry text parsers.
A REST WHOIS API keeps that complexity out of your Go code. You send a GET request with an API key and decode normalized JSON. This guide shows how to integrate the WhoisJSON WHOIS API into Go — from a reusable net/http client to HTTP 429 retries and a worker pool for bulk domain scans.
Companion guides already cover Python and Node.js. The endpoint and response schema are identical across languages.
Prerequisites
- Go 1.21+ — examples use the standard library only (
net/http,encoding/json,context). - A free WhoisJSON API key — sign up here for 1,000 free requests per month. No credit card required.
- Familiarity with
context.Context— used for timeouts and cancellation on every request.
Basic WHOIS Lookup in Go
Authenticate with Authorization: TOKEN=YOUR_API_KEY. The base URL is https://whoisjson.com/api/v1.
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)
const baseURL = "https://whoisjson.com/api/v1"
// Full struct definitions are shown in the next section.
// For a minimal first run, decode into map[string]any instead.
type WhoisClient struct {
APIKey string
HTTP *http.Client
}
func NewWhoisClient(apiKey string) *WhoisClient {
return &WhoisClient{
APIKey: apiKey,
HTTP: &http.Client{
Timeout: 15 * time.Second,
},
}
}
func (c *WhoisClient) Lookup(ctx context.Context, domain string) (map[string]any, error) {
u, err := url.Parse(baseURL + "/whois")
if err != nil {
return nil, err
}
q := u.Query()
q.Set("domain", domain)
u.RawQuery = q.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "TOKEN="+c.APIKey)
res, err := c.HTTP.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("whois %s: HTTP %d: %s", domain, res.StatusCode, string(body))
}
var data map[string]any
if err := json.Unmarshal(body, &data); err != nil {
return nil, err
}
return data, nil
}
func main() {
client := NewWhoisClient(os.Getenv("WHOISJSON_API_KEY"))
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
data, err := client.Lookup(ctx, "example.com")
if err != nil {
panic(err)
}
fmt.Println(data["name"], data["expires"])
} Remaining-Requests header. Read it with res.Header.Get("Remaining-Requests") to track quota in your services.Decode Into Typed Structs
For production code, prefer structs over code map[string]any | . Optional RDAP enrichment fields ( code age | , code expiration | , code statusAnalysis | ) may be absent on some TLDs — use pointers or code omitempty | .
type Registrar struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
URL string `json:"url,omitempty"`
Phone string `json:"phone,omitempty"`
}
type Age struct {
Days int `json:"days"`
Months int `json:"months"`
Years int `json:"years"`
IsNewlyRegistered bool `json:"isNewlyRegistered"`
IsYoung bool `json:"isYoung"`
}
type Expiration struct {
DaysLeft int `json:"daysLeft"`
IsExpiringSoon bool `json:"isExpiringSoon"`
IsExpired bool `json:"isExpired"`
}
type WhoisResponse struct {
Name string `json:"name"`
Registered bool `json:"registered"`
Source string `json:"source"`
Created string `json:"created"`
Changed string `json:"changed"`
Expires string `json:"expires"`
Status []string `json:"status"`
Nameserver []string `json:"nameserver"`
Registrar *Registrar `json:"registrar,omitempty"`
Age *Age `json:"age,omitempty"`
Expiration *Expiration `json:"expiration,omitempty"`
}
func (c *WhoisClient) LookupTyped(ctx context.Context, domain string) (*WhoisResponse, error) {
raw, err := c.Lookup(ctx, domain)
if err != nil {
return nil, err
}
b, err := json.Marshal(raw)
if err != nil {
return nil, err
}
var w WhoisResponse
if err := json.Unmarshal(b, &w); err != nil {
return nil, err
}
return &w, nil
}For every field in the payload, see the WHOIS JSON field reference. When contacts are missing, follow working with redacted WHOIS.
Safe Field Access
Always nil-check nested objects. Unregistered domains and privacy-protected records omit fields your code might assume exist.
func summarize(w *WhoisResponse) {
registrar := "N/A"
if w.Registrar != nil && w.Registrar.Name != "" {
registrar = w.Registrar.Name
}
fmt.Printf("Domain : %s\n", w.Name)
fmt.Printf("Registered: %v\n", w.Registered)
fmt.Printf("Registrar : %s\n", registrar)
fmt.Printf("Created : %s\n", w.Created)
fmt.Printf("Expires : %s\n", w.Expires)
fmt.Printf("Source : %s\n", w.Source)
if w.Age != nil {
fmt.Printf("Age days : %d (new=%v)\n", w.Age.Days, w.Age.IsNewlyRegistered)
}
if w.Expiration != nil {
fmt.Printf("Days left : %d (soon=%v)\n", w.Expiration.DaysLeft, w.Expiration.IsExpiringSoon)
}
}
Handle HTTP 429 and Retries
Rate limits return429 Too Many Requests. Treat them as backpressure, not WHOIS data errors. Retry with exponential backoff and a hard attempt limit. Full patterns are documented in the rate limits guide.
func (c *WhoisClient) LookupWithRetry(ctx context.Context, domain string, maxAttempts int) (*WhoisResponse, error) {
var lastErr error
for attempt := 1; attempt <= maxAttempts; attempt++ {
w, err := c.LookupTyped(ctx, domain)
if err == nil {
return w, nil
}
lastErr = err
if !isRetryable(err) || attempt == maxAttempts {
break
}
delay := time.Duration(attempt*attempt) * time.Second
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(delay):
}
}
return nil, lastErr
}
func isRetryable(err error) bool {
msg := err.Error()
return strings.Contains(msg, "HTTP 429") ||
strings.Contains(msg, "HTTP 502") ||
strings.Contains(msg, "HTTP 503") ||
strings.Contains(msg, "HTTP 504")
}Do not retry code 400 | or code 401 | — fix validation or authentication instead.
Bulk Lookups with a Worker Pool
For portfolios or threat feeds, bound concurrency. A small worker pool respects rate limits better than unbounded goroutines.
type Result struct {
Domain string
Whois *WhoisResponse
Err error
}
func BulkLookup(ctx context.Context, client *WhoisClient, domains []string, workers int) []Result {
jobs := make(chan string)
out := make(chan Result, len(domains))
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for domain := range jobs {
w, err := client.LookupWithRetry(ctx, domain, 4)
out <- Result{Domain: domain, Whois: w, Err: err}
}
}()
}
go func() {
for _, d := range domains {
jobs <- d
}
close(jobs)
wg.Wait()
close(out)
}()
results := make([]Result, 0, len(domains))
for r := range out {
results = append(results, r)
}
return results
}Start with 3–5 workers on the free tier. Increase carefully on paid plans — see bulk WHOIS lookups for plan selection and pacing.
Use Case: Flag Newly Registered Domains
Use RDAP age.isNewlyRegistered when present. This mirrors the newly registered domains threat-signal guide.
func isNewlyRegistered(w *WhoisResponse) bool {
if w == nil || !w.Registered {
return false
}
if w.Age != nil {
return w.Age.IsNewlyRegistered
}
return false
}
func scanNRD(ctx context.Context, client *WhoisClient, domains []string) {
for _, r := range BulkLookup(ctx, client, domains, 4) {
if r.Err != nil {
fmt.Printf("%s ERROR %v\n", r.Domain, r.Err)
continue
}
if isNewlyRegistered(r.Whois) {
fmt.Printf("%s NEWLY_REGISTERED age=%dd\n", r.Domain, r.Whois.Age.Days)
}
}
}
Limits and Best Practices
- Always pass a
context.Contextwith a timeout — never rely only onhttp.Client.Timeoutfor cancel trees. - Store the API key in an environment variable or secret manager, not in source.
- Reuse one
*http.Client(connection pooling) across lookups. - Treat empty contacts as normal after GDPR — see redacted WHOIS guidance.
- Prefer monitoring for continuous drift; use Go clients for pipelines, CI and on-demand enrichment.
Frequently Asked Questions
How do I query WHOIS data in Go?
Send an authenticated GET to https://whoisjson.com/api/v1/whois?domain=example.com with Authorization: TOKEN=YOUR_API_KEY, then decode the JSON response with encoding/json.
Do I need a third-party Go WHOIS library?
No. The standard library (net/http and encoding/json) is enough. A REST API avoids maintaining registrar-specific text parsers in Go.
How should Go code handle HTTP 429 from a WHOIS API?
Retry with exponential backoff and a maximum attempt count. Do not treat 429 as “domain not found”. Read Remaining-Requests when available.
How can I look up many domains concurrently in Go?
Use a fixed-size worker pool of goroutines with a job channel. Bound concurrency to stay within your plan rate limit.
Is there a free tier for Go integrations?
Yes. WhoisJSON includes 1,000 free requests per month across WHOIS, DNS, SSL, availability and related endpoints — no credit card required.
Conclusion
Go and a REST WHOIS API are a strong combination for domain intelligence: typed responses, context-aware timeouts, and controlled concurrency. Start with a small client wrapper, decode into structs, retry 429s carefully, then scale with a worker pool when you need bulk coverage.
The WhoisJSON API handles TLD routing, RDAP fallback, and structured JSON — so your Go code stays focused on workers, timeouts and business logic rather than registry parsers.
Start querying WHOIS data in Go
Create a free account and get 1,000 API requests per month. No credit card required.