Applying templating to our Golang web app




Hello and welcome to part 17 of the Golang tutorial series. We just covered templating with the html/template package, and now we're ready to apply it to our news aggregator.

Full code from the previous tutorial:

package main

import (
	"fmt"
	"net/http"
	"html/template"
)

type NewsAggPage struct {
    Title string
    News string
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "<h1>Whoa, Go is neat!</h1>")
}

func newsAggHandler(w http.ResponseWriter, r *http.Request) {
	p := NewsAggPage{Title: "Amazing News Aggregator", News: "some news"}
    t, _ := template.ParseFiles("basictemplating.html")
    t.Execute(w, p)
}

func main() {
	http.HandleFunc("/", indexHandler)
	http.HandleFunc("/agg/", newsAggHandler)
	http.ListenAndServe(":8000", nil) 
}

Our full code from the latest news aggregator application is:

package main

import (
	"encoding/xml"
	"fmt"
	"io/ioutil"
	"net/http"
)

type Sitemapindex struct {
	Locations []string `xml:"sitemap>loc"`
}

type News struct {
	Titles []string `xml:"url>news>title"`
	Keywords []string `xml:"url>news>keywords"`
	Locations []string `xml:"url>loc"`
}

type NewsMap struct {
	Keyword string
	Location string
}

func main() {
	var s Sitemapindex
	var n News
	resp, _ := http.Get("https://www.washingtonpost.com/news-sitemap-index.xml")
	bytes, _ := ioutil.ReadAll(resp.Body)
	xml.Unmarshal(bytes, &s)
	news_map := make(map[string]NewsMap)

	for _, Location := range s.Locations {
		resp, _ := http.Get(Location)
		bytes, _ := ioutil.ReadAll(resp.Body)
		xml.Unmarshal(bytes, &n)

		for idx, _ := range n.Keywords {
			news_map[n.Titles[idx]] = NewsMap{n.Keywords[idx], n.Locations[idx]}
		}
	}
	for idx, data := range news_map {
		fmt.Println("\n\n\n\n\n",idx)
		fmt.Println("\n",data.Keyword)
		fmt.Println("\n",data.Location)
	}
}

We basically want to combine these two programs at this point, and modify our template a bit.

The first thing we're going to is modify the NewsAggPage type. We were initially using the News value as a string, but now we want this to be a map:

type NewsAggPage struct {
    Title string
    News map[string]NewsMap
}

Then we'll add the encoding/xml and io/ioutil imports:

import (
	"fmt"
	"net/http"
	"html/template"
	"encoding/xml"
	"io/ioutil"
)

Next, let's take the NewsMap, Sitemapindex, and News definitions from our latest news aggregator app and add those into our webapp, so now all of our structs:

type NewsMap struct {
	Keyword string
	Location string
}

type NewsAggPage struct {
    Title string
    News map[string]NewsMap
}

type Sitemapindex struct {
	Locations []string `xml:"sitemap>loc"`
}

type News struct {
	Titles []string `xml:"url>news>title"`
	Keywords []string `xml:"url>news>keywords"`
	Locations []string `xml:"url>loc"`
}

Next, we need to work on the newsAggHandler. We just need to take the following code from our latest news aggregator program:

	var s Sitemapindex
	var n News
	resp, _ := http.Get("https://www.washingtonpost.com/news-sitemap-index.xml")
	bytes, _ := ioutil.ReadAll(resp.Body)
	xml.Unmarshal(bytes, &s)
	news_map := make(map[string]NewsMap)

	for _, Location := range s.Locations {
		resp, _ := http.Get(Location)
		bytes, _ := ioutil.ReadAll(resp.Body)
		xml.Unmarshal(bytes, &n)

		for idx, _ := range n.Keywords {
			news_map[n.Titles[idx]] = NewsMap{n.Keywords[idx], n.Locations[idx]}
		}

and put this in the web app's newsAggHandler, and then modify the line for defining p to:

p := NewsAggPage{Title: "Amazing News Aggregator", News: news_map}

Then we'll change the template.ParseFiles to newsaggtemplate.html

    t, _ := template.ParseFiles("newsaggtemplate.html")

Now, our full webapp code:

package main

import (
	"fmt"
	"net/http"
	"html/template"
	"encoding/xml"
	"io/ioutil"
)

type NewsMap struct {
	Keyword string
	Location string
}

type NewsAggPage struct {
    Title string
    News map[string]NewsMap
}

type Sitemapindex struct {
	Locations []string `xml:"sitemap>loc"`
}

type News struct {
	Titles []string `xml:"url>news>title"`
	Keywords []string `xml:"url>news>keywords"`
	Locations []string `xml:"url>loc"`
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "<h1>Whoa, Go is neat!</h1>")
}

func newsAggHandler(w http.ResponseWriter, r *http.Request) {
	var s Sitemapindex
	var n News
	resp, _ := http.Get("https://www.washingtonpost.com/news-sitemap-index.xml")
	bytes, _ := ioutil.ReadAll(resp.Body)
	xml.Unmarshal(bytes, &s)
	news_map := make(map[string]NewsMap)

	for _, Location := range s.Locations {
		resp, _ := http.Get(Location)
		bytes, _ := ioutil.ReadAll(resp.Body)
		xml.Unmarshal(bytes, &n)

		for idx, _ := range n.Keywords {
			news_map[n.Titles[idx]] = NewsMap{n.Keywords[idx], n.Locations[idx]}
		}
	}

	p := NewsAggPage{Title: "Amazing News Aggregator", News: news_map}
    t, _ := template.ParseFiles("newsaggtemplate.html")
    t.Execute(w, p)
}

func main() {
	http.HandleFunc("/", indexHandler)
	http.HandleFunc("/agg/", newsAggHandler)
	http.ListenAndServe(":8000", nil) 
}

Now, we need to work on our template, which we'll call newsaggtemplate.html. What we're going to do is build a table. If you do not know HTML, this is not an HTML tutorial. If you need one, there are many online. To begin, we give show the page title:

<h1>{{.Title}}</h1>

Now we can build our table. A typical table would be something like:

<table>
    <col width="35%">
    <col width="65%">
    <thead>
        <tr>
            <th>Title</th>
            <th>Keywords</th>
        </tr>
    </thead>
    <tbody>
		 <tr>
            <td>some title</td>
            <td>some keywords</td>
        </tr>
        <tr>
            <td>another title</td>
            <td>the other keywords</td>
        </tr>
    </tbody>
</table>

What we then want is to iterate through our NewsMap type, which we've passed to our document in the NewsAggPage type, under the News name

	p := NewsAggPage{Title: "Amazing News Aggregator", News: news_map}
    t, _ := template.ParseFiles("aggregatorfinish.html")
    t.Execute(w, p)

So our template has access to this data under a variable called News, which we can reference with {{ .News }}. Next, we want to iterate over this, rather than just simply output News. To iterate, we will again use range, only the syntax is slightly different. In our HTML template, we use range $key, $value := .YourVar. When you are just displaying a single value, you can use {{ .SingleValue }}, but, when you begin a loop, you need some way to let the templating know that the loop is over, so you will also need an ending to it:

        {{ range $key, $value := .News }}
         ...do some stuff
        {{ end }}

In the "do some stuff" part, you can make reference to the key and value. The reason you need to separate the logic like this is so that you can combine the html and the logic. So, for example, let's create a table row with our variables per item in News.

        {{ range $key, $value := .News }}
         <tr>
            <td><a href="{{ $value.Location }}" target='_blank'>{{ $key }}</td>
            <td>{{ $value.Keyword }}</td>
        </tr>
        {{ end }}

Notice how you can use the {{ logic within quotes too. So, here, each row of our table has the title, which links to the article, and then has all of the keywords in the 2nd column.

Our full template code:

<h1>{{.Title}}</h1>

<table>
    <col width="35%">
    <col width="65%">
    <thead>
        <tr>
            <th>Title</th>
            <th>Keywords</th>
        </tr>
    </thead>
    <tbody>
		{{ range $key, $value := .News }}
		 <tr>
            <td><a href="{{ $value.Location }}" target='_blank'>{{ $key }}</td>
            <td>{{ $value.Keyword }}</td>
        </tr>
		{{ end }}
    </tbody>
</table>

At this point, we can run the go webapp file, and visit http://127.0.0.1:8000/agg/ in our browser to see our giant table. One option we have to quickly and easily make this page a bit better is to use something like DataTables, which is a JavaScript plugin that can quickly improve your data-intensive tables. To apply this, we'll need to bring in jquery, the datatables css, and then the datatables javascript. To do this, we'll add the following to the top of our template:

<head>
    <script type="text/javascript" charset="utf8" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
    <link rel="stylesheet" type="text/css" href="//cdn.datatables.net/1.10.16/css/jquery.dataTables.css">
    <script type="text/javascript" charset="utf8" src="//cdn.datatables.net/1.10.16/js/jquery.dataTables.js"></script>
</head>

Next, at the end of our template we'll add:

<script>$(document).ready(function() {
    $('#fancytable').DataTable();
} );</script>  

Now, we need to give our table the id of "fancytable:"

<table id="fancytable">

We can also give it one of the DataTables classes, for now, I will go with "display"

<table id="fancytable" class="display">

This will give it some over interaction and such. From here, we can use the search box to search for specific terms. Since Donald Trump is the president, there should be many results if you begin to type "Trump." The search is live and immediate, which is neat. Here's the full template code just in case you're missing something:

<head>
    <script type="text/javascript" charset="utf8" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
    <link rel="stylesheet" type="text/css" href="//cdn.datatables.net/1.10.16/css/jquery.dataTables.css">
    <script type="text/javascript" charset="utf8" src="//cdn.datatables.net/1.10.16/js/jquery.dataTables.js"></script>
</head>

<h1>{{.Title}}</h1>

<table id="fancytable" class="display">
    <col width="35%">
    <col width="65%">
    <thead>
        <tr>
            <th>Title</th>
            <th>Keywords</th>
        </tr>
    </thead>
    <tbody>
        {{ range $key, $value := .News }}
         <tr>
            <td><a href="{{ $value.Location }}" target='_blank'>{{ $key }}</td>
            <td>{{ $value.Keyword }}</td>
        </tr>
        {{ end }}
    </tbody>
</table>

<script>$(document).ready(function() {
    $('#fancytable').DataTable();
} );</script>  

Whew, alright, so we've got our first working news aggregator, a proof of concept... but jeez this thing is slow. I personally see ~5000ms. There are two major reasons for the speed here. The first reason for some lag is the passing of all of this data to the browser. We've got almost 1500 rows in this table, lots of keywords...etc. This is an expensive page overall. That said, the *main* draw on our speed is more likely to be the fact that we're visiting each page linearly, in a chain, meaning we request for the first sitemap data, wait for the response, do some stuff, then visit the next sitemap, wait for a response...etc. Those response waits add up. What we'll be talking about in the coming tutorials is options we have for speeding things up.

The next tutorial:





  • Introduction to the Go Programming Language
  • Go Language Syntax
  • Go Language Types
  • Pointers in Go Programming
  • Simple Web App in Go Programming
  • Structs in the Go Programming Language
  • Methods in Go Programming
  • Pointer Receivers in Go Programming
  • More Web Dev in Go Language
  • Acessing the Internet in Go
  • Parsing XML with Go Programming
  • Looping in Go Programming
  • Continuing our Go Web application
  • Mapping in Golang
  • Mapping Golang sitemap data
  • Golang Web App HTML Templating
  • Applying templating to our Golang web app
  • Goroutines - Concurrency in Goprogramming
  • Synchronizing Goroutines - Concurrency in Golang
  • Defer - Golang
  • Panic and Recover in Go Programming
  • Go Channels - Concurrency in Go
  • Go Channels buffering, iteration, and synchronization
  • Adding Concurrency to speed up our Golang Web Application