+ Integration Guide
· Updated August 28, 2026
Reddit Scraping API with Go: Complete Guide
How to search Reddit and scrape posts using Go and the FetchLayer API. Clean, idiomatic Go examples with proper error handling and struct decoding.
Written by Alex P.
- Go
- Golang
- reddit scraping
- reddit API
- API integration
This guide shows how to use the FetchLayer Reddit API with Go. All examples use the standard library — no third-party HTTP clients needed.
Setup
- Get a free API key (no credit card)
- Go 1.21+ (for
slogand modern stdlib features)
API Client
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
const baseURL = "https://api.fetchlayer.dev/reddit"
var apiKey = os.Getenv("FETCHLAYER_API_KEY")
func reddit(endpoint string, body any, result any) error {
payload, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("marshal: %w", err)
}
req, err := http.NewRequest("POST", baseURL+"/"+endpoint, bytes.NewReader(payload))
if err != nil {
return fmt.Errorf("request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("do: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("API returned %d", resp.StatusCode)
}
return json.NewDecoder(resp.Body).Decode(result)
}
Types
type RedditPost struct {
Title string `json:"title"`
Subreddit string `json:"subreddit"`
Author string `json:"author"`
Score int `json:"score"`
NumComments int `json:"numComments"`
URL string `json:"url"`
Selftext string `json:"selftext,omitempty"`
}
type SearchResponse struct {
Results []RedditPost `json:"results"`
}
type CommunityPostsResponse struct {
Subreddit string `json:"subreddit"`
Posts []RedditPost `json:"posts"`
}
type Comment struct {
Author string `json:"author"`
Body string `json:"body"`
Score int `json:"score"`
Replies []Comment `json:"replies,omitempty"`
}
type PostResponse struct {
Title string `json:"title"`
Author string `json:"author"`
Score int `json:"score"`
NumComments int `json:"numComments"`
Selftext string `json:"selftext"`
URL string `json:"url"`
Comments []Comment `json:"comments"`
}
type UserProfile struct {
Username string `json:"username"`
DisplayName string `json:"displayName"`
TotalKarma int `json:"totalKarma"`
AccountAge string `json:"accountAge"`
}
Search Reddit
func main() {
var data SearchResponse
err := reddit("search", map[string]any{
"query": "best Go web frameworks",
"sort": "top",
"limit": 10,
}, &data)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
for _, post := range data.Results {
fmt.Printf("[%d] %s — r/%s\n", post.Score, post.Title, post.Subreddit)
}
}
Run it:
FETCHLAYER_API_KEY=ss-your-key go run main.go
Get Subreddit Posts
var data CommunityPostsResponse
err := reddit("community-posts", map[string]any{
"subreddit": "golang",
"sort": "top",
"time": "week",
"limit": 20,
}, &data)
if err != nil {
log.Fatal(err)
}
for _, post := range data.Posts {
fmt.Printf("[%d] %s\n", post.Score, post.Title)
}
Scrape a Post with Comments
var thread PostResponse
err := reddit("post", map[string]any{
"url": "https://www.reddit.com/r/golang/comments/abc123/some_post/",
"pages": 2,
}, &thread)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s — %d comments\n", thread.Title, len(thread.Comments))
for _, c := range thread.Comments[:min(5, len(thread.Comments))] {
fmt.Printf(" %s: %s\n", c.Author, truncate(c.Body, 100))
}
Get User Profile
var profile UserProfile
err := reddit("user-profile", map[string]any{
"username": "spez",
}, &profile)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s — %d karma — %s\n", profile.Username, profile.TotalKarma, profile.AccountAge)
Full Example: Reddit Monitor
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
const baseURL = "https://api.fetchlayer.dev/reddit"
var apiKey = os.Getenv("FETCHLAYER_API_KEY")
type RedditPost struct {
Title string `json:"title"`
Subreddit string `json:"subreddit"`
Score int `json:"score"`
URL string `json:"url"`
}
type SearchResponse struct {
Results []RedditPost `json:"results"`
}
func reddit(endpoint string, body any, result any) error {
payload, _ := json.Marshal(body)
req, _ := http.NewRequest("POST", baseURL+"/"+endpoint, bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("API returned %d", resp.StatusCode)
}
return json.NewDecoder(resp.Body).Decode(result)
}
func main() {
keyword := "your-brand"
if len(os.Args) > 1 {
keyword = os.Args[1]
}
subreddits := []string{"startups", "SaaS", "webdev", "golang"}
fmt.Printf("Searching for %q...\n\n", keyword)
for _, sub := range subreddits {
var data SearchResponse
err := reddit("search", map[string]any{
"query": keyword,
"subreddit": sub,
"sort": "new",
"limit": 5,
}, &data)
if err != nil {
fmt.Fprintf(os.Stderr, "r/%s: %v\n", sub, err)
continue
}
if len(data.Results) > 0 {
fmt.Printf("r/%s:\n", sub)
for _, post := range data.Results {
fmt.Printf(" [%d] %s\n", post.Score, post.Title)
fmt.Printf(" %s\n\n", post.URL)
}
}
}
}
FETCHLAYER_API_KEY=ss-your-key go run main.go "react vs svelte"
Handling Errors and Rate Limits
The client above returns a generic error for any non-200 response, which is enough for a one-off script but not for a long-running job. FetchLayer returns standard HTTP status codes: 401 for a missing or invalid API key, 400 when a required field like query or subreddit is missing, and 429 once you exceed your plan’s request rate. Distinguish them so the caller can decide what to do about each:
import (
"errors"
"strconv"
"time"
)
var ErrRateLimited = errors.New("rate limited")
func reddit(endpoint string, body any, result any) error {
for attempt := 0; attempt <= 2; attempt++ {
payload, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("marshal: %w", err)
}
req, err := http.NewRequest("POST", baseURL+"/"+endpoint, bytes.NewReader(payload))
if err != nil {
return fmt.Errorf("request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("do: %w", err)
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
return json.NewDecoder(resp.Body).Decode(result)
case http.StatusUnauthorized:
return fmt.Errorf("invalid or missing API key")
case http.StatusBadRequest:
return fmt.Errorf("bad request to /%s", endpoint)
case http.StatusTooManyRequests:
if attempt == 2 {
return ErrRateLimited
}
wait, _ := strconv.Atoi(resp.Header.Get("Retry-After"))
if wait == 0 {
wait = 5
}
time.Sleep(time.Duration(wait) * time.Second)
continue
default:
return fmt.Errorf("API returned %d", resp.StatusCode)
}
}
return ErrRateLimited
}
A 429 mid-run is expected behavior on the free tier, not a bug — the retry above respects Retry-After rather than hammering the endpoint again immediately. Check for ErrRateLimited with errors.Is in the caller so it’s handled differently from an auth or validation failure.
Context timeouts
An http.Client without a timeout will wait indefinitely. In a scheduled job that means a hung request keeps the process alive until something else kills it. Set a client-level timeout and pass a per-request context:
var client = &http.Client{Timeout: 60 * time.Second}
func redditCtx(ctx context.Context, endpoint string, body any, out any) error {
payload, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("marshal request: %w", err)
}
req, err := http.NewRequestWithContext(
ctx, http.MethodPost,
baseURL+"/"+endpoint,
bytes.NewReader(payload),
)
if err != nil {
return fmt.Errorf("build request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("call %s: %w", endpoint, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
msg, _ := io.ReadAll(resp.Body)
return fmt.Errorf("%s returned %d: %s", endpoint, resp.StatusCode, msg)
}
return json.NewDecoder(resp.Body).Decode(out)
}
Using NewRequestWithContext rather than NewRequest is what makes cancellation actually propagate — the caller’s ctx can abort an in-flight request, which matters when a parent operation is already doomed:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
var result SearchResponse
if err := redditCtx(ctx, "search", map[string]any{
"query": "golang concurrency",
"sort": "top",
"limit": 10,
}, &result); err != nil {
log.Fatal(err)
}
Concurrent scans with errgroup
Go’s concurrency is the reason to use it for this kind of work. errgroup gives you goroutines with error propagation and a built-in concurrency limit:
go get golang.org/x/sync/errgroup
import "golang.org/x/sync/errgroup"
type subredditResult struct {
Subreddit string
Posts []Post
}
func scanSubreddits(ctx context.Context, keyword string, subs []string) ([]subredditResult, error) {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(3) // cap in-flight requests so we don't trigger a 429
results := make([]subredditResult, len(subs))
for i, sub := range subs {
i, sub := i, sub // capture per iteration (unnecessary on Go 1.22+)
g.Go(func() error {
var resp SearchResponse
err := redditCtx(ctx, "search", map[string]any{
"query": keyword,
"subreddit": sub,
"sort": "new",
"limit": 10,
}, &resp)
if err != nil {
return fmt.Errorf("r/%s: %w", sub, err)
}
results[i] = subredditResult{Subreddit: sub, Posts: resp.Results}
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
Writing into results[i] by index needs no mutex — each goroutine owns exactly one slot, so there’s no shared write. That’s cleaner and faster than appending under a lock.
Note that errgroup.WithContext cancels the shared context on the first error, so a single failure aborts the rest. That’s right for a pipeline where partial data is useless. If you’d rather collect what succeeded, use a plain sync.WaitGroup and store errors per slot instead:
type outcome struct {
Result subredditResult
Err error
}
func scanTolerant(ctx context.Context, keyword string, subs []string) []outcome {
out := make([]outcome, len(subs))
sem := make(chan struct{}, 3)
var wg sync.WaitGroup
for i, sub := range subs {
wg.Add(1)
go func(i int, sub string) {
defer wg.Done()
sem <- struct{}{} // acquire
defer func() { <-sem }() // release
var resp SearchResponse
if err := redditCtx(ctx, "search", map[string]any{
"query": keyword, "subreddit": sub, "sort": "new", "limit": 10,
}, &resp); err != nil {
out[i] = outcome{Err: fmt.Errorf("r/%s: %w", sub, err)}
return
}
out[i] = outcome{Result: subredditResult{Subreddit: sub, Posts: resp.Results}}
}(i, sub)
}
wg.Wait()
return out
}
The buffered channel is Go’s idiomatic semaphore — capacity three means at most three goroutines are past the acquire at any moment.
What’s Next
- Reddit API with Node.js — JavaScript version
- Reddit API with TypeScript — typed JS version
- Reddit API with Bun — Bun runtime
- How to Scrape Reddit in 2026 — all scraping methods
- FetchLayer API Reference