Twists of Templates
Introduction
Structuring our projects at the outset is vital for defining their future scalability and maintainability. As a Golang developer, beginning with a single file is convenient; without much ceremony, you can have a functioning piece. However, as our needs and project complexities expand, so must our organization's strategy.
Begin with a single file, and it's a breath of fresh air, simple and straightforward. However, as projects mature and requirements compound, that same structure might feel constricting, even chaotic.
In this chapter, we transition from a minimalist approach to a more structured directory system, exploring the reasons behind the introduction of key folder names like cmd, internal, and pkg, and understanding their importance.
In addition to directory structuring, we'll explore Go templates, covering the basics of Go's standard template system and delving into composition and partials. The goal is to provide a solid foundation for effectively using templates in Go.
Let's begin our hands-on journey, transitioning from a single-file setup to organizing features in their rightful places.
Project Layout
When developing in Go, one might come across the Standard Go Project Layout. It's worth noting, however, that despite its name, it isn't an official standard of the Go community. Even the project maintainers themselves acknowledge its unofficial nature.
This layout has sparked a multitude of discussions among Go developers. Some criticize it, while others adopt it enthusiastically. What cannot be denied is the influence it has had.
The structure it suggests addresses a common challenge: the absence of a widely accepted Go project layout. With various approaches in play, though they may not be dramatically different, diving into a new project can be cumbersome.
Adopting a common layout, such as the one this project suggests, streamlines on-boarding. Developers can quickly identify components, understand the project's flow, and become productive faster. This not only reduces the initial friction but also brings a cohesive feel to Go projects across the board.
I favor this approach not only because I've observed many real-world projects employing variations of this layout but also because, interestingly, even before I discovered this recommended structure, my projects naturally followed a pattern closely resembling it. It's as if this layout resonates with a collective intuition or a shared sense of organizational aesthetics.
While the 'standard' layout provides comprehensive guidance for several directories, we won't delve into each one here. The official repository offers in-depth explanations for all of them. However, for the scope of this article, we'll concentrate on three specific directories: cmd, internal, pkg, and finally, assets for storing templates. Let's get started.
The cmd
directory
The cmd
directory serves as the home for the main applications of a project. Ideally, each sub-directory within cmd
corresponds to the name of the executable you're aiming to produce. For example, /cmd/myapp
would contain the entry point for generating a myapp
binary.
When working with this directory, it's essential to keep things streamlined. It shouldn't be brimming with extensive code. If a piece of your code has the potential for reuse in other projects, then /pkg
is in its rightful place. On the other hand, if the code is project-specific or there's a desire to limit its reuse, /internal
that is where it should reside. This deliberate placement can provide clear guidelines and safeguard its intended use.
Often, projects utilize the cmd
directory to house a concise main
function, acting primarily as an entry point. This function typically imports and invokes code from the /internal
and /pkg
directories.
In scenarios where, in addition to the main web application, there's a need to deliver CLI tools like migration utilities, code generators, or CLI-based clients for app services, the cmd directory becomes particularly handy. Each tool can be neatly placed in its respective sub-directory, ensuring clarity and structured organization.
However, for this project, our primary application's entry point, the main.go launcher, will reside in the root folder. This approach isn't unusual. Should there be a need for additional executables in the future, they'll find their home in dedicated directories under cmd. This arrangement simplifies the utilization of Go's embedded package, making the integration of resources more straightforward and efficient, as we will see later.
The internal
directory
The internal
directory serves a special purpose within Go projects. It contains application and library code intended for private use, shielding it from being imported by other programs or libraries outside of its parent directory. What makes this directory unique is that its access restrictions are enforced by the Go compiler itself. If you'd like to dive deeper into this compiler behavior, refer to the Go 1.4 release notes.
For added organization within the internal
directory, you can employ nested directories to categorize and structure your private code further. This way, you can segregate different modules or functionalities of your application. For example, your primary application code might reside in /internal/app/myapp
, while specific utility functions or private libraries could be placed in directories like /internal/repo
or /internal/service
, among others.
It's worth noting that the above provides just a brief insight. For a comprehensive understanding of the internal
directory and its intricacies, refer to the official repository.
The pkg
directory
The pkg
directory offers a place to store Go libraries and packages intended for external consumption. The key purpose here is to group Go code separately, especially useful when the root directory houses numerous non-Go components. This structure makes it more straightforward to run various Go tools and keeps the root directory decluttered.
However, as your project grows and your root directory becomes increasingly busy, especially when dealing with external resources, the pkg layout can be beneficial.
Historically, the Go source code was once used pkg
for its packages, influencing many community projects to adopt the same convention. Even though it was later phased out from the Go core, the convention persisted in many other projects.
Moving our dummy handlers
When structuring a Go web application, the placement and naming of certain components often boil down to a balance between convention and clarity. Why name a directory 'controllers' instead of 'handlers,' especially since Go typically refers to web route functions with the latter name?
Despite some initial resistance, perhaps out of a desire for originality or avoiding common nomenclatures from other platforms, I've gravitated towards using terminology directly from the MVC (Model, View, Controller) framework to name our packages, particularly the ones containing the handlers.
The term controller
for our package, for instance, provides immediate clarity and alignment. This nomenclature resonates well, making the organizational intent evident and readily comprehensible, especially for developers familiar with other platforms where MVC is prevalent.
Given this context, let's refactor our code, moving the handlers from main.go
to a separate package located in /internal/controller/
.
Refactoring the code
In this code snippet, we are modifying the go.mod
file of a Go module. The go.mod
file is a crucial part of Go's module system, as it defines the module's identity, dependencies, and other configuration settings.
Initially, the go.mod
file looks like this:
module hello
go 1.21.0
require github.com/gorilla/mux v1.8.0
Here, the module "hello" defines its name as "hello." However, we want to change the module name to match the structure github.com/username/repo
To achieve this, we modify the go.mod file as follows
module github.com/solutioncrafting/plykan
go 1.21.0
require github.com/gorilla/mux v1.8.0
Please remember to replace solutioncrafting/plykan
with your actual GitHub username and the repo name e you've created. This way, your Go module will be correctly linked to your specific GitHub repository.
Now create a new directory: /internal/controller/
and within it, a file named boardcontroller.go
.
package controller
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
)
func BoardsIndexHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Listing all boards.")
}
func NewBoardHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Displaying form for new board.")
}
func CreateBoardHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Creating a new board.")
}
func ShowBoardHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
fmt.Fprintf(w, "Showing board with ID: %s.", id)
}
func EditBoardHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
fmt.Fprintf(w, "Editing board with ID: %s.", id)
}
func UpdateBoardHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
fmt.Fprintf(w, "Updating board with ID: %s.", id)
}
func DeleteBoardHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
fmt.Fprintf(w, "Deleting board with ID: %s.", id)
}
func DeleteConfirmHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
fmt.Fprintf(w, "Confirm delete for board with ID: %s.", id)
}
π Did you notice something? After relocating our functions to the new controller
package, we capitalized their initial letters. This change isn't arbitrary. In Go, the capitalization of the first letter of a function or variable determines its visibility outside the package. By making them start with uppercase letters, we're ensuring these functions are exportable and accessible from our main application, or any other package that might need them.
π While restructuring our application, I also opted to drop the "Handler" suffix from all the functions within the controller
package. Given that every function in this package is inherently a handler, it felt redundant to continuously state it. This not only simplifies our naming but also makes the code more concise and readable. Remember, context often alleviates the need for verbosity.
Then, in our main.go
, we'll import this new package and adjust the function calls:
package main
import (
"net/http"
"github.com/gorilla/mux"
"github.com/solutioncrafting/plykan.git/internal/controller"
)
func main() {
r := mux.NewRouter()
// Board routes
r.HandleFunc("/boards", controller.BoardsIndexH).Methods("GET")
r.HandleFunc("/boards/new", controller.NewBoard).Methods("GET")
r.HandleFunc("/boards", controller.CreateBoard).Methods("POST")
r.HandleFunc("/boards/{id}", controller.ShowBoard).Methods("GET")
r.HandleFunc("/boards/{id}/edit", controller.EditBoard).Methods("GET")
r.HandleFunc("/boards/{id}", controller.UpdateBoard).Methods("PUT")
r.HandleFunc("/boards/{id}", controller.DeleteBoard).Methods("DELETE")
r.HandleFunc("/boards/{id}/confirm-delete", controller.DeleteConfirm).Methods("GET")
http.ListenAndServe(":8080", r)
}
By restructuring our code in this manner, we achieve several things:
Separation of Concerns: Our main.go
is now more concise, focusing only on route configuration and server startup. The actual logic for each endpoint resides separately, leading to better maintainability. In a future article, we will also explore moving the routing definition to its own package to further enhance the organization of our codebase.
Clarity: By organizing our handlers under the controller
directory, we signal to developers that these handlers are essentially the controllers in the MVC pattern. This makes it easier to find and understand the application flow.
Scalability: As our application grows, we can easily add more controllers or even group them under sub-directories without cluttering the main application file.
Remember that you can enjoy a more reader-friendly experience by visiting our repository at https://github.com/solutioncrafting/plykan. While this platform may not provide the best code reading experience, GitHub offers a smoother interface.
Go templates
On embedding templates and assets
Given our progression in building Plykan, it's important to have a clear structure for our resources. The recommended directory structure for our templates, based on the standard Go Standard Layout, designates 'assets' as the appropriate directory for these resources.
βββ assets/
β βββ templates/
β β βββ default.tmpl
β β βββ pages/
β β β βββ board.tmpl
This setup will suffice for our current needs. However, as our application grows and we introduce more controllers, we'll need to refine and possibly expand this structure to stay organized.
Currently, we're referencing these templates directly from the file system, making it simpler during the development phase. However, as a hint of what's to come, Go offers a powerful feature that allows us to embed such resources directly into the compiled binary, offering both flexibility and deployment ease. We'll delve deeper into this in future articles.
Web applications demand dynamic content rendering, easy updates, and content customization. This is where Go's standard template system stands out. While many languages depend on external libraries for templating, Go has a built-in solution. This means a reliable, consistently maintained tool is always available without extra installations or configurations.
But Go's templates offer more than just injecting data into HTML. They come with features like conditional logic, loops, and custom functions, giving developers the flexibility to create intricately rendered web pages without compromising speed or adaptability.
Template composition
Web applications often have recurring patterns or sections across different pages: headers, footers, sidebars, and more. Instead of repeatedly defining these sections, Go's templating system supports composition, allowing developers to define and reuse smaller templates within larger ones.
We are building a Kanban board, presentation and user experience play a main role. Go's templating system allows for a neat composition, ensuring modularity and reusability. Let's see how we can leverage this feature for our board.
Imagine a default structure for our web pages:
For the Kanban board page, we'd like to customize the title and content:
π The above representation is a simplified, hard-coded version of a Kanban board. As we delve deeper into the development process, we'll be introducing more dynamics and functionality to this layout. For now, it serves as a foundational structure, giving us an idea of what the final board might look like.
Go templates provide a powerful way to structure web pages into modular components. While these templates are often stored in files with .tmpl
extensions, it's essential to understand that a single file can contain several distinct templates, each enclosed within its block. These blocks define different parts of a web page, such as headers, content sections, or footers.
Let's consider a scenario where we have two .tmpl
files: default.tmpl
and board.tmpl
. In default.tmpl
, we define a template default
that serves as the primary structure of our web page. Within this default
template, we reference smaller templates named head
(representing the content of an HTML <head>
section) and "main" (serving as the primary content holder).
Surprisingly, in board.tmpl
, we find templates named head and main. It's important to note that these templates in board.tmpl
reference the blocks within that single template file.
The real magic happens when we connect these templates. In our primary template, default
we use the {{template "name" .}}
syntax to incorporate smaller nested one. The dot .
signifies the current data being processed, which is provided when rendering the template. By using this syntax, we seamlessly assemble the various components of our web page. The head
template from default.tmpl
effortlessly integrates with the <head>
section, while the main
template fills the main content area. This approach unifies our templates, creating a cohesive web page composed of individual modular components.
At first glance, this template assembly might seem a bit complex, but as we explore how it works and get some hands-on experience, you'll find it becomes a natural way to structure them for your web applications.
We'll start by defining a board data struct. For simplicity, let's assume our board has three columns: ToDo, In Progress, and Done, each containing a slice of tasks. While this initial structure serves as our starting point, it's important to note that our application is a work in progress, and as we develop it incrementally, we'll adapt and refine the data structure as needed.
type Page struct {
Head HeadData
Board BoardData
}
type HeadData struct {
Title string
Description string
}
type BoardData struct {
ID string
ToDoTasks []Task
InProgressTasks []Task
DoneTasks []Task
}
type Task struct {
ID string
Name string
Detail string
}
Next, we'll adjust our ShowBoard
function in internal/controller/board.go
to provide some dummy data
- The code begins by extracting a variable "id" from the request URL. While in a real-world scenario, this ID would typically represent a specific board's identifier, it's important to note that, at this stage, we are working with not real dynamic data. As such, the
id
hardcoded value extracted from the URL doesn't significantly affect the data displayed on the Kanban board; it serves primarily for illustrative purposes - Next, it initializes dummy data (
sampleToDoTasks
,sampleInProgressTasks
,sampleDoneTasks
). These dummy tasks represent the columns (To Do, In Progress, Done) displayed on the Kanban board. In a real application, this data would typically come from a database or some other data source. However, for the purpose of this example, it's hard-coded for now. - The
pageData
structure is created to organize all the data needed for rendering the web page. It includesHeadData
metadata andBoardData
for the Kanban board-related information. - Using
template.ParseFiles
, the Go template engine reads and parses HTML templates, likedefault.tmpl
andboard.tmpl
, from specified file paths. This process not only captures the template content but also understands template syntax, such as{{template "name" .}}
. Importantly, it ensures that all template references within these files can be satisfied, allowing templates to reference and connect with one another during rendering, forming a cohesive web page structure. - If there's an error during template parsing, we return a
500 - Internal Server Error response
. - Finally, the
ts.ExecuteTemplate
function acts like a bridge between our web page's blueprint and the actual content we want to display. It takes the "default" template, which defines the structure of our page, and seamlessly integrates the data from thepageData
structure into the designated areas within the template. This process ensures that our web page is assembled correctly, and presented to the user - If there's an error during template execution, it also returns a
500 - Internal Server Error
response.
Updating the main.tmpl
It's not uncommon to start with hardcoded content to understand the fundamental structure and rendering process. However, as the application grows, you'll quickly realize the need for dynamic content rendering. This is especially true for a Kanban board, where tasks may frequently change, and the content isn't static.
Consider the previous hardcoded at assets/templates/pages/board.tmpl
, this template rigidly displays three columns: "ToDo," "In Progress," and "Done." It serves well as a foundational step, but as we proceed, we want to render lists of tasks within each of these columns based on actual data.
Here's how our template will evolve, the range
action in Go templates do the trick. In our updated implementation, we utilized range
to iterate over and display tasks in each column. In Go templates, the range
action allows you to loop over slices or maps, making it easy to render dynamic lists of items.
With the range action, tasks within each column are no longer static. They're rendered dynamically based on the data we pass to the template. By iterating over each task in .ToDo
, .InProgress
, and .Done
, we can display an up-to-date list in each column without the need for manual updates.
The magic here lies in the dot (.
) notation, which serves as a reference to the data we provide when rendering the template. In this case, it's the pageData
struct created in the ShowBoard
function. So when we use .ToDo
, .InProgress
, and .Done
in the template, they act as 'keys' to extract the respective data frompageData
, making sure that the tasks are seamlessly integrated into the Kanban board layout.
This enhanced template allows our Kanban board to showcase currently data, with the potential for future dynamic updates from the database. This sets the stage for further interactivity and dynamic content updates.
Partials
When working with partial templates, the idea is to create a reusable template that can be configured or customized for different use cases. In our case, we want to create a single partial template for Kanban columns that can adapt to different columns (e.g., "To Do", "In Progress," "Done") while maintaining a consistent structure.
Let's add assets/templates/partials/column.tmpl
$ mkdir -p assets/templates/partials
$ touch assets/templates/partials/column.tmpl
To get this new dir structure
Creating a partial template
Begin by creating a single partial template for Kanban columns. This template will serve as a blueprint for all columns and should include the common structure, such as headers and placeholders for tasks:
We also need to update the board.tmpl
if we want to make it use this new partial
In the updated code, we've introduced a range loop that iterates over the three columns, allowing us to dynamically render each one. Within the loop, we use {{template "kanbanColumn" $column}}
to pass each individual column, represented by the variable $column
, to the kanbanColumn
template. This enables us to reuse the kanbanColumn
template for all columns, making our code more modular and efficient.
Let's run the app to see how it looks
$ go run main.go
Open the browser at http://localhost:8080/boards/111
`
πThe `111` at the end of the URL could be any other value since we are not using that identifier to get the board from the database.
You should see something like this
The board doesn't look very appealing, right? It doesn't even look like a board at all as there are no styles applied. Let's add some CSS magic to it.
Entering TailwindCSS
Now that we have our Kanban board structured, it's time to make it visually appealing. We'll use Tailwind CSS, which is known for its simplicity and flexibility. Tailwind provides a wide range of pre-built classes to help us style our web page components without having to write extensive custom CSS.
We also need to update the default.tmpl
in order to use it.
We're adding the `<link ...>` tag on line 5 to incorporate Tailwind CSS, allowing us to enhance the styling of our board. For simplicity, at least for now, we're fetching it from a content delivery network (CDN) rather than installing it locally.
Now we can sprinkle a bit of CSS on our templates to make our board look like a real board.
Configuring the Partial Dynamically
To make this partial template adaptable for different columns, we can dynamically configure it when rendering. In your Go code, define a ColumnData
struct that contains the necessary information for each column:
type ColumnData struct {
ColumnName string // "To Do," "In Progress," "Done"
Tasks []string // List of tasks for the column
Color string // Background color class
}
Now, when rendering the template for each column, create an instance of ColumnData
with the appropriate values and execute the kanbanColumn
partial template.
Therefore, we need to update our pageData
in the ShowBoard
function of the board controller.
We keep our code DRY and reduce redundancy. Any changes or improvements to the column structure can be made in one place, ensuring consistency across columns. It also simplifies the process of adding new columns to our Kanban board in the future.
Let's run the app again and take a look at our new board with sleek styling.
It's starting to look pretty neat, isn't it?
We've made significant progress today, including our initial refactor and the introduction of templates. Now, let's have a sneak peek at what's on the horizon for our upcoming chapter.
Up Next: Logging and Dependency Injection
We'll now take a brief detour from perfecting the visual aspects of our pages to delve into fundamental concepts. This includes exploring leveled logging and introducing dependency injection, unlocking various capabilities. For instance, we'll seamlessly integrate the logger into our handlers and, in the future, apply this approach to streamline the usage of configuration and other dependencies during initialization.
Cheers!
References
- TailwindCSS
- Refactor: refactor/moving-handlers
- Templates: feature/templates