iT邦幫忙

2023 iThome 鐵人賽

DAY 11
0
自我挑戰組

30天打造自己的RSS閱讀器:Go語言與DevOps的實戰應用系列 第 11

Day 11: 在 VSCode 中建立 Go 專案,打造你的 RSS 閱讀器 API

  • 分享至 

  • xImage
  •  

好啦,經過前一天對於自動化測試和測試金字塔的探討,我們準備好可以開始真正動手寫程式了 XD
今天我們會用VSCode開發環境,使用 Go 語言來打造 RSS 閱讀器的 API。

開發環境設定:VSCode 與 Go

VSCode是個相當方便、功能多樣的編輯器,對Go語言也有很棒的支持。你可以從官網下載VSCode,然後再從其擴充功能市集安裝 Go 語言的支援套件。

https://ithelp.ithome.com.tw/upload/images/20230914/20162813jbKHoWhPX5.png

當然也不要忘記去 Go 的官網下載 Go 語言的安裝檔,並且設定好環境變數。

建立新專案

建立一個資料夾,並初始化 go mod

mkdir MyGoProject
cd MyGoProject
go mod init github.com/yourusername/too-simple-rss-reader

初始化後,你會在專案資料夾中看到一個 go.mod 檔案。這個檔案將用於管理你的 Go 專案依賴。

https://ithelp.ithome.com.tw/upload/images/20230914/20162813AVSxGP0rF2.png

打造 MVP (Minimum Viable Product)

現在,新增一個名為 main.go 的檔案。這個檔案將作為我們專案的入口點。為了快速展示一個最小可行的例子,我們先編寫一個簡單的 code。

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"sync"
	"time"

	"github.com/mmcdole/gofeed"
)

type Feed struct {
	URL   string     `json:"url"`
	Read  bool       `json:"read"`
	Items []FeedItem `json:"items"`
}

type FeedItem struct {
	Title string `json:"title"`
	Link  string `json:"link"`
	Read  bool   `json:"read"`
}

var (
	feedMap      = make(map[string]Feed)
	feedMapMutex = &sync.RWMutex{}
)

func subscribeFeed(w http.ResponseWriter, r *http.Request) {
	var feed Feed
	err := json.NewDecoder(r.Body).Decode(&feed)
	if err != nil {
		http.Error(w, "Invalid JSON", http.StatusBadRequest)
		return
	}

	fp := gofeed.NewParser()
	parsedFeed, err := fp.ParseURL(feed.URL)
	if err != nil {
		http.Error(w, "Invalid RSS URL", http.StatusBadRequest)
		return
	}

	feed.Items = make([]FeedItem, len(parsedFeed.Items))
	for i, item := range parsedFeed.Items {
		feed.Items[i] = FeedItem{Title: item.Title, Link: item.Link, Read: false}
	}

	feedMapMutex.Lock()
	feedMap[feed.URL] = feed
	feedMapMutex.Unlock()

	json.NewEncoder(w).Encode(feed)
}

func listSubscribedFeeds(w http.ResponseWriter, r *http.Request) {
	feedMapMutex.RLock()
	defer feedMapMutex.RUnlock()

	feeds := make([]Feed, 0, len(feedMap))
	for _, feed := range feedMap {
		feeds = append(feeds, feed)
	}

	json.NewEncoder(w).Encode(feeds)
}

func deleteFeed(w http.ResponseWriter, r *http.Request) {
	var feed Feed
	err := json.NewDecoder(r.Body).Decode(&feed)
	if err != nil {
		http.Error(w, "Invalid JSON", http.StatusBadRequest)
		return
	}

	feedMapMutex.Lock()
	delete(feedMap, feed.URL)
	feedMapMutex.Unlock()

	// Return an empty feed to indicate success
	json.NewEncoder(w).Encode(Feed{})
}

func markItemRead(w http.ResponseWriter, r *http.Request) {
	var payload struct {
		URL   string `json:"url"`
		Title string `json:"title"`
		Read  bool   `json:"read"`
	}

	err := json.NewDecoder(r.Body).Decode(&payload)
	if err != nil {
		http.Error(w, "Invalid JSON", http.StatusBadRequest)
		return
	}

	feedMapMutex.Lock()
	defer feedMapMutex.Unlock()

	feed, ok := feedMap[payload.URL]
	if !ok {
		http.Error(w, "Feed not found", http.StatusBadRequest)
		return
	}

	for i, item := range feed.Items {
		if item.Title == payload.Title {
			feed.Items[i].Read = payload.Read
			break
		}
	}

	feedMap[payload.URL] = feed

	// Return the updated feed to indicate success
	json.NewEncoder(w).Encode(feed)
}

func updateFeeds() {
	fp := gofeed.NewParser()
	feedMapMutex.Lock()
	for url, feed := range feedMap {
		parsedFeed, err := fp.ParseURL(url)
		if err != nil {
			fmt.Printf("Error parsing feed %s: %v\n", url, err)
			continue
		}

		existingItems := make(map[string]bool)
		for _, item := range feed.Items {
			existingItems[item.Title] = true
		}

		for _, newItem := range parsedFeed.Items {
			if _, exists := existingItems[newItem.Title]; !exists {
				feed.Items = append(feed.Items, FeedItem{Title: newItem.Title, Link: newItem.Link, Read: false})
			}
		}

		feedMap[url] = feed
	}
	feedMapMutex.Unlock()
}

func autoUpdateFeeds() {
	ticker := time.NewTicker(10 * time.Minute)
	for range ticker.C {
		updateFeeds()
	}
}

func main() {
	http.HandleFunc("/subscribe", subscribeFeed)
	http.HandleFunc("/list", listSubscribedFeeds)
	http.HandleFunc("/delete", deleteFeed)
	http.HandleFunc("/markRead", markItemRead)

	go autoUpdateFeeds()

	http.ListenAndServe(":8080", nil)
}

專案功能介紹

本專案提供了一個基本的 RSS 閱讀器 API,它能夠幫助你更有效地管理和瀏覽你喜愛的 RSS feeds。一旦成功啟動服務,透過訪問 http://localhost:8080/,你將能夠接觸到以下核心功能:

  1. 取得所有訂閱的 Feed 列表:你可以獲取一個包含所有目前訂閱的 RSS feeds 的列表。
  2. 新增新的 Feed 訂閱:提供一個 RSS URL,即可將其新增到你的訂閱列表中。
  3. 刪除 Feed 訂閱:可以從你的訂閱列表中移除不再需要的 Feed。

優化和後續方向

在我們的基本架構確定之後,我們還有很多事情可以做:

  1. 錯誤處理與日誌:我們的基礎版本還未包括全面的錯誤處理和日誌系統。這些是生產環境中不可或缺的。為了資安需求甚至可能需要有稽核紀錄,詳細記錄哪個人在甚麼時候做了甚麼。
  2. 併發與同步:為了提高效能,我們可以使用 Go 的 goroutines 和 channels。
  3. 資料儲存:目前,所有的 feeds 都儲存在記憶體中。程式一離開或是當掉就會丟失資料。,我們可能會需要一個可以持久儲存資料的方案,比如說 PostgresSQL+Redis 的組合。
  4. 用戶認證:如果你希望讓用戶能儲存他們自己的 feeds,你會需要添加一個認證機制。
  5. 前端界面:雖然這是一個 API 專案,但有空的話也可以加上一個前端介面以增加用戶友善度。
  6. 獲取單一 Feed 的最新文章:目前的 API 會傳回所有的 Feed 的文章內容,未來可改由輸入特定 Feed 的 URL,只獲得該 Feed 最新的文章列表,避免傳回過多內容,最好還可以增加 pagination,一次只取得一部分內容,優化使用者體驗。
  7. 搜索功能:透過關鍵字搜尋,找到訂閱的 Feed 中感興趣的文章。

總結

今天我們初步建立了RSS閱讀器的基礎架構,包括一個簡單的Go語言API和前端HTML頁面。在接下來的幾天,我們將逐步增加更多功能和測試。

希望你會喜歡這次的內容,如果你有什麼問題或建議,隨時可以告訴我喔!謝謝!


上一篇
Day 10:自動化測試的 單元測試、測試金字塔 解說
下一篇
Day 12:單元測試 in Go
系列文
30天打造自己的RSS閱讀器:Go語言與DevOps的實戰應用30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言