5 redditnews.go (Third Iteration)

So far, all the action happens in the main function. As the program grows, structure and modularity become important. What if we want to check several subreddits?

Create a function named Get that takes the name of subreddit, makes the API call, and returns the items from that subreddit.

func Get(reddit string) ([]Item, error) {}

The function Get takes an argument called reddit that is a string. It also returns an []Item slice and an error value (also a string.)

Here’s the code for our Get function:

func Get(reddit string) ([]Item, error) {
        url := fmt.Sprintf("http://reddit.com/r/%s.json", reddit)
	resp, err := http.Get(url)
	if err != nil {
	        return nil, err
	}

	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
	        return nil, errors.New(resp.Status)
	}

	r := new(Response)
	err = json.NewDecoder(resp.Body).Decode(r)
	if err != nil {
	        return nil, err
	}

	items := make([]Item, len(r.Data1.Children))
	for i, child := range r.Data1.Children {
	        items[i] = child.Data2
	}
	return items, nil
}
  • Our new Get function allows us to call the Reddit API request from anywhere in our program, instead of only from our main() function.
  • When we call the Get function, we will specify which Subreddit to use.
  • We use fmt.Sprintf to construct the request URL from the provided reddit string.
  • Exiting the function, we return a nil slice and a non-nil error value, or vice versa.
  • The response’s Status field is just a string; we use the errors.New function to convert it to an error value.
  • We use the make function to allocate an Item slice big enough to store the response data.
  • We iterate over the response’s Children slice, assigning each child’s Data2 element to the corresponding element in the items slice.

Next, we need to implement a formatted string that returns the Article’s Author, Score, URL and Title.

The fmt package knows how to format the built-in types, but it can be told how to format user-defined types, too.

When you pass a value to the fmt.Print functions, it checks to see if it implements the fmt.Stringer interface:

type Stringer interface {
        String() string
}

Any type that implements a String() string method is a Stringer, and the fmt package will use that method to format values of that type.

Thus the Stringer interface will allow us to format User-Defined types, such as the “Item” type we’re about to define, to format our Subreddit results.

Here’s a String method for the Item type that returns the Author, Score, URL, Title and a newline:

func (i Item) String() string {
    	return fmt.Sprintf(
	        "Author: %s\nScore: %d\nURL: %s\nTitle: %s\n\n",
		i.Author,
		i.Score,
		i.URL,
		i.Title)
}

To print the item we just pass it to Println, which uses the provided String method to format the Item.

fmt.Println(item)

Now that we’ve implemented this, change your main() function to reflect this:

for _, item := range items {
        fmt.Println(item)
}

Discussion

range copies the values from the slice you’re iterating over making the original value untouchable. We could use pointers or the index instead. How? I would leave that to you to experiment.

Note that we need to add the following import errors to the program that follows.

The complete program so far:

Program redditnews.go


package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/http"
)

type Item struct {
	Author string `json:"author"`
	Score  int    `json:"score"`
	URL    string `json:"url"`
	Title  string `json:"title"`
}

type response struct {
        Data1 struct {
                Children []struct {
                        Data2 Item `json:"data"`
                } `json:"children"`
        } `json:"data"`
}

func Get(reddit string) ([]Item, error) {
	url := fmt.Sprintf("http://reddit.com/r/%s.json", reddit)
	resp, err := http.Get(url)
	if err != nil {
		return nil, err
	}

	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, errors.New(resp.Status)
	}

	r := new(response)
	err = json.NewDecoder(resp.Body).Decode(r)
	if err != nil {
		return nil, err
	}

	items := make([]Item, len(r.Data1.Children))
	for i, child := range r.Data1.Children {
		items[i] = child.Data2
	}
	return items, nil
}

func (i Item) String() string {
	return fmt.Sprintf(
		"Author: %s\nScore: %d\nURL: %s\nTitle: %s\n\n",
		i.Author,
		i.Score,
		i.URL,
		i.Title)
}

func main() {
	items, err := Get("golang")
	if err != nil {
		log.Fatal(err)
	}

	for _, item := range items {
		fmt.Println(item)
	}
}

You can download this code here.

Now when we run our program we should see a nicely formatted list of links.