RSS ไปยังโซเชียลเน็ตเวิร์ก 66 แห่ง: Facebook, Instagram, X, LinkedIn, Telegram และอื่นๆ บล็อก พันธมิตร (Affiliate) ติดต่อเรา
เข้าสู่ระบบ เริ่มต้นใช้งานฟรี
Updated: 2026-10-25
Echo RSS Feed in Go: Structs, Dates and ETag Caching

Short answer: To serve an RSS feed from an Echo application written in Go, define structs for the channel and its items, load the newest published rows from the database with the request context, marshal the structs with encoding/xml so every value is escaped, format dates in UTC and return the bytes with the application/rss+xml content type. Hash the finished body into an ETag, answer a matching If-None-Match header with a 304, take the last build date from the newest item and not from the clock, test the route with curl and a validator, then give the public address to an auto-poster.

Echo is a minimal, fast web framework for Go with a clean router, middleware and helpers for responses. Teams use it for APIs, internal tools and content services where they want routing and speed without a large framework around them. Sooner or later such a service publishes something that people want to follow: articles, release notes, offers, events or listings. A feed is the standard way to let readers, aggregators and auto-posters watch that content without an account or an API key. Echo has no feed support of its own and needs none, because the Go standard library already contains an XML encoder that escapes text correctly. This guide builds a complete Echo RSS feed with typed structs, a database query bound to the request, stable identifiers, correct dates and conditional requests, explains each decision and ends with the checks that keep the feed dependable once other systems rely on it.

When an Echo service needs a feed

A feed is worth adding whenever new items appear on a schedule you do not control: a changelog that grows with every release, a directory of listings, a news section or a library of guides. Readers who follow it in a feed reader get every item in order. Search engines and aggregators discover new pages faster. And an auto-poster such as PostRSS can share each new item on social networks the moment it is published, which turns a quiet publishing routine into a steady stream of visitors.

If your content already lives in a blog or a CMS, use the feed that system provides instead; see how to find your WordPress feed link. If you prefer another Go framework, the same ideas appear in the Fiber RSS feed guide และ the guide to the gorilla feeds package. Build the route yourself when the content is in your own database and no ready-made feed exists.

Set up the module

Create a module and add Echo. The example reads from SQLite through a pure Go driver, so nothing needs to be compiled with a C toolchain; any database that implements database/sql works the same way.

mkdir feedsvc && cd feedsvc
go mod init example.com/feedsvc
go get github.com/labstack/echo/v4
go get modernc.org/sqlite

Assume a table named articles with the columns slug, title, summary, image_url, image_bytes, image_type and published_at. The last column holds a UTC unix timestamp, which keeps comparisons simple and avoids time zone surprises. Items scheduled for the future simply have a timestamp that has not arrived yet.

The complete application

The program below defines the document as Go structs, queries the database with the context of the request, marshals the XML and sends it with a strong validator. The condition on the publication time matters: without it, an item scheduled for tomorrow would appear in the feed today, and an auto-poster would share it before it exists on the website.

package main

import (
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"encoding/xml"
	"net/http"
	"strings"
	"time"

	"github.com/labstack/echo/v4"
	_ "modernc.org/sqlite"
)

const site = "https://example.com"

type AtomLink struct {
	XMLName xml.Name `xml:"http://www.w3.org/2005/Atom link"`
	Href    string   `xml:"href,attr"`
	Rel     string   `xml:"rel,attr"`
	Type    string   `xml:"type,attr"`
}

type Enclosure struct {
	URL    string `xml:"url,attr"`
	Length int64  `xml:"length,attr"`
	Type   string `xml:"type,attr"`
}

type GUID struct {
	IsPermaLink bool   `xml:"isPermaLink,attr"`
	Value       string `xml:",chardata"`
}

type Item struct {
	Title       string     `xml:"title"`
	Link        string     `xml:"link"`
	GUID        GUID       `xml:"guid"`
	PubDate     string     `xml:"pubDate"`
	Description string     `xml:"description"`
	Enclosure   *Enclosure `xml:"enclosure,omitempty"`
}

type Channel struct {
	Title         string   `xml:"title"`
	Link          string   `xml:"link"`
	Description   string   `xml:"description"`
	Language      string   `xml:"language"`
	LastBuildDate string   `xml:"lastBuildDate"`
	Self          AtomLink
	Items         []Item `xml:"item"`
}

type RSS struct {
	XMLName xml.Name `xml:"rss"`
	Version string   `xml:"version,attr"`
	Channel Channel  `xml:"channel"`
}

func rfc822(ts int64) string {
	return time.Unix(ts, 0).UTC().Format(time.RFC1123Z)
}

func loadItems(c echo.Context, db *sql.DB, limit int) ([]Item, int64, error) {
	rows, err := db.QueryContext(c.Request().Context(),
		"SELECT slug, title, summary, image_url, image_bytes, image_type, published_at "+
			"FROM articles WHERE published_at <= ? ORDER BY published_at DESC LIMIT ?",
		time.Now().Unix(), limit)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()

	var items []Item
	var newest int64
	for rows.Next() {
		var slug, title, summary string
		var imageURL, imageType sql.NullString
		var imageBytes sql.NullInt64
		var published int64
		if err := rows.Scan(&slug, &title, &summary, &imageURL, &imageBytes, &imageType, &published); err != nil {
			return nil, 0, err
		}
		if newest == 0 {
			newest = published
		}
		url := site + "/articles/" + slug + "/"
		item := Item{
			Title:       title,
			Link:        url,
			GUID:        GUID{IsPermaLink: true, Value: url},
			PubDate:     rfc822(published),
			Description: summary,
		}
		if imageURL.Valid && imageURL.String != "" {
			mediaType := imageType.String
			if mediaType == "" {
				mediaType = "image/jpeg"
			}
			item.Enclosure = &Enclosure{URL: imageURL.String, Length: imageBytes.Int64, Type: mediaType}
		}
		items = append(items, item)
	}
	return items, newest, rows.Err()
}

func etagMatches(header, etag string) bool {
	for _, token := range strings.Split(header, ",") {
		token = strings.TrimPrefix(strings.TrimSpace(token), "W/")
		if token == "*" || token == etag {
			return true
		}
	}
	return false
}

func feedHandler(db *sql.DB) echo.HandlerFunc {
	return func(c echo.Context) error {
		items, newest, err := loadItems(c, db, 20)
		if err != nil {
			return echo.NewHTTPError(http.StatusInternalServerError, "feed unavailable")
		}
		doc := RSS{
			Version: "2.0",
			Channel: Channel{
				Title:         "Example Journal",
				Link:          site + "/",
				Description:   "News and guides from Example Journal.",
				Language:      "en",
				LastBuildDate: rfc822(newest),
				Self:          AtomLink{Href: site + "/feed.xml", Rel: "self", Type: "application/rss+xml"},
				Items:         items,
			},
		}
		body, err := xml.Marshal(doc)
		if err != nil {
			return echo.NewHTTPError(http.StatusInternalServerError, "feed unavailable")
		}
		body = append([]byte(xml.Header), body...)

		sum := sha256.Sum256(body)
		etag := `"` + hex.EncodeToString(sum[:16]) + `"`
		h := c.Response().Header()
		h.Set("ETag", etag)
		h.Set("Cache-Control", "public, max-age=300")
		if etagMatches(c.Request().Header.Get("If-None-Match"), etag) {
			return c.NoContent(http.StatusNotModified)
		}
		return c.Blob(http.StatusOK, "application/rss+xml; charset=utf-8", body)
	}
}

func main() {
	db, err := sql.Open("sqlite", "site.db")
	if err != nil {
		panic(err)
	}
	e := echo.New()
	e.GET("/feed.xml", feedHandler(db))
	e.Logger.Fatal(e.Start(":8080"))
}

Save it as main.go and start it with go run .. The route accepts GET, and the framework answers 405 for other methods, which is the correct behaviour for a feed. If your monitoring or a validator uses HEAD, register the same handler with e.HEAD as well.

Why the structs matter

Go’s xml package turns structs into elements and attributes by tags, and it escapes text for you. A title that contains an ampersand or an angle bracket cannot break the document, which is the most common failure of feeds assembled from string pieces. The tags also keep the shape of the feed visible in one place: the channel holds the metadata and a list of items, each item has the five or six elements that readers and auto-posters expect, and the optional enclosure disappears from the output when there is no image. The Atom self link is written with its own namespace, which keeps the feed friendly to validators that recommend it.

How the ETag logic works

Auto-posters, readers and search engines request a feed often, and almost always nothing has changed. A validator lets them ask a cheap question: has the document changed since the version I hold? The handler hashes the finished bytes, quotes the hash as an entity tag and compares it with the If-None-Match header. A match produces a 304 response with no body, which saves bandwidth for both sides and keeps your server calm.

The hash is computed from the body, which brings one rule: the body must not change unless the content changes. That is why the example takes the last build date from the newest item and not from time.Now. If you wrote the current time into that element, the hash would change on every request, no client would ever receive a 304 and the validator would be useless. The same rule applies to any value generated at request time. A compressing proxy may turn a strong tag into a weak one, which is why the comparison above strips the W/ prefix before it compares.

Where each field comes from

Feed elementSourceRule
titleArticle titlePlain text, escaped by the encoder
linkPublic address of the pageAbsolute, with https, returning 200 directly
guidSame address, or a stored identifierNever changes after publication
pubDatePublication timestampRFC 822 style in UTC, never the current time
descriptionShort summaryOne or two sentences of plain text
enclosureFeatured imageAddress, size in bytes and media type

The identifier is the most important decision. An auto-poster remembers which items it has already shared by their GUID. If the value changes when someone edits a slug or republishes an article, the same story is shared again. If you may rename slugs, store a separate permanent identifier in the database and write it in the guid element with isPermaLink set to false.

Dates in the right format

RSS dates follow the RFC 822 pattern, with English abbreviations for days and months. Go’s time.RFC1123Z layout produces exactly that shape with a numeric zone, which every feed parser accepts. The timestamp comes from the database as an integer, is converted with time.Unix and is always switched to UTC before formatting, so the server’s local zone never leaks into the output. Mixing in local time is the most common cause of feeds whose items appear hours late or early in readers.

Caching without delaying new posts

The Cache-Control header in the example lets shared caches keep the response for five minutes, and the ETag lets everyone revalidate cheaply after that. If you place a content delivery network in front of the application, keep the lifetime short so a new item does not wait long for its turn. For busy services you can also keep the rendered bytes in memory for a few seconds behind a mutex, which protects the database without delaying anything noticeably. For the background, read how auto-posting tools use ETags and conditional requests และ what 301, 304, 404 and 500 mean for auto-posting.

Test the feed before you connect it

A few minutes of testing prevents days of confusion later. Start with a Go test that proves the conditional logic, using the httptest package from the standard library and an in-memory database.

package main

import (
	"database/sql"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/labstack/echo/v4"
)

func TestFeedConditionalGet(t *testing.T) {
	db, _ := sql.Open("sqlite", ":memory:")
	db.SetMaxOpenConns(1)
	db.Exec(`CREATE TABLE articles (slug TEXT, title TEXT, summary TEXT,
		image_url TEXT, image_bytes INTEGER, image_type TEXT, published_at INTEGER)`)
	db.Exec(`INSERT INTO articles VALUES ('first', 'First', 'Summary', NULL, NULL, NULL, 1700000000)`)

	e := echo.New()
	e.GET("/feed.xml", feedHandler(db))

	first := httptest.NewRecorder()
	e.ServeHTTP(first, httptest.NewRequest(http.MethodGet, "/feed.xml", nil))
	if first.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d", first.Code)
	}
	etag := first.Header().Get("ETag")

	req := httptest.NewRequest(http.MethodGet, "/feed.xml", nil)
	req.Header.Set("If-None-Match", etag)
	second := httptest.NewRecorder()
	e.ServeHTTP(second, req)
	if second.Code != http.StatusNotModified {
		t.Fatalf("expected 304, got %d", second.Code)
	}
}

Then check the running service from the outside:

  • Run curl -i https://example.com/feed.xml and confirm a 200 status, the right content type and an ETag header.
  • Repeat the request with the tag you received in an If-None-Match header and confirm that the answer is 304.
  • Paste the public address into the W3C Feed Validation Service and fix every error.
  • Publish a test item, wait for the cache lifetime and confirm that it appears once, at the top.

There is a longer checklist in the feed validation guide and a short curl routine in the guide to debugging a feed with curl.

Running Echo in production

Run the binary behind a reverse proxy that terminates HTTPS, and make sure the proxy passes the real host and scheme so that absolute links are correct. Keep the site address in configuration instead of a constant, so a staging copy never publishes production links. Set sensible timeouts on the server, add a health route that does not touch the database and monitor the feed route separately, because a feed can fail while the home page still works. If a middleware for authentication or rate limiting is attached to the whole router, exclude the feed route from it; an auto-poster must reach the feed without a login.

Common mistakes to avoid

  • Current time in the body. It changes the hash on every request and disables the 304 answer.
  • Unstable GUIDs. Changing them makes auto-posters share old stories again; see why a feed auto-posts the same article twice.
  • Future items in the feed. Always filter on the publication time.
  • Wrong content type. A feed sent as text or HTML confuses some readers; see the content type header guide.
  • Hand-built strings. Concatenating XML invites broken characters; let the encoder escape the values.
  • Ignoring the request context. A query without the context keeps running after the client has gone.

How PostRSS works with an Echo feed

PostRSS has been publishing RSS and Atom feeds to social networks since 2014. Paste the address of your feed route, connect your accounts and choose how each post is built from the title, the description or the page title. PostRSS checks the feed as often as every minute and shares each new item, with its image and link, to the channels you choose among 66 supported networks, messengers, team chats and blogs, including LinkedIn, X, Bluesky, Mastodon and Telegram. Keyword filters, UTM parameters and posting windows let you tune each channel, and webhooks connect PostRSS to your own systems. See the PostRSS features page and the plan limits on the pricing page. More developer guides are under the Technical Guide tag.

การอ่านที่เกี่ยวข้อง

The bottom line

Echo needs only a handler, a few structs and the standard library to serve a dependable RSS feed: select published items dated in the past with the request context, let the XML encoder escape every value, use a stable GUID, format dates in UTC, derive the last build date from the newest item and send the result with the right content type. Add a hash-based ETag so repeat requests end in a 304. Test the route with httptest, curl and a validator, keep the address unchanged and connect it to an auto-poster. After that, every item you publish can reach your social channels automatically.

FAQ

Does Echo have a built-in RSS generator?

No. Echo has no feed support, but the encoding/xml package in the Go standard library marshals typed structs into a valid RSS document, and the handler returns it with the right content type.

Should I use a feed library or encoding/xml?

Both work. The standard library needs no dependency and gives full control over every element, while a library can save a few lines. Either way, never build the XML by joining strings, because values are not escaped.

Why does my feed never return 304?

Most often the body changes between requests, for example because the last build date is the current time. Take that date from the newest item and the hash stays the same until the content changes.

How many items should the feed contain?

Between ten and twenty of the newest published items is enough. An auto-poster only needs recent items, and a smaller file is faster to download and parse.

Can I use this feed with PostRSS?

Yes. PostRSS reads any valid RSS or Atom address that is reachable without a login. Paste the address, connect your channels and new items are shared as soon as they appear.

New guides, once a month

What changed in the networks, what broke, and how to fix it before it costs you reach.

We send a confirmation e-mail first. Unsubscribe any time.

เครื่องมืออื่น ๆ จากทีมของเรา

พัฒนาโดย Internet Solutions ทีมผู้สร้าง PostRSS แต่ละผลิตภัณฑ์ช่วยประหยัดเวลาของคุณในแบบที่ต่างกัน

แชทสด AI สำหรับเว็บไซต์ Talkmio เว็บไซต์ของคุณตอบผู้เข้าชมได้ตลอด 24 ชั่วโมงจากเนื้อหาของคุณเอง ในภาษาของพวกเขา แพ็กเกจฟรี · ไม่ต้องใช้บัตร ผู้ช่วย AI Ask Mio แชท เขียนโค้ด ออกแบบ เขียนงาน และค้นคว้า Mio เลือกโมเดลที่ดีที่สุดให้กับแต่ละงาน แพ็กเกจฟรี ระบบอัตโนมัติ AI สำหรับบล็อกและโซเชียล AI Blog Autopilot AI เขียนบทความ SEO ยาว 2,000–3,000 คำ และแชร์แต่ละบทความไปยังโซเชียลเน็ตเวิร์กกว่า 60 แห่ง 3 บทความแรกฟรี ตรวจสุขภาพเว็บไซต์ การตรวจสอบ AI ของไซต์ SEO ความเร็ว SSL ความปลอดภัย และการตั้งค่าอีเมลในรายงานเดียว เรียงตามสิ่งที่ควรแก้ก่อน ตรวจครั้งแรกฟรี ตรวจ SEO เชิงลึก การตรวจสอบ SEO AI ของไซต์ ตรวจ SEO ทั้งเว็บใน 7 ด้าน รวมถึงการมองเห็นในการค้นหาด้วย AI พร้อมรายการแก้ไขเรียงตามผลกระทบ ตรวจครั้งแรกฟรี ฟีด RSS และฟีดสินค้า เครื่องมือสร้างฟีด RSS สร้าง RSS จากหน้าเว็บใดก็ได้ พร้อมฟีดสินค้าสำหรับ Google และ Meta ที่อัปเดตเอง แพ็กเกจฟรี พัฒนาเว็บไซต์และ SEO Internet Solutions เว็บไซต์ ร้านค้าออนไลน์ และระบบเฉพาะทาง ออกแบบ สร้าง และดูแลโดยทีมของเรา ตั้งแต่ปี 2011