I'm a beginner trying to get a better understanding of golang so I decided to write an atom feeder that runs from the terminal.

I am using the gofeed library to parse the atom feeds and the tview library to make the terminal ui.

I would like to get some advice on better ways to separate my code from these libraries so its easier to unit test, without having to use a mocking library. Any comments on the code in general would be appreciated.

I've included the three functions that do most of the work, the complete code is on github.

func main() {
    // init threadsafe feed data
    safeFeedData = &SafeFeedData{feedData: make(map[string]FeedDataModel)}

    // init ui elements
    app = tview.NewApplication()

    feedList = tview.NewList().ShowSecondaryText(false)
    feedList.AddItem("Fetching Feed Data", "", 0, nil)

    entriesList = tview.NewList().ShowSecondaryText(false)
    entriesList.AddItem("Fetching Feed Data", "", 0, nil)

    entryTextView = tview.NewTextView().
        SetChangedFunc(func() {

    entryTextView.SetText("Fetching Feed Data")

    // set up flex layout
    flex := tview.NewFlex().
        AddItem(feedList, 0, 1, false).
            AddItem(entriesList, 0, 1, false).
            AddItem(entryTextView, 0, 2, false), 0, 2, false)

    // async call to load feed data
    go loadAllFeedDataAndUpdateInterface()

    // call to run tview app
    if uiError := app.SetRoot(flex, true).SetFocus(feedList).Run(); uiError != nil {

// This will run asynchronously to fetch atom feeds and load the data into the interface
func loadAllFeedDataAndUpdateInterface() {
    var configError error
    allFeeds, configError = loadJsonConfig()
    if configError != nil {

    for i := 0; i < len(allFeeds.Feeds); i++ {
        feedData, atomFeedError := loadFeedData(allFeeds.Feeds[i].URL)
        if atomFeedError != nil {
        } else {
            safeFeedData.setSiteData(allFeeds.Feeds[i].URL, feedData)

    // Using QueueUpdateDraw as its a threadsafe way to update tview primitives
    app.QueueUpdateDraw(func() {
        // add items to feed list
        for _, feed := range allFeeds.Feeds {
            feedData := safeFeedData.GetEntries(feed.URL)
            feedList.AddItem(feedData.name, feed.URL, 0, func() {
                // handle user selecting item by moving focus to entry list

        // handle user changing selected feed item by loading entries list
        feedList.SetChangedFunc(func(i int, feedName string, url string, shortcut rune) {

        // handle user changing selected item of entries list by loading entry text view
        entriesList.SetChangedFunc(func(i int, entryName string, secondaryText string, shortcut rune) {

        // when user hits escape in entries list, move focus back to feed list
        entriesList.SetDoneFunc(func() {

        // load initial state of interface
        //make sure there's at least one entry in selected
        if len(safeFeedData.GetEntries(getSelectedFeedUrl()).entries) > 0 {

// loadFeedData use gofeed library to load data from atom feed
func loadFeedData(url string) (FeedDataModel, error) {
    fp := gofeed.NewParser()
    fp.UserAgent = "Clacks - Terminal Atom Reader"
    feedData, err := fp.ParseURL(url)
    if err != nil {
        return FeedDataModel{}, errors.New("Error processing atom feed: " + err.Error())

    if len(feedData.Items) > 0 {
        feedName := feedData.Title
        entrySlice := make([]Entry, len(feedData.Items))
        for i, item := range feedData.Items {
            entrySlice[i] = Entry{
                title: html.UnescapeString(strip.StripTags(item.Title)),
                content: strings.TrimSpace(html.UnescapeString(strip.StripTags(item.Description))),
        return FeedDataModel{name: feedName, entries: entrySlice}, nil

    return FeedDataModel{}, nil



Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.