Build a Blog With Go Templates
This article was written by an external contributor.
Go templates are a robust feature used to generate text or HTML outputs based on data in a Go program. You can customize how the data is displayed by passing an object to a template. Templates are often used to generate web pages, emails, and other text-based outputs. A very popular real-life use of a Go template is in the kubectl command line tool, where you can pass a template to the --template
flag to customize the output to your needs.
Templates overview
In Go, there are two packages that provide the templating functionality: the text/template and html/template packages. Both offer the exact same set of interfaces; the only difference is that the latter automatically secures the HTML output against various attacks. This makes html/template
a better choice for generating HTML output, and is why this article will use the html/template
package.
A template is simply a string with some special commands called actions, which are enclosed in double curly braces. The actions are used to access or evaluate data, or to control the template’s structure.
Here’s an example template:
tmpl := "Hello {{.Name}}!"
The above template has a single action that prints the value of the Name
field of the data object passed to the template. The .
character in the action refers to the data object passed to the template, and .Name
accesses the Name
field of the object.
To render the template, you must parse the template with the Parse
function and use the Execute
function, which takes a writer and the data object as arguments and writes the output to the writer.
// Defining the data to pass type User struct { Name string } user := User{"James"} t, err := template.New("test").Parse(tmpl) if err != nil { panic(err) } err = t.Execute(os.Stdout, user) if err != nil { panic(err) }
The above outputs Hello James!
to the console.
Apart from accessing data, you can use actions like if
to conditionally render content, and range
to iterate over a collection. You can also define your own functions and use them in the template. A complete overview of templates can be found here.
Building a Blog With Templates
To follow along with the tutorial, you need to have Go installed and set up on your system. You’ll also want to install GoLand.
You can find the code for this tutorial on GitHub. Feel free to clone and explore the code, or to follow along with the tutorial to create the application from scratch.
Creating a new project
Start GoLand and click on the New project button. Choose the Go option and provide a name, such as “GoBlog”.
Click on Create to create the project. Create a file main.go
at the root of the project with the following content:
package main import ( "database/sql" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" _ "github.com/mattn/go-sqlite3" )
You’ll notice the modules are highlighted in red. This is because these modules have not been downloaded yet.
GoLand can automatically download them for you. Simply hover over any highlighted entries and click on Sync dependencies of GoBlog in the popup that appears.
After a few seconds, the modules will be downloaded, and the red highlighting will disappear.
Please note that if you have Auto-format on save enabled, as soon as the file is saved (for example, when the window loses focus), GoLand will format the code and remove the unused dependencies. You can disable autosaving when switching to a different application or a built-in terminal in Preference / Settings | Tools | Actions on Save | Configure autosave options… | Autosave.
You can also add imports manually. The Sync Dependencies action downloads the packages to cache memory, and when you add imports manually, you get autocomplete suggestions.
As the first step of coding the server, declare the following two global variables in main.go
:
var router *chi.Mux var db *sql.DB
While you can use the standard library to set up the REST server, this article will use the chi router for this purpose – chi is an easy-to-use, lightweight, and superfast router that comes bundled with many features.
router
will store the chi router instance, and db
will store the database object.
Define the Article
struct, which will have a title, content, and ID:
type Article struct { ID int `json:"id"` Title string `json:"title"` Content template.HTML `json:"content"` }
Note that for Content
, you’re using template.HTML
instead of string
. Because the content will be rich text and rendered as HTML, you need to use template.HTML
to prevent the HTML from being escaped when the templates are rendered.
Setting up the database
Create the file db.go
, which will house the functionalities related to the database. Start by importing the necessary module:
package main import ( "database/sql" )
In this article, you’ll use the database/sql
package to interact with the database. However, the templating system is unaffected by your choice of database package; you can use it with any other database package that you like.
This tutorial will use the SQLite database to store the data, but you can use any other SQL database, such as MySQL or PostgreSQL. A complete list of databases supported by database/sql
can be found here.
Define the connect
function, which will create the initial connection to the database. You will be using a local file named data.sqlite
to store the data. If the file doesn’t exist, it will be created automatically.
func connect() (*sql.DB, error) { var err error db, err = sql.Open("sqlite3", "./data.sqlite") if err != nil { return nil, err } sqlStmt := ` create table if not exists articles (id integer not null primary key autoincrement, title text, content text); ` _, err = db.Exec(sqlStmt) if err != nil { return nil, err } return db, nil }
You’ll notice the SQL statement is highlighted in GoLand.
GoLand has a built-in database plugin that can connect to different databases and lets you query, create, and manage tables, all without leaving the IDE. To use the plugin, you need to configure a data source. Simply click on the highlighted SQL statement, and in the context actions menu (the yellow bulb icon), click Configure data source. Alternatively, you can click in highlighted area and press Alt+Enter (⌥ ↩) to see available intention actions.
In the dialog that appears, click on +
to add a new data source, and select SQLite as the database type. Provide a name for the database, such as “Database”, and use data.sqlite
as the file name. If the appropriate database driver is not installed, you’ll be prompted to install it. Once ready, click on OK to save the data source.
GoLand will create the file if it does not exist, connect to it, and open a database console. This is where you can write the SQL queries. You can also see the newly created database in the Database tool window on the right side.
Return to the code, place the cursor on the create table
SQL statement, and press Ctrl+Enter (⌘↩). Select the console from the popup menu, and GoLand will run the query in the console.
You will see from the logs that the table has been successfully created; this will also be reflected in the database tool window.
Now let’s write the rest of the database functions. The dbCreateArticle()
function will create a new article in the database from an Article
struct:
func dbCreateArticle(article *Article) error { query, err := db.Prepare("insert into articles(title,content) values (?,?)") defer query.Close() if err != nil { return err } _, err = query.Exec(article.Title, article.Content) if err != nil { return err } return nil }
Here, you have a prepared statement that will be used to insert the article into the database. The ?
placeholders will be replaced by the values of the Title
and Content
fields of the Article
struct. You’ll notice GoLand correctly identifies the embedded SQL statement and highlights it.
Like before, you can run the query by placing the cursor on the SQL statement and pressing Ctrl+Enter (⌘↩). This time, you’ll be prompted to provide values for the placeholder. Enter the values and click on Execute.
Note that you need to add quotes to the values, since these will be substituted verbatim.
You can double-click the table name in the database tool window to see all the rows in the database. The newly created article should also show up here.
The dbGetAllArticles()
function will return all the articles in the database as a slice of Article
structs:
func dbGetAllArticles() ([]*Article, error) { query, err := db.Prepare("select id, title, content from articles") defer query.Close() if err != nil { return nil, err } result, err := query.Query() if err != nil { return nil, err } articles := make([]*Article, 0) for result.Next() { data := new(Article) err := result.Scan( &data.ID, &data.Title, &data.Content, ) if err != nil { return nil, err } articles = append(articles, data) } return articles, nil }
The dbGetArticle()
function will return a single article from the database based on the ID:
func dbGetArticle(articleID string) (*Article, error) { query, err := db.Prepare("select id, title, content from articles where id = ?") defer query.Close() if err != nil { return nil, err } result := query.QueryRow(articleID) data := new(Article) err = result.Scan(&data.ID, &data.Title, &data.Content) if err != nil { return nil, err } return data, nil }
The final pieces of the puzzle are the dbUpdateArticle()
and dbDeleteArticle()
functions, which will update and delete an article from the database, respectively:
func dbUpdateArticle(id string, article *Article) error { query, err := db.Prepare("update articles set (title, content) = (?,?) where id=?") defer query.Close() if err != nil { return err } _, err = query.Exec(article.Title, article.Content, id) if err != nil { return err } return nil } func dbDeleteArticle(id string) error { query, err := db.Prepare("delete from articles where id=?") defer query.Close() if err != nil { return err } _, err = query.Exec(id) if err != nil { return err } return nil }
Creating the routes
With the database functions complete, return to main.go
to write the rest of the server. Start by writing the catch()
function, which simply panics in case of an error:
func catch(err error) { if err != nil { fmt.Println(err) panic(err) } }
The main()
function is where you will set up the routes and the middlewares. Add the following code in main.go
:
func main() { router = chi.NewRouter() router.Use(middleware.Recoverer) var err error db, err = connect() catch(err) router.Use(ChangeMethod) router.Get("/", GetAllArticles) router.Route("/articles", func(r chi.Router) { r.Get("/", NewArticle) r.Post("/", CreateArticle) r.Route("/{articleID}", func(r chi.Router) { r.Use(ArticleCtx) r.Get("/", GetArticle) // GET /articles/1234 r.Put("/", UpdateArticle) // PUT /articles/1234 r.Delete("/", DeleteArticle) // DELETE /articles/1234 r.Get("/edit", EditArticle) // GET /articles/1234/edit }) }) err = http.ListenAndServe(":8005", router) catch(err) }
Note the use of the Recoverer middleware. When the catch()
function panics, this middleware will recover the server, log the error with a stack trace, and send a 500 Internal Server Error response to the client.
The above code sets up the following routes:
GET /
: Displays all the articles in the database.GET /articles
: Displays the form to create a new article.POST /articles
: Creates a new article in the database.GET /articles/{articleID}
: Displays a single article.PUT /articles/{articleID}
: Updates an article in the database.DELETE /articles/{articleID}
: Deletes an article from the database.GET /articles/{articleID}/edit
: Displays the form to edit an article.
In addition to the routes, the code sets up two middlewares:
ChangeMethod
: This middleware will change the request method toPUT
orDELETE
if the request method isPOST
and the form field_method
is set toPUT
orDELETE
, respectively. This is required because HTML forms only supportGET
andPOST
methods.ArticleCtx
: This middleware will fetch the article from the database and store it in the request context. It will be used by the routes under the/articles/{articleID}
path.
Let’s write the ChangeMethod
function first. As mentioned before, it will look for the _method
form element and change the request method accordingly:
func ChangeMethod(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { switch method := r.PostFormValue("_method"); method { case http.MethodPut: fallthrough case http.MethodPatch: fallthrough case http.MethodDelete: r.Method = method default: } } next.ServeHTTP(w, r) }) }
The ArticleCtx
middleware will access the article ID from the URL parameters and fetch the article from the database. If the article is found, it will be stored in the request context. If the article is not found, the middleware will return a 404 status code:
func ArticleCtx(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { articleID := chi.URLParam(r, "articleID") article, err := dbGetArticle(articleID) if err != nil { fmt.Println(err) http.Error(w, http.StatusText(404), 404) return } ctx := context.WithValue(r.Context(), "article", article) next.ServeHTTP(w, r.WithContext(ctx)) }) }
The GetAllArticles()
function will use the dbGetAllArticles()
function to fetch all the articles from the database, and display them on a webpage by rendering a template. For now, let’s leave the rendering part empty:
func GetAllArticles(w http.ResponseWriter, r *http.Request) { articles, err := dbGetAllArticles() catch(err) fmt.Println(articles) //TODO: Render template }
The NewArticle()
function will simply display the form to create a new article. It doesn’t need to interact with the database, and as such, it’s pretty bare bones:
func NewArticle(w http.ResponseWriter, r *http.Request) { //TODO: Render template }
The CreateArticle()
function will be called when the form to create a new article has been submitted. It will extract the Title
and Content
fields from the form, and use the dbCreateArticle()
function to create a new article in the database. After that, it will redirect the user to the /
page:
func CreateArticle(w http.ResponseWriter, r *http.Request) { title := r.FormValue("title") content := r.FormValue("content") article := &Article{ Title: title, Content: template.HTML(content), } err := dbCreateArticle(article) catch(err) http.Redirect(w, r, "/", http.StatusFound) }
The GetArticle()
function will display a single article. Thanks to the ArticleCtx
middleware, you don’t need to fetch the article here – you can simply get it from the request context:
func GetArticle(w http.ResponseWriter, r *http.Request) { article := r.Context().Value("article").(*Article) fmt.Println(article) //TODO: Render template }
The EditArticle
function will display the form to edit an article:
func EditArticle(w http.ResponseWriter, r *http.Request) { article := r.Context().Value("article").(*Article) fmt.Println(article) // TODO: Render template }
The UpdateArticle()
function will be called when the form to edit an article has been submitted. It will extract the Title
and Content
fields from the form, and use the dbUpdateArticle()
function to update the article in the database. After that, it will redirect the user to the /articles/{articleID}
page:
func UpdateArticle(w http.ResponseWriter, r *http.Request) { article := r.Context().Value("article").(*Article) title := r.FormValue("title") content := r.FormValue("content") newArticle := &Article{ Title: title, Content: template.HTML(content), } err := dbUpdateArticle(strconv.Itoa(article.ID), newArticle) catch(err) http.Redirect(w, r, fmt.Sprintf("/articles/%d", article.ID), http.StatusFound) }
Finally, the DeleteArticle()
function will delete the article from the database and redirect the user to the /
page:
func DeleteArticle(w http.ResponseWriter, r *http.Request) { article := r.Context().Value("article").(*Article) err := dbDeleteArticle(strconv.Itoa(article.ID)) catch(err) http.Redirect(w, r, "/", http.StatusFound) }
Rendering templates
The routes are now ready; the next step is to render the templates. Create a templates
directory with an index.html
file within it, which will be rendered in the GetAllArticles()
function. Add the following code to the file:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>All articles</title> </head> <body> {{if eq (len .) 0}} Nothing to see here {{end}} {{range .}} <div> <a href="/articles/{{.ID}}">{{.Title}}</a> </div> {{end}} <p> <a href="/articles">Create new article</a> </p> </body> </html>
This template utilizes the if
and range
template functions. The if
function will check if the number of articles equals zero. If it is, it will display the Nothing to see here
message; otherwise, it will display the list of articles. The range
function will loop through the list of articles and display them. The .
variable refers to the slice of all articles passed to the template. Within the body of the range
function, the .
variable refers to a single article. The ID
and Title
fields of the article are accessed using the dot notation.
Let’s update the GetAllArticles()
function to render the template:
func GetAllArticles(w http.ResponseWriter, r *http.Request) { articles, err := dbGetAllArticles() catch(err) t, _ := template.ParseFiles("templates/index.html") err = t.Execute(w, articles) catch(err) }
Let’s see how the app looks so far. Right click on the project name in the Project sidebar, then go to the Run menu item and click go build GoBlog. This will build the project and run it.
Open your browser and navigate to http://localhost:8005
. You should see the article that you created previously.
Let’s now create the form to create a new article. Since the Content
field is a rich text, you’ll need a rich text editor. This article will use the TinyMCE editor. You’ll need to sign up for a free account to get an API key. Once you have the API key, create a new.html
file in the templates
directory and add the following code to it:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Create a new article</title> <script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script> <script> tinymce.init({ selector: '#mytextarea', }); </script> </head> <body> <h1>Create a new article</h1> <form method="post" action="/articles"> <input id="title" type="text" name="title" placeholder="Enter the title"> <textarea id="mytextarea" name="content"></textarea> <button id="submit" type="submit">Create</button> </form> </body> </html>
You’ll need to replace no-api-key
in the above code with your TinyMCE API key.
Note that this file simply displays a form. It’s not strictly a template, but rather a simple HTML file, so you don’t need to parse it using the template.ParseFiles()
function. You can simply use the http.ServeFile()
function to serve it. Add the following line to the NewArticle
() function:
http.ServeFile(w, r, "templates/new.html")
You can visit http://localhost:8005/articles
to see the form in action.
Make sure you restart the server if it is running. To do this, use the restart button next to the configurations menu.
Whenever you change Go files, you need to restart the server. This is not explicitly mentioned in the tutorial.
Nested templates
You may have noticed that the two templates you’ve created so far share a large chunk of code. The basic structure of the HTML page is the same in both templates – the only difference is the title, content, and scripts of the page. You can use nested templates to avoid repeating the same code in multiple templates. Create a base.html
file in the templates
directory and add the following code to it:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>{{ template "title" . }}</title> {{ template "scripts" }} </head> <body> {{ template "body" . }} </body> </html>
This template uses three other templates: title
, scripts
, and body
. You’ll define different versions of these templates, which will be substituted in base.html
. These templates will be used to display the title, scripts, and the body of the page. Also, note that the title
and body
templates pass the current object using the .
variable, enabling the nested templates to access it if needed.
Replace the contents of index.html
with the following code:
{{define "title"}}All articles{{end}} {{define "scripts"}}{{end}} {{define "body"}} {{if eq (len .) 0}} Nothing to see here {{end}} {{range .}} <div> <a href="/articles/{{.ID}}">{{.Title}}</a> </div> {{end}} <p> <a href="/articles">Create new article</a> </p> {{end}}
Using the define
function, you can define the nested templates. Here, you’ve simply extracted the title, body, and scripts (which are empty because this page did not load any scripts) into their own templates.
To make this work, you need to amend the GetAllArticles()
function to load the base.html
template in addition to index.html
:
t, _ := template.ParseFiles("templates/base.html", "templates/index.html")
Note the order of the templates. base.html
must come before index.html
.
Replace new.html
with the following code:
{{define "title"}}Create new article{{end}} {{define "scripts"}} <script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script> <script> tinymce.init({ selector: '#mytextarea', }); </script> {{end}} {{define "body"}} <form method="post" action="/articles"> <input type="text" name="title" placeholder="Enter the title"> <textarea id="mytextarea" name="content"></textarea> <button id="submit" type="submit">Create</button> </form> {{end}}
Since new.html
is now a template, http.ServeFile
won’t work anymore. You need to parse the template and execute it:
func NewArticle(w http.ResponseWriter, r *http.Request) { t, _ := template.ParseFiles("templates/base.html", "templates/new.html") err := t.Execute(w, nil) catch(err) }
At this point, you can create a new article using the form.
After clicking on Create, you’ll be redirected to the root URL, and the newly created article will appear in the list.
Observe that the rich text is stored as HTML in the database.
Create a file named article.html
, which will display a single article:
{{define "title"}}{{.Title}}{{end}} {{define "scripts"}}{{end}} {{define "body"}} <h1>{{.Title}} </h1> <div> {{.Content}} </div> <div> <a href="/articles/{{.ID}}/edit">Edit</a> <form action="/articles/{{.ID}}" method="post"> <input type="hidden" name="_method" value="DELETE"> <button type="submit">Delete</button> </form> </div> {{end}}
This page has a link to the edit page and a form to delete the article. Note the hidden _method
element. As discussed before, the presence of this element will convert the request to a DELETE
request courtesy of the ChangeMethod
middleware.
Modify the GetArticle
function to render the template:
func GetArticle(w http.ResponseWriter, r *http.Request) { article := r.Context().Value("article").(*Article) t, _ := template.ParseFiles("templates/base.html", "templates/article.html") err := t.Execute(w, article) catch(err) }
Clicking on the title of an article on the home page will now display the article.
Image uploading
TinyMCE supports image uploading by default. You can drag and drop images into the editor, and they’ll be converted into base64 strings and stored as part of the Content
field.
The image will show up on the article page, as well.
However, it’s not considered good practice to store images as base64 strings, so let’s add support for image uploading.
Add two new routes to the router:
func main() { ... router.Use(ChangeMethod) router.Get("/", GetAllArticles) router.Post("/upload", UploadHandler) // Add this router.Get("/images/*", ServeImages) // Add this router.Route("/articles", func(r chi.Router) { ... }) ... }
The /upload
route will handle the image upload and store them as files in the images directory, and the /images/*
route will serve the images.
Let’s write the UploadHandler
function:
func UploadHandler(w http.ResponseWriter, r *http.Request) { const MAX_UPLOAD_SIZE = 10 << 20 // Set the max upload size to 10 MB r.Body = http.MaxBytesReader(w, r.Body, MAX_UPLOAD_SIZE) if err := r.ParseMultipartForm(MAX_UPLOAD_SIZE); err != nil { http.Error(w, "The uploaded file is too big. Please choose a file that's less than 10MB in size", http.StatusBadRequest) return } file, fileHeader, err := r.FormFile("file") if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } defer file.Close() // Create the uploads folder if it doesn't already exist err = os.MkdirAll("./images", os.ModePerm) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Create a new file in the uploads directory filename := fmt.Sprintf("/images/%d%s", time.Now().UnixNano(), filepath.Ext(fileHeader.Filename)) dst, err := os.Create("." + filename) if err != nil { fmt.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } defer dst.Close() // Copy the uploaded file to the specified destination _, err = io.Copy(dst, file) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } fmt.Println(filename) response, _ := json.Marshal(map[string]string{"location": filename}) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) w.Write(response) }
This function creates a new file in the images
directory and copies the incoming file to this new file. It then returns the file’s location as a JSON response, which TinyMCE uses to link the image.
The ServeImages
function is pretty straightforward:
func ServeImages(w http.ResponseWriter, r *http.Request) { fmt.Println(r.URL) fs := http.StripPrefix("/images/", http.FileServer(http.Dir("./images"))) fs.ServeHTTP(w, r) }
It simply serves the files in the images
directory as static files.
The final step is to let TinyMCE know about the /upload
route. In new.html
, modify the tinymce.init
call with the following:
tinymce.init({ selector: '#mytextarea', plugins: 'image', toolbar: 'undo redo | blocks | image | ' + 'bold italic backcolor | alignleft aligncenter ' + 'alignright alignjustify | bullist numlist outdent indent | ' + 'removeformat | help', images_upload_url: "/upload", relative_urls : false, remove_script_host : false, convert_urls : true, });
The plugins
option loads the image upload plugin, and the toolbar
option adds the image upload button to the toolbar. The images_upload_url
option specifies the route to which the image will be uploaded. The relative_urls
, remove_script_host
, and convert_urls
options are used to convert the relative URLs returned by the /upload
route to absolute URLs.
The new article page will now show the image upload button in the toolbar.
Click on the Upload tab and upload any image you want.
It will be uploaded and linked to the article.
Finally, create the template edit.html
with the following code:
{{define "title"}}Create new article{{end}} {{define "scripts"}} <script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script> <script> tinymce.init({ selector: '#mytextarea', plugins: 'image', toolbar: 'undo redo | blocks | image | ' + 'bold italic backcolor | alignleft aligncenter ' + 'alignright alignjustify | bullist numlist outdent indent | ' + 'removeformat | help', images_upload_url: "/upload", relative_urls : false, remove_script_host : false, convert_urls : true, }); </script> {{end}} {{define "body"}} <form method="post" action="/articles/{{.ID}}"> <input type="text" name="title" value="{{.Title}}"> <textarea id="mytextarea" name="content">{{.Content}}</textarea> <input type="hidden" name="_method" value="PUT"> <button id="submit" type="submit" onclick="submitForm()">Edit</button> </form> {{end}}
It’s very similar to the new article form, except it populates the form with the article’s title and content. The action
attribute of the form is set to /articles/{id}
, and the _method
field is set to PUT
to indicate that the form is used to edit an article.
Render this template in the EditArticle
function:
func EditArticle(w http.ResponseWriter, r *http.Request) { article := r.Context().Value("article").(*Article) t, _ := template.ParseFiles("templates/base.html", "templates/edit.html") err := t.Execute(w, article) catch(err) }
You can now click on the Edit button on the article page to edit the article.
After editing, you’ll be redirected to the same page, now displaying the updated article.
You can also click on the Delete button to delete the article.
Conclusion
If you’d like to review all of the code for this project in one place, you can do so here. Templates in Go offer robust functionalities related to customizing output formats. The html/template
package in particular sees extensive use in web development due to its ability to output secure HTML. In this article, you learned how to use the html/template
package to create a simple blog application.