Using Go to Render HTML

I wanted to create some sort of administrative user interface for a backend messaging webserver. I really like Rust, but had a hard time finding a crate that could reliably render HTML while also providing session authentication and security. I've done some simple HTML templating with Go before, and it seemed like a good choice for this project. This page is dedicated to exploring how I created a frontend user interface allowing users to POST JSON content to a backend service.



  • This is what an HTML template looks like in Go.
  • The majority is standard HTML, but notice that if-else branching and an Error variable is present (see double curly brackets).
 
<div class="container d-flex justify-content-center align-items-center min-vh-100">
<div class="card shadow" style="width: 100%; max-width: 400px;">
    <div class="card-body">
        <h2 class="card-title text-center mb-4">Login</h2>
        {{if .Error}}
        <div class="alert alert-danger">
            {{.Error}}
        </div>
        {{end}}
        <form action={{.LoginPath}} method="POST">
            <input type="hidden" name="gorilla.csrf.Token" value="{{.CSRF}}">

            <div class="form-group">
                <label for="username">Username</label>
                <input type="text" class="form-control" id="username" name="username" required>
            </div>

            <div class="form-group">
                <label for="password">Password</label>
                <input type="password" class="form-control" id="password" name="password" required>
            </div>

            <button type="submit" class="btn btn-primary btn-block">Login</button>
        </form>
    </div>
</div>
</div>                        
                        
login form




  • Go allows data to be passed to the template using a handler function. In this case an error message and and CSRF token are passed to the handler function, along with a configuration that specifies the path at which to submit the login form data.
  • When a visitor types in the address specified later on in this page in the RunServer() function, this HTML will be rendered by Go and served to the client as any static HTML page.
  • The renderLogin() function defines how this will happen.
  • I've defined another function that will process the form data sent to the the server when a visitor attempts to login. This function is responsible for authenticating users based upon the credentials that they provided.
  • Usernames are stored and compared as plaintext, but passwords are hashed and these hashes used in constant time comparisons.
  • I've done my best to follow security best practices, but I am still not going to expose the login logic here.
func (h *HandlerWithConfig) renderLogin(w http.ResponseWriter, r *http.Request, errorMsg string, csrfToken string) {
    tmpl, err := template.ParseFiles("templates/login.html")
    if err != nil {
        log.Printf("Template error: %v", err)
        http.Error(w, "Error loading template", http.StatusInternalServerError)
        return
    }

    data := struct {
        LoginPath string
        Error     string
        CSRF      string
    }{
        LoginPath: h.Config.LoginPath,
        Error:     errorMsg,
        CSRF:      csrfToken,
    }

    if err := tmpl.Execute(w, data); err != nil {
        log.Printf("Template execution error: %v", err)
        http.Error(w, "Error rendering template", http.StatusInternalServerError)
        return
    }
}
                    




  • Once a user authenticates, they will be redirected to a page containing form fields used to generate a JSON payload sent to my backend server.
  • range .Languages specifies that an array of Languages will be used to generate a drop down displaying the name of each Language.
  • Similar logic is used for displaying Durations.
<div class="container mt-5">
<h2 class="mb-4">Send Koradi Bulletin</h2>
<form action="{{.SubmitPath}}" method="POST">
    <!-- Add CSRF Token -->
    <input type="hidden" name="gorilla.csrf.Token" value="{{.CSRF}}">
    
    <div class="form-group">
        <label for="language">Select Language:</label>
        <select class="form-control" id="language" name="language">
            {{range .Languages}}
            <option value="{{.}}">{{.}}</option>
            {{end}}
        </select>
    </div>

    <div class="form-group">
        <label for="duration">Select Duration:</label>
        <select class="form-control" id="duration" name="duration">
            {{range .Durations}}
            <option value="{{.}}">{{.}}</option>
            {{end}}
        </select>
    </div>

    <div class="form-group">
        <label for="title">Title:</label>
        <input type="text" class="form-control" id="title" name="title" required>
    </div>

    <div class="form-group">
        <label for="message">Message:</label>
        <textarea class="form-control" id="message" name="message" rows="4" required></textarea>
    </div>

    <div class="form-group">
        <label for="image_url">Image URL (Optional):</label>
        <input type="text" class="form-control" id="image_url" name="image_url">
    </div>

    <button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>                  
                        
login form




  • The template is supplied data as shown in the handler function used to populate those drop downs, specify an endpoint to POST the data to, and include a CSRF token, all similar to before.
  • I've defined another function (not shown) that will serialize the form data into a JSON payload expected by my backend server and then transmit that data securely (using TLS) after adding a security header that the backend server will check.
  • If this process results in any errors, they will be redirected to another template that will display the errors, or else display a success message.
func (h *HandlerWithConfig) renderForm(w http.ResponseWriter, r *http.Request) {
	data := MessageFormData{
		Languages:  []string{"English", "Spanish", "French", "Italian", "Portuguese", "German"},
		Durations:  []string{"Hour", "Day", "Week", "Quarter", "Year"},
		SubmitPath: h.Config.SubmitMessagePath,
		CSRF:       csrf.Token(r),
	}

	tmpl, err := template.ParseFiles("templates/message.html")
	if err != nil {
		http.Error(w, "Error loading template", http.StatusInternalServerError)
		return
	}
	tmpl.Execute(w, data)
}
                            

Final Steps

The final step is to map endpoints to handler functions and run an HTTPS server. Server specifications can be defined to control request and response behavior, and additional middleware like logging and security headers can be layered on. Go has a lot of built in web functionality that makes this easy:

mux := http.NewServeMux()
mux.HandleFunc(cfg.Paths.Login, handler.loginHandler)
mux.Handle(cfg.Paths.Submit, handler.authMiddleware(http.HandlerFunc(handler.handleSubmit)))
mux.Handle(cfg.Paths.Create, handler.authMiddleware(http.HandlerFunc(handler.renderForm)))

server := &http.Server{
    Addr:         cfg.Server.Address,
    Handler:      middleware.Chain(mux, security, csrf, logging),
    TLSConfig:    tlsConfig,
    ReadTimeout:  cfg.Server.ReadTimeout,
    WriteTimeout: cfg.Server.WriteTimeout,
    IdleTimeout:  cfg.Server.IdleTimeout,
}
                        


Top Of Page