A simple, robust RSS and Atom feed parser for Go with comprehensive context support for cancellation and timeouts. This library provides a clean API for fetching and parsing RSS 2.0 and Atom 1.0 feeds with proper error handling, resource management, and modern Go conventions.
- RSS 2.0 and Atom 1.0 Support - Parse both major feed formats
- Context Support - Full cancellation and timeout support using
context.Context - Custom HTTP Clients - Use your own HTTP client configurations
- Reddit Feed Support - Special handling for Reddit feeds with proper user agents
- Character Encoding - Automatic detection and conversion of various encodings
- Resource Management - Proper cleanup of HTTP response bodies
- Comprehensive Error Handling - Detailed error messages with context
- Modern Go Conventions - Context as first parameter, error wrapping
- Zero Dependencies - Only uses standard library and one charset library
go get github.com/ungerik/go-rsspackage main
import (
"context"
"fmt"
"log"
"github.com/ungerik/go-rss"
)
func main() {
ctx := context.Background()
// Fetch an RSS feed
resp, err := rss.Read(ctx, "https://example.com/feed.rss", false)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
// Parse the RSS feed
channel, err := rss.Regular(ctx, resp)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Channel: %s\n", channel.Title)
fmt.Printf("Description: %s\n", channel.Description)
for _, item := range channel.Item {
fmt.Printf("- %s (%s)\n", item.Title, item.Link)
}
}package main
import (
"context"
"fmt"
"log"
"github.com/ungerik/go-rss"
)
func main() {
ctx := context.Background()
// Fetch an Atom feed
resp, err := rss.Read(ctx, "https://example.com/feed.atom", false)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
// Parse the Atom feed
feed, err := rss.Atom(ctx, resp)
if err != nil {
log.Fatal(err)
}
for _, entry := range feed.Entry {
fmt.Printf("- %s (Updated: %s)\n", entry.Title, entry.Updated)
}
}Fetches an RSS or Atom feed from the given URL using the default HTTP client.
Parameters:
ctx- Context for cancellation and timeout controlurl- The URL of the feed to fetchreddit- Set totruefor Reddit feeds to use appropriate user agent
Returns:
*http.Response- HTTP response (caller must close the body)error- Any error that occurred during fetching
ReadWithClient(ctx context.Context, url string, client *http.Client, reddit bool) (*http.Response, error)
Fetches a feed using a custom HTTP client, allowing for custom configurations.
Use cases:
- Custom timeouts
- Proxy settings
- Custom headers
- Custom transport logic
Fetches a feed without SSL certificate verification.
Parses an RSS 2.0 feed from an HTTP response.
Returns:
*Channel- Parsed RSS channel dataerror- Any parsing error
Parses an Atom 1.0 feed from an HTTP response.
Returns:
*Feed- Parsed Atom feed dataerror- Any parsing error
type Channel struct {
Title string `xml:"title"` // Channel title
Link string `xml:"link"` // Channel URL
Description string `xml:"description"` // Channel description
Language string `xml:"language"` // Channel language
LastBuildDate Date `xml:"lastBuildDate"` // Last build date
Item []Item `xml:"item"` // Channel items
}type Item struct {
Title string `xml:"title"` // Item title
Link string `xml:"link"` // Item URL
Comments string `xml:"comments"` // Comments URL
PubDate Date `xml:"pubDate"` // Publication date
GUID string `xml:"guid"` // Unique identifier
Category []string `xml:"category"` // Categories
Enclosure []ItemEnclosure `xml:"enclosure"` // Media enclosures
Description string `xml:"description"` // Item description
Author string `xml:"author"` // Author email
Content string `xml:"content"` // Full content
FullText string `xml:"full-text"` // Complete text
}type Feed struct {
Entry []Entry `xml:"entry"` // Feed entries
}
type Entry struct {
ID string `xml:"id"` // Unique identifier
Title string `xml:"title"` // Entry title
Updated string `xml:"updated"` // Last updated time
}The Date type provides methods for parsing dates in various formats:
// Parse using multiple common formats
t, err := item.PubDate.Parse()
// Parse with specific format
t, err := item.PubDate.ParseWithFormat(time.RFC822)
// Format parsed date
formatted, err := item.PubDate.Format("2006-01-02 15:04:05")
// Format without error handling (returns error string on failure)
formatted := item.PubDate.MustFormat("2006-01-02")package main
import (
"context"
"fmt"
"time"
"github.com/ungerik/go-rss"
)
func main() {
// Create context with 10 second timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
resp, err := rss.Read(ctx, "https://slow-feed.com/rss", false)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer resp.Body.Close()
channel, err := rss.Regular(ctx, resp)
if err != nil {
fmt.Printf("Parse error: %v\n", err)
return
}
fmt.Printf("Successfully parsed: %s\n", channel.Title)
}package main
import (
"context"
"fmt"
"time"
"github.com/ungerik/go-rss"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
// Cancel after 5 seconds
go func() {
time.Sleep(5 * time.Second)
cancel()
fmt.Println("Operation cancelled")
}()
resp, err := rss.Read(ctx, "https://example.com/feed.rss", false)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer resp.Body.Close()
// ... rest of processing
}package main
import (
"context"
"fmt"
"net/http"
"time"
"github.com/ungerik/go-rss"
)
func main() {
// Create custom HTTP client with timeout
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
DisableCompression: true,
},
}
ctx := context.Background()
resp, err := rss.ReadWithClient(ctx, "https://example.com/feed.rss", client, false)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer resp.Body.Close()
channel, err := rss.Regular(ctx, resp)
if err != nil {
fmt.Printf("Parse error: %v\n", err)
return
}
fmt.Printf("Channel: %s\n", channel.Title)
}package main
import (
"context"
"fmt"
"github.com/ungerik/go-rss"
)
func main() {
ctx := context.Background()
// Fetch Reddit feed with special user agent
resp, err := rss.Read(ctx, "https://reddit.com/r/golang.rss", true)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer resp.Body.Close()
channel, err := rss.Regular(ctx, resp)
if err != nil {
fmt.Printf("Parse error: %v\n", err)
return
}
fmt.Printf("Reddit Channel: %s\n", channel.Title)
}package main
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/ungerik/go-rss"
)
func main() {
feeds := []string{
"https://blog.golang.org/feed.atom",
"https://example.com/feed.rss",
"https://reddit.com/r/golang.rss",
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
var wg sync.WaitGroup
results := make(chan string, len(feeds))
for i, feedURL := range feeds {
wg.Add(1)
go func(index int, url string) {
defer wg.Done()
isReddit := false
if strings.Contains(url, "reddit.com") {
isReddit = true
}
resp, err := rss.Read(ctx, url, isReddit)
if err != nil {
results <- fmt.Sprintf("Feed %d error: %v", index, err)
return
}
defer resp.Body.Close()
// Determine feed type and parse accordingly
if strings.Contains(url, ".atom") || isReddit {
feed, err := rss.Atom(ctx, resp)
if err != nil {
results <- fmt.Sprintf("Feed %d parse error: %v", index, err)
return
}
results <- fmt.Sprintf("Feed %d: %d entries", index, len(feed.Entry))
} else {
channel, err := rss.Regular(ctx, resp)
if err != nil {
results <- fmt.Sprintf("Feed %d parse error: %v", index, err)
return
}
results <- fmt.Sprintf("Feed %d: %s (%d items)", index, channel.Title, len(channel.Item))
}
}(i, feedURL)
}
go func() {
wg.Wait()
close(results)
}()
for result := range results {
fmt.Println(result)
}
}The library provides comprehensive error handling with wrapped errors for better debugging:
resp, err := rss.Read(ctx, "https://example.com/feed.rss", false)
if err != nil {
// Check for specific error types
if strings.Contains(err.Error(), "context deadline exceeded") {
fmt.Println("Request timed out")
} else if strings.Contains(err.Error(), "context canceled") {
fmt.Println("Request was cancelled")
} else {
fmt.Printf("Other error: %v\n", err)
}
return
}Run the test suite:
go test -vThe tests include:
- Feed parsing validation
- Context cancellation testing
- Context timeout testing
- Error handling validation
See the example/ and examples/ directories for comprehensive usage examples:
example/main.go- Basic usage with context and timeoutexamples/context/main.go- Advanced context usage patterns
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.
- Added
context.Contextas first parameter to all functions - Removed
WithContextsuffix variants - Improved error handling with wrapped errors
- Added comprehensive documentation
- Enhanced resource management
- Initial release with basic RSS/Atom parsing
- Context support with
WithContextvariants - Reddit feed support