Go is the language of choice for building high-performance backend services, CLI tools, and infrastructure. If you're building something in Go that needs to capture website screenshots - whether that's a monitoring dashboard, a link preview service, or a reporting pipeline - this guide covers everything you need.
We'll use the GrabShot screenshot API with Go's standard library (no external dependencies required) and build up to production-ready patterns including concurrency, retries, and batch processing.
Before diving into Go, here's the equivalent curl command so you can verify your API key works:
curl "https://grabshot.dev/api/screenshot?url=https://example.com&format=png" \
-H "X-API-Key: YOUR_API_KEY" \
-o screenshot.png
Now the same thing in Go using only the standard library:
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
)
func main() {
apiKey := os.Getenv("GRABSHOT_API_KEY")
params := url.Values{}
params.Set("url", "https://example.com")
params.Set("format", "png")
params.Set("width", "1280")
params.Set("height", "720")
req, err := http.NewRequest("GET",
"https://grabshot.dev/api/screenshot?"+params.Encode(), nil)
if err != nil {
fmt.Fprintf(os.Stderr, "request error: %v\n", err)
os.Exit(1)
}
req.Header.Set("X-API-Key", apiKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Fprintf(os.Stderr, "fetch error: %v\n", err)
os.Exit(1)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
fmt.Fprintf(os.Stderr, "API error %d: %s\n", resp.StatusCode, body)
os.Exit(1)
}
f, err := os.Create("screenshot.png")
if err != nil {
fmt.Fprintf(os.Stderr, "file error: %v\n", err)
os.Exit(1)
}
defer f.Close()
written, err := io.Copy(f, resp.Body)
if err != nil {
fmt.Fprintf(os.Stderr, "write error: %v\n", err)
os.Exit(1)
}
fmt.Printf("Saved screenshot.png (%d bytes)\n", written)
}
Zero dependencies. That's the Go way.
For anything beyond a one-off script, you'll want a proper client struct with configuration, timeouts, and error handling:
package grabshot
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
type Client struct {
apiKey string
baseURL string
httpClient *http.Client
}
type ScreenshotOptions struct {
URL string
Format string // "png", "jpeg", "webp"
Width int
Height int
FullPage bool
Delay int // milliseconds to wait before capture
UserAgent string
}
func NewClient(apiKey string) *Client {
return &Client{
apiKey: apiKey,
baseURL: "https://grabshot.dev/api",
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
func (c *Client) Screenshot(ctx context.Context, opts ScreenshotOptions) ([]byte, error) {
params := url.Values{}
params.Set("url", opts.URL)
if opts.Format != "" {
params.Set("format", opts.Format)
}
if opts.Width > 0 {
params.Set("width", fmt.Sprintf("%d", opts.Width))
}
if opts.Height > 0 {
params.Set("height", fmt.Sprintf("%d", opts.Height))
}
if opts.FullPage {
params.Set("full_page", "true")
}
if opts.Delay > 0 {
params.Set("delay", fmt.Sprintf("%d", opts.Delay))
}
req, err := http.NewRequestWithContext(ctx, "GET",
c.baseURL+"/screenshot?"+params.Encode(), nil)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
req.Header.Set("X-API-Key", c.apiKey)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("executing request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, body)
}
return io.ReadAll(resp.Body)
}
Usage is clean:
client := grabshot.NewClient(os.Getenv("GRABSHOT_API_KEY"))
img, err := client.Screenshot(context.Background(), grabshot.ScreenshotOptions{
URL: "https://github.com",
Format: "webp",
Width: 1440,
})
if err != nil {
log.Fatal(err)
}
os.WriteFile("github.webp", img, 0644)
This is where Go really shines. Need to screenshot 100 URLs? Goroutines and channels make it straightforward. Here's a worker pool pattern with rate limiting:
package main
import (
"context"
"fmt"
"os"
"sync"
"time"
)
type Result struct {
URL string
Data []byte
Error error
}
func BatchScreenshots(client *grabshot.Client, urls []string, concurrency int) []Result {
var (
wg sync.WaitGroup
results = make([]Result, len(urls))
sem = make(chan struct{}, concurrency)
limiter = time.NewTicker(200 * time.Millisecond) // 5 req/sec
)
defer limiter.Stop()
for i, u := range urls {
wg.Add(1)
go func(idx int, targetURL string) {
defer wg.Done()
sem <- struct{}{} // acquire slot
defer func() { <-sem }() // release slot
<-limiter.C // rate limit
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
data, err := client.Screenshot(ctx, grabshot.ScreenshotOptions{
URL: targetURL,
Format: "png",
Width: 1280,
})
results[idx] = Result{URL: targetURL, Data: data, Error: err}
if err != nil {
fmt.Fprintf(os.Stderr, "[%d] FAIL %s: %v\n", idx, targetURL, err)
} else {
filename := fmt.Sprintf("screenshots/%03d.png", idx)
os.WriteFile(filename, data, 0644)
fmt.Printf("[%d] OK %s (%d bytes)\n", idx, targetURL, len(data))
}
}(i, u)
}
wg.Wait()
return results
}
func main() {
client := grabshot.NewClient(os.Getenv("GRABSHOT_API_KEY"))
os.MkdirAll("screenshots", 0755)
urls := []string{
"https://github.com",
"https://golang.org",
"https://news.ycombinator.com",
"https://dev.to",
"https://stackoverflow.com",
}
results := BatchScreenshots(client, urls, 3) // 3 concurrent workers
success := 0
for _, r := range results {
if r.Error == nil {
success++
}
}
fmt.Printf("\nDone: %d/%d successful\n", success, len(results))
}
Three goroutines running in parallel, rate-limited to 5 requests per second. Adjust concurrency based on your GrabShot plan limits.
Network requests fail. APIs have transient errors. Here's a retry wrapper with exponential backoff:
func (c *Client) ScreenshotWithRetry(ctx context.Context, opts ScreenshotOptions, maxRetries int) ([]byte, error) {
var lastErr error
for attempt := 0; attempt <= maxRetries; attempt++ {
if attempt > 0 {
backoff := time.Duration(1<<uint(attempt-1)) * time.Second
select {
case <-time.After(backoff):
case <-ctx.Done():
return nil, ctx.Err()
}
}
data, err := c.Screenshot(ctx, opts)
if err == nil {
return data, nil
}
lastErr = err
}
return nil, fmt.Errorf("failed after %d retries: %w", maxRetries, lastErr)
}
First retry waits 1 second, second waits 2 seconds, third waits 4 seconds. Context cancellation is respected at each step, so you won't get stuck in a retry loop if the parent context times out.
A common Go pattern is a background service that periodically screenshots a list of URLs and stores the results. Here's the core loop:
func MonitorLoop(client *grabshot.Client, urls []string, interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
// Run immediately on start
captureAll(client, urls)
for range ticker.C {
captureAll(client, urls)
}
}
func captureAll(client *grabshot.Client, urls []string) {
timestamp := time.Now().Format("2006-01-02_15-04-05")
dir := fmt.Sprintf("captures/%s", timestamp)
os.MkdirAll(dir, 0755)
for i, u := range urls {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
data, err := client.ScreenshotWithRetry(ctx, grabshot.ScreenshotOptions{
URL: u,
Format: "png",
Width: 1440,
FullPage: true,
}, 2)
cancel()
if err != nil {
log.Printf("FAIL %s: %v", u, err)
continue
}
path := fmt.Sprintf("%s/%03d.png", dir, i)
os.WriteFile(path, data, 0644)
log.Printf("OK %s -> %s", u, path)
}
}
Pair this with a simple HTTP server to view the captures, and you've got a lightweight monitoring tool. No Puppeteer installation, no Chrome dependency - just HTTP calls to GrabShot.
For reference, here are minimal examples in other languages. The same API, different syntax:
const res = await fetch(
`https://grabshot.dev/api/screenshot?url=${encodeURIComponent(url)}&format=png`,
{ headers: { 'X-API-Key': process.env.GRABSHOT_API_KEY } }
);
const buffer = Buffer.from(await res.arrayBuffer());
fs.writeFileSync('screenshot.png', buffer);
import requests, os
r = requests.get("https://grabshot.dev/api/screenshot", params={
"url": "https://example.com", "format": "png"
}, headers={"X-API-Key": os.environ["GRABSHOT_API_KEY"]})
with open("screenshot.png", "wb") as f:
f.write(r.content)
| Parameter | Type | Description |
|---|---|---|
| url | string | Target URL to screenshot (required) |
| format | string | png, jpeg, or webp |
| width | int | Viewport width in pixels (default: 1280) |
| height | int | Viewport height in pixels (default: 720) |
| full_page | bool | Capture entire scrollable page |
| delay | int | Wait N ms before capture (for dynamic content) |
| quality | int | JPEG/WebP quality 1-100 |
See the full API documentation for all available options including device emulation, custom CSS injection, and cookie handling.
Free tier includes 25 screenshots per month. No credit card required.
Get Your Free API KeyGo's standard library gives you everything you need to work with a screenshot API: HTTP client, URL encoding, file I/O, and goroutines for concurrency. No third-party packages required.
The patterns in this guide work for any volume: from a CLI tool that captures one screenshot to a monitoring service processing thousands per hour. Start with the free tier and scale up as needed.