Help us shape the future of Go! Take the 2026 Go Developer Survey

reddit

package module
v0.10.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 4, 2026 License: BSD-3-Clause Imports: 11 Imported by: 0

README

go-reddit/reddit

A dependency-free (stdlib-only, CGO=0) Go client for Reddit's public JSON API. It powers the go-reddit/reader macOS app and the go-ruby-reddit Ruby binding, but stands on its own.

  • Zero dependencies — only the Go standard library, so it builds for every Go target (all six 64-bit arches) with CGO_ENABLED=0.
  • Anonymous or OAuth — read public listings anonymously, or authenticate with the app-only (client_credentials) or script (password) grant. Reddit increasingly blocks the anonymous .json endpoints with a 403; OAuth is the reliable path for server-side and datacenter callers.
  • 100% test coverage, including every error branch.

Install

go get github.com/go-reddit/reddit

Usage

package main

import (
	"context"
	"fmt"

	"github.com/go-reddit/reddit"
)

func main() {
	// A descriptive User-Agent is required; Reddit rate-limits generic ones.
	c := reddit.NewClient(reddit.WithUserAgent("myapp/1.0 (by /u/you)"))

	page, err := c.Subreddit(context.Background(), "golang", reddit.SortHot,
		reddit.ListingOptions{Limit: 25})
	if err != nil {
		panic(err)
	}
	for _, p := range page.Posts {
		fmt.Printf("%5d  %s  (u/%s)\n", p.Score, p.Title, p.Author)
	}

	// Page with the returned cursor:
	next, _ := c.Subreddit(context.Background(), "golang", reddit.SortHot,
		reddit.ListingOptions{Limit: 25, After: page.After})
	_ = next
}

Register an app at https://www.reddit.com/prefs/apps to get a client id and secret, then:

// Application-only: read public content, no user account.
c := reddit.NewClient(
	reddit.WithUserAgent("myapp/1.0 (by /u/you)"),
	reddit.WithOAuth(clientID, clientSecret),
)

// Or the "script app" grant, acting as a specific user:
c = reddit.NewClient(
	reddit.WithUserAgent("myapp/1.0 (by /u/you)"),
	reddit.WithOAuthScript(clientID, clientSecret, username, password),
)

The client fetches and caches the bearer token automatically, refreshing it before expiry, and routes requests through oauth.reddit.com.

Comments
res, _ := c.Comments(context.Background(), "golang", "abc123",
	reddit.ListingOptions{Limit: 100})
fmt.Println(res.Post.Title)
for _, cm := range res.Comments {   // Replies nested recursively
	fmt.Printf("u/%s: %s\n", cm.Author, cm.Body)
}

API

Method Endpoint
Subreddit(ctx, name, sort, opts) r/<name>/<sort>.json
Frontpage(ctx, sort, opts) /<sort>.json (logged-out front page)
Comments(ctx, subreddit, id, opts) r/<sub>/comments/<id>.json

Sorts: SortHot, SortNew, SortTop, SortRising, SortControvers, SortBest. Time windows for top/controversial: TimeHour … TimeAll.

Non-2xx responses surface as a typed *reddit.APIError carrying the status code, so callers can distinguish a 429 (rate limit) from a 403 (blocked) or a 404 (gone).

License

BSD-3-Clause — see LICENSE. Copyright the go-reddit/reddit authors.

Documentation

Overview

Package reddit is a dependency-free (stdlib-only, CGO=0) client for Reddit's public read-only JSON API.

It targets the anonymous endpoints that back every subreddit and post page — appending ".json" to any Reddit URL returns the same listing the site renders. No OAuth, no API key: the only requirement Reddit enforces on anonymous traffic is a descriptive, non-generic User-Agent (a missing or browser-mimicking UA earns a 429). Set one with WithUserAgent.

The client is deliberately small and allocation-light so it can be reused unchanged from a native binary, a wasm proxy, or a Ruby binding (github.com/go-ruby-reddit). It builds for every Go target, cgo disabled.

Index

Constants

View Source
const DefaultBaseURL = "https://www.reddit.com"

DefaultBaseURL is Reddit's canonical JSON host. The "www" host (rather than "old" or "api") returns the modern listing schema this package decodes.

View Source
const DefaultRequestsPerMinute = 60

DefaultRequestsPerMinute is the self-imposed request rate a fresh client uses until Reddit's own X-Ratelimit headers refine it. 60/min (one request per second) sits at Reddit's documented OAuth budget and well under what the anonymous JSON endpoints tolerate, so a first burst does not trip a 429.

View Source
const DefaultUserAgent = "go-reddit/reddit (+https://github.com/go-reddit/reddit)"

DefaultUserAgent identifies this library. Reddit rate-limits anonymous requests that omit a UA or impersonate a browser, so callers are strongly encouraged to override this with WithUserAgent using their own contact string (Reddit's guidance: "<platform>:<app-id>:<version> (by /u/<name>)").

Variables

This section is empty.

Functions

This section is empty.

Types

type APIError

type APIError struct {
	StatusCode int
	Status     string
	Body       string
}

APIError is returned when Reddit responds with a non-2xx status. It carries the HTTP status code and a short snippet of the body for diagnosis (Reddit serves an HTML or JSON error page on 403/429/5xx).

func (*APIError) Error

func (e *APIError) Error() string

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client fetches listings and comments from Reddit's public JSON API. The zero value is not usable; construct one with NewClient. A Client is safe for concurrent use as long as the underlying http.Client is.

func NewClient

func NewClient(opts ...Option) *Client

NewClient returns a Client with sensible defaults (a 30-second HTTP timeout, the canonical base URL, DefaultUserAgent, and a DefaultRequestsPerMinute rate limiter), then applies each Option in order.

func (*Client) Comments

func (c *Client) Comments(ctx context.Context, subreddit, id string, opts ListingOptions) (*PostWithComments, error)

Comments fetches a post and its comment tree. The comments endpoint returns a two-element array: [ Listing(the post), Listing(the comments) ]. subreddit may be blank (Reddit resolves the post by id alone) but supplying it avoids a redirect. id is the base-36 post id ("abc123"), with or without the "t3_" prefix.

func (*Client) Friends added in v0.9.0

func (c *Client) Friends(ctx context.Context) ([]Friend, error)

Friends fetches the redditors the authenticated account follows, via GET /api/v1/me/friends. Reddit returns a single "UserList" thing whose data.children each name one followed account, so a reader can import the people a logged-in account follows the same way it imports its subreddits. Authentication is required — a session cookie (see WithSessionCookie) or OAuth (the "mysubreddits" scope) — and an anonymous client is rejected before any request is made. The list is unpaginated: Reddit returns the account's full friends list in one response.

func (*Client) Frontpage

func (c *Client) Frontpage(ctx context.Context, sort Sort, opts ListingOptions) (*Page, error)

Frontpage fetches the logged-out front page under the given sort (blank defaults to SortHot). This is r/all-ish aggregate content, not a personalised feed (anonymous access has no account).

func (*Client) MySubreddits added in v0.7.0

func (c *Client) MySubreddits(ctx context.Context) ([]SubredditInfo, error)

MySubreddits fetches every subreddit the authenticated user is subscribed to, paging through Reddit's /subreddits/mine/subscriber listing until it is exhausted, so a reader can import a logged-in account's subscriptions in one call. Authentication is required — a session cookie (see WithSessionCookie) or OAuth — and an anonymous client is rejected before any request is made. Results come back in Reddit's own order (roughly by subscriber count). At most [mySubredditsMaxPages] pages are fetched.

func (*Client) SearchPosts added in v0.5.0

func (c *Client) SearchPosts(ctx context.Context, query, subreddit string, sort SearchSort, opts ListingOptions) (*Page, error)

SearchPosts searches Reddit for posts (links and self posts) matching query, returning a page of posts (the same shape a listing returns) so a search can be shown or followed as a feed. When subreddit is non-empty the search is restricted to that subreddit; otherwise it is site-wide. sort selects the order (blank → relevance). A blank query is rejected. Limit (Reddit clamps to 100) and After paginate. Works anonymously or with a session cookie / OAuth.

func (*Client) SearchSubreddits added in v0.3.0

func (c *Client) SearchSubreddits(ctx context.Context, query string, opts ListingOptions) (*SubredditPage, error)

SearchSubreddits searches Reddit for subreddits whose name, title or description matches query, returning a page of results. Subreddit names cannot be enumerated, so this search endpoint is how a UI discovers them; a caller can further filter the returned names/descriptions locally (e.g. by a regular expression). A blank query is rejected. ListingOptions.Limit (Reddit clamps to 100) and After paginate. Works anonymously or with a configured session cookie / OAuth.

func (*Client) Subreddit

func (c *Client) Subreddit(ctx context.Context, name string, sort Sort, opts ListingOptions) (*Page, error)

Subreddit fetches a page of posts from r/<name> under the given sort. A blank sort defaults to SortHot; a blank name is rejected. The special name "all" and "popular" resolve to Reddit's aggregate feeds.

func (*Client) UserPosts added in v0.6.0

func (c *Client) UserPosts(ctx context.Context, name string, sort Sort, opts ListingOptions) (*Page, error)

UserPosts fetches a page of a redditor's submissions (their "submitted" posts, not their comments), so a reader can follow a person the way it follows a subreddit. A leading "u/" or "/u/" on name is tolerated. sort is new / hot / top (blank → new, the natural order for a person's posts); Time scopes top. A blank name is rejected. Limit (Reddit clamps to 100) and After paginate. Works anonymously or with a session cookie / OAuth.

type Comment

type Comment struct {
	ID         string    `json:"id"`
	Fullname   string    `json:"name"`
	Author     string    `json:"author"`
	Body       string    `json:"body"`
	Score      int       `json:"score"`
	CreatedUTC float64   `json:"created_utc"`
	Stickied   bool      `json:"stickied"`
	Replies    []Comment `json:"-"`
}

Comment is a single "t1" thing in a comment tree. Replies is populated recursively; leaf comments (or the truncated "load more" frontier) have a nil slice.

func (Comment) Created

func (c Comment) Created() time.Time

Created returns the comment's creation time in UTC.

type Friend added in v0.9.0

type Friend struct {
	Name  string  // username (follow as u/<Name>)
	ID    string  // account fullname, e.g. "t2_1w72"
	Date  float64 // friendship creation time, Unix seconds
	RelID string  // relationship id
}

Friend is one redditor the authenticated account follows (Reddit's "friends" list). Name is the username — subscribe to a person's submissions as u/<Name>. ID is their "t2_…" fullname, Date the Unix time (seconds) the friendship was created, and RelID the friendship relationship id.

type ListingOptions

type ListingOptions struct {
	// Limit caps the number of posts returned (Reddit clamps to 100).
	Limit int
	// After is the fullname ("t3_xxxxxx") to page from; use [Page.After].
	After string
	// Time scopes SortTop / SortControvers; ignored otherwise.
	Time TimeRange
}

ListingOptions tunes a listing request. The zero value asks Reddit for its default page size starting at the top of the listing.

type Option

type Option func(*Client)

Option customises a Client at construction time.

func WithBaseURL

func WithBaseURL(base string) Option

WithBaseURL overrides the API host. Primarily a test seam (point it at an httptest server); an empty string is ignored. Any trailing slash is trimmed so path joining stays predictable.

func WithHTTPClient

func WithHTTPClient(h *http.Client) Option

WithHTTPClient supplies the http.Client used for every request. Use it to inject timeouts, proxies, or a test transport. A nil client is ignored.

func WithOAuth

func WithOAuth(clientID, clientSecret string) Option

WithOAuth configures application-only ("client_credentials") OAuth using a registered script/web app's client id and secret. This grants read access to public listings without a user account and works from IPs where the anonymous ".json" endpoints are blocked. Empty credentials are ignored (the Client stays anonymous).

func WithOAuthScript

func WithOAuthScript(clientID, clientSecret, username, password string) Option

WithOAuthScript configures the resource-owner-password ("script app") grant, authenticating as a specific Reddit user. Use it when you need per-user context; otherwise prefer WithOAuth. Empty credentials are ignored.

func WithRateLimit added in v0.8.0

func WithRateLimit(perMinute int) Option

WithRateLimit paces the client to at most perMinute requests, so aggregating many subscriptions (or paging an infinite scroll) glides under Reddit's budget instead of tripping 429 "Too Many Requests". The client further adapts to the X-Ratelimit headers Reddit returns and honours a 429's Retry-After. A perMinute <= 0 restores DefaultRequestsPerMinute.

func WithSessionCookie added in v0.2.0

func WithSessionCookie(cookie string) Option

WithSessionCookie authenticates reads with a logged-in browser's reddit_session cookie instead of OAuth. Reddit's self-serve OAuth registration is effectively closed to new personal projects, so supplying the cookie from an already-signed-in browser is the practical path for individual, read-only use.

The argument may be either the bare reddit_session value or a full "reddit_session=…; other=…" Cookie header string; both normalise to a Cookie header value (a bare value is wrapped as "reddit_session=<value>"; a string that already contains "name=value" pairs is used verbatim). Surrounding whitespace is trimmed and an empty (or whitespace-only) argument is ignored.

Unlike WithOAuth, this keeps the default www base URL — the cookie authenticates the same anonymous ".json" endpoints. When both a session cookie and OAuth are configured, the session cookie takes precedence: the request carries the Cookie header and no Bearer token.

func WithUserAgent

func WithUserAgent(ua string) Option

WithUserAgent sets the User-Agent header. An empty string is ignored so the DefaultUserAgent always remains in effect. Reddit blocks generic agents, so production callers should pass a unique, descriptive value.

type Page

type Page struct {
	Posts  []Post
	After  string
	Before string
}

Page is a decoded listing: the posts plus the paging cursors Reddit returns. After is the fullname to pass back via ListingOptions.After to fetch the next page ("" when the listing is exhausted).

type Post

type Post struct {
	ID          string  `json:"id"`
	Fullname    string  `json:"name"` // e.g. "t3_abc123", used for paging/After
	Title       string  `json:"title"`
	Author      string  `json:"author"`
	Subreddit   string  `json:"subreddit"`
	Permalink   string  `json:"permalink"`
	URL         string  `json:"url"`
	Domain      string  `json:"domain"`
	SelfText    string  `json:"selftext"`
	Thumbnail   string  `json:"thumbnail"`
	Score       int     `json:"score"`
	Ups         int     `json:"ups"`
	NumComments int     `json:"num_comments"`
	CreatedUTC  float64 `json:"created_utc"`
	IsSelf      bool    `json:"is_self"`
	Over18      bool    `json:"over_18"`
	Stickied    bool    `json:"stickied"`
	Flair       string  `json:"link_flair_text"`
	// Media fields. Reddit exposes a post's image(s)/video through several shapes;
	// the Images / VideoURL accessors (see media.go) resolve them uniformly.
	PostHint      string                   `json:"post_hint"` // "image","hosted:video","rich:video","link",…
	IsVideo       bool                     `json:"is_video"`
	IsGallery     bool                     `json:"is_gallery"`
	Preview       postPreview              `json:"preview"`
	GalleryData   galleryData              `json:"gallery_data"`
	MediaMetadata map[string]mediaMetaItem `json:"media_metadata"`
	Media         postMedia                `json:"media"`
}

Post is a single link or self-post ("t3" thing). Fields mirror the subset of Reddit's JSON that a reader UI needs; unmapped fields are dropped.

func (Post) Created

func (p Post) Created() time.Time

Created returns the post's creation time in UTC.

func (p Post) FullPermalink() string

FullPermalink joins the site-relative Permalink onto the canonical host so it is directly openable. Reddit's permalinks already begin with "/".

func (Post) HasThumbnail

func (p Post) HasThumbnail() bool

HasThumbnail reports whether Thumbnail is a real image URL rather than one of Reddit's sentinel strings ("self", "default", "nsfw", "spoiler", "").

func (Post) Images added in v0.4.0

func (p Post) Images() []string

Images returns the post's display image URLs in display order, best-effort and HTML-unescaped, de-duplicated: a gallery's items first (in order), else the resolved preview source image(s), else a direct-image URL. It is empty when the post carries no image the caller can show. A reddit-hosted video's poster comes through the preview, so a video post returns its poster here too (use Post.VideoURL for the playable stream).

func (Post) PreviewImage added in v0.4.0

func (p Post) PreviewImage() string

PreviewImage returns the single best display image for the post (the first of Post.Images), or "" when there is none.

func (Post) VideoURL added in v0.4.0

func (p Post) VideoURL() string

VideoURL returns the reddit-hosted video's playable MP4 fallback URL, or "" when the post is not a reddit-hosted video.

type PostWithComments

type PostWithComments struct {
	Post     Post
	Comments []Comment
}

PostWithComments bundles a resolved post with its (partial) comment tree, as returned by the /comments/<id> endpoint.

type SearchSort added in v0.5.0

type SearchSort string

SearchSort selects how post-search results are ordered.

const (
	// SearchRelevance is Reddit's default search order.
	SearchRelevance SearchSort = "relevance"
	// SearchHot / SearchTop / SearchNew / SearchComments mirror the feed sorts.
	SearchHot      SearchSort = "hot"
	SearchTop      SearchSort = "top"
	SearchNew      SearchSort = "new"
	SearchComments SearchSort = "comments"
)

type Sort

type Sort string

Sort selects the ordering of a listing endpoint.

const (
	SortHot        Sort = "hot"
	SortNew        Sort = "new"
	SortTop        Sort = "top"
	SortRising     Sort = "rising"
	SortControvers Sort = "controversial"
	SortBest       Sort = "best"
)

Listing sort orders understood by Reddit. Best is only meaningful on the logged-out front page; the rest apply to any subreddit.

type SubredditInfo added in v0.3.0

type SubredditInfo struct {
	Name              string // display_name, e.g. "golang" (subscribe as r/<Name>)
	Title             string // human title
	PublicDescription string // short public blurb
	Subscribers       int64  // subscriber count
	Over18            bool   // NSFW flag
	URL               string // path, e.g. "/r/golang/"
	Type              string // subreddit_type: public / restricted / private
}

SubredditInfo is a subreddit's metadata as returned by the search and listing endpoints (a Reddit "t5" thing). It carries what a discovery UI needs to show a result and let the user subscribe by Name.

type SubredditPage added in v0.3.0

type SubredditPage struct {
	Subreddits []SubredditInfo
	After      string
	Before     string
}

SubredditPage is a page of subreddit search results. After is the fullname to pass back via ListingOptions.After for the next page ("" when exhausted).

type TimeRange

type TimeRange string

TimeRange scopes SortTop / SortControvers to a window. Ignored by other sorts.

const (
	TimeHour  TimeRange = "hour"
	TimeDay   TimeRange = "day"
	TimeWeek  TimeRange = "week"
	TimeMonth TimeRange = "month"
	TimeYear  TimeRange = "year"
	TimeAll   TimeRange = "all"
)

Time windows for the "top" and "controversial" sorts.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL