├── .gitignore ├── .travis.yml ├── Dockerfile ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── main.go ├── main_test.go ├── static └── golang.png └── templates ├── footer.html ├── header.html ├── hello.html ├── index.html └── invalid.html /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.txt 2 | golang-templates-example 3 | rice-box.go 4 | target 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.15 5 | - tip 6 | 7 | install: 8 | - "go version" 9 | - "go env" 10 | 11 | script: 12 | - "make" 13 | 14 | after_success: 15 | - "bash <(curl -s https://codecov.io/bash)" 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang 2 | 3 | EXPOSE 8080 4 | 5 | ENV PROJECT /go/src/github.com/jmcfarlane/golang-templates-example 6 | 7 | ADD . $PROJECT 8 | WORKDIR $PROJECT 9 | 10 | RUN go env 11 | RUN go version 12 | RUN go get github.com/GeertJohan/go.rice/rice 13 | RUN go get -t ./... 14 | run go generate 15 | RUN go install 16 | 17 | CMD ["/go/bin/golang-templates-example"] 18 | 19 | 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = /bin/sh 2 | 3 | # all: Run tests and perform build 4 | all: deps test 5 | go build 6 | 7 | # help: Print help information 8 | help: 9 | @echo ">> Help info for supported targets:" 10 | @grep -E '^# [-a-z./]+:' Makefile | grep -v https:// | sed -e 's|#| make|g' | sort 11 | 12 | # coverage: Display code coverage in html 13 | coverage: test 14 | @echo ">> Rendering code coverage" 15 | go tool cover -html=coverage.txt 16 | @echo echo "Success 👍" 17 | 18 | # generate: Run go generate for all packages 19 | generate: 20 | @echo ">> Running codegen" 21 | go generate -v 22 | 23 | # test: Run go test 24 | test: generate 25 | @echo ">> Running tests" 26 | go test -race -coverprofile=coverage.txt -covermode=atomic -v 27 | @echo echo "Success 👍" 28 | 29 | # deps: Install dependencies 30 | deps: 31 | @echo ">> Installing dependencies" 32 | @go get github.com/GeertJohan/go.rice/rice 33 | @echo echo "Success 👍" 34 | 35 | # vet: Run go vet 36 | vet: generate 37 | @echo ">> Running go vet" 38 | go vet -x 39 | @echo echo "Success 👍" 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A simple Golang templates example 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/jmcfarlane/golang-templates-example)](https://goreportcard.com/report/jmcfarlane/golang-templates-example) 4 | [![Build Status](https://img.shields.io/travis/jmcfarlane/golang-templates-example/main.svg)](https://github.com/jmcfarlane/golang-templates-example/tree/main) 5 | [![codecov](https://codecov.io/gh/jmcfarlane/golang-templates-example/branch/main/graph/badge.svg)](https://codecov.io/gh/jmcfarlane/golang-templates-example) 6 | 7 | During the process of learning Golang templates, certain aspects were 8 | confusing to me. The goal of this little repo is to document what I 9 | eventually wound up doing. Hopefully with feedback this repo could 10 | serve as an example of at least one way to use templates effectively. 11 | 12 | By **no** means is this intended to be a proper (*or even correct*) 13 | howto on Golang templates, rather it's just what I've learned so far. 14 | Here's what I was trying to accomplish: 15 | 16 | 1. Have a directory of templates (`header.html`, `foobar.html`, etc). 17 | 1. Have a directory of static files (css, images, etc). 18 | 1. Use some templates as full pages (`about.html`, `hello.html`). 19 | 1. Use some templates as partials (`header.html`, `footer.html`). 20 | 1. Serve static content in a manner similar to 21 | [http.FileServer](https://golang.org/pkg/net/http/#example_FileServer). 22 | 1. Exclude templates from the static files being served. 23 | 1. Support custom template functions. 24 | 1. Compile everything into a single static binary (including templates 25 | and static files). 26 | 27 | ## Installation 28 | 29 | ``` 30 | go get github.com/GeertJohan/go.rice/rice 31 | go get -d github.com/jmcfarlane/golang-templates-example 32 | ``` 33 | 34 | ## Run 35 | 36 | ``` 37 | cd $GOPATH/src/github.com/jmcfarlane/golang-templates-example 38 | go get -t ./... 39 | go generate 40 | go test -v 41 | go build 42 | ./golang-templates-example 43 | curl http://localhost:8080 44 | ``` 45 | 46 | ## Optionally use make 47 | 48 | If you have `make` installed, you can try this repo by doing: 49 | 50 | ``` 51 | make 52 | ./golang-templates-example 53 | ``` 54 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jmcfarlane/golang-templates-example 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/GeertJohan/go.rice v1.0.2 7 | github.com/julienschmidt/httprouter v1.3.0 8 | github.com/stretchr/testify v1.7.0 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg= 2 | github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= 3 | github.com/GeertJohan/go.rice v1.0.2 h1:PtRw+Tg3oa3HYwiDBZyvOJ8LdIyf6lAovJJtr7YOAYk= 4 | github.com/GeertJohan/go.rice v1.0.2/go.mod h1:af5vUNlDNkCjOZeSGFgIJxDje9qdjsO6hshx0gTmZt4= 5 | github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw= 6 | github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= 7 | github.com/daaku/go.zipexe v1.0.0 h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY= 8 | github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= 13 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 14 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= 15 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 16 | github.com/nkovacs/streamquote v1.0.0 h1:PmVIV08Zlx2lZK5fFZlMZ04eHcDTIFJCv/5/0twVUow= 17 | github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 22 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 24 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 25 | github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8= 26 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 27 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 28 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 29 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 30 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 31 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate rice embed-go 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "html/template" 9 | "log" 10 | "net/http" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | 15 | rice "github.com/GeertJohan/go.rice" 16 | "github.com/julienschmidt/httprouter" 17 | ) 18 | 19 | // Model of stuff to render a page 20 | type Model struct { 21 | Title string 22 | Name string 23 | } 24 | 25 | // Templates with functions available to them 26 | var ( 27 | templateMap = template.FuncMap{ 28 | "Upper": func(s string) string { 29 | return strings.ToUpper(s) 30 | }, 31 | } 32 | templates = template.New("").Funcs(templateMap) 33 | templateBox *rice.Box 34 | ) 35 | /* 36 | func newTemplate(path string, _ os.FileInfo, _ error) error { 37 | if path == "" { 38 | return nil 39 | } 40 | templateString, err := templateBox.String(path) 41 | if err != nil { 42 | log.Panicf("Unable to extract: path=%s, err=%s", path, err) 43 | } 44 | if _, err = templates.New(filepath.Join("templates", path)).Parse(templateString); err != nil { 45 | log.Panicf("Unable to parse: path=%s, err=%s", path, err) 46 | } 47 | return nil 48 | } 49 | */ 50 | 51 | func newTemplate(path string, fileInfo os.FileInfo, _ error) error { 52 | if path == "" { 53 | return nil 54 | } 55 | /* 56 | * takeRelativeTo function will take the absolute path 'path' which is by default passed to 57 | * our 'newTemplate' by Walk function, and will eliminate the intial part of the path up to the end of the 58 | * specified directory 'afterDir' ('templates' in this case). Then it will return the rest starting from 59 | * the very end of afterDir. If the specified afterDir has more than 1 occurances in the path, 60 | * only the first occurance will be considered and the other occurances will be ignored. 61 | * eg, If path = "/home/Projects/go/website/templates/html/index.html", then 62 | * relativPath := takeRelativeTo(path, "templates") returns "/html/index.html" ; 63 | * If path = "/home/Projects/go/website/templates/testing.html", then ; 64 | * relativPath := takeRelativeTo(path, "templates") returns "/testing.html" ; 65 | * If path = "/home/Projects/go/website/templates/html/templates/components/footer.html", then 66 | * relativPath := takeRelativeTo(path, "templates") returns "/html/templates/components/footer.html" . 67 | */ 68 | takeRelativeTo := func(givenpath string, afterDir string) string { 69 | if strings.Contains(givenpath, afterDir+string(filepath.Separator)) { 70 | wantedpart := strings.SplitAfter(givenpath, afterDir)[1:] 71 | return filepath.Join(wantedpart...) 72 | } 73 | return givenpath 74 | } 75 | //if path is a directory, skip Parsing template. Trying to Parse a template from a directory caused an error, now fixed. 76 | if !fileInfo.IsDir() { 77 | //get relative path starting from the end of 'templates' . 78 | relativPath := takeRelativeTo(path, "templates") 79 | templateString, err := templateBox.String(relativPath) 80 | if err != nil { 81 | log.Panicf("Unable to extract: path=%s, err=%s", relativPath, err) 82 | } 83 | if _, err = templates.New(filepath.Join("templates", relativPath)).Parse(templateString); err != nil { 84 | log.Panicf("Unable to parse: path=%s, err=%s", relativPath, err) 85 | } 86 | } 87 | return nil 88 | } 89 | 90 | // Render a template given a model 91 | func renderTemplate(w http.ResponseWriter, tmpl string, p interface{}) { 92 | err := templates.ExecuteTemplate(w, tmpl, p) 93 | if err != nil { 94 | http.Error(w, err.Error(), http.StatusInternalServerError) 95 | } 96 | } 97 | 98 | func broken(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 99 | renderTemplate(w, "templates/missing.html", nil) 100 | } 101 | 102 | // Well hello there 103 | func hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 104 | model := Model{Name: ps.ByName("name")} 105 | renderTemplate(w, "templates/hello.html", &model) 106 | } 107 | 108 | func index(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 109 | renderTemplate(w, "templates/index.html", nil) 110 | } 111 | 112 | func getRouter() *httprouter.Router { 113 | // Load and parse templates (from binary or disk) 114 | templateBox = rice.MustFindBox("templates") 115 | templateBox.Walk("", newTemplate) 116 | 117 | // mux handler 118 | router := httprouter.New() 119 | 120 | // Index routee 121 | router.GET("/", index) 122 | 123 | // Example route that takes one rest style option 124 | router.GET("/hello/:name", hello) 125 | 126 | // Example route that encounters an error 127 | router.GET("/broken/handler", broken) 128 | 129 | // Serve static assets via the "static" directory 130 | fs := rice.MustFindBox("static").HTTPBox() 131 | router.ServeFiles("/static/*filepath", fs) 132 | return router 133 | } 134 | 135 | func main() { 136 | listen := flag.String("-listen", ":8080", "Interface and port to listen on") 137 | flag.Parse() 138 | fmt.Println("Listening on", *listen) 139 | log.Fatal(http.ListenAndServe(*listen, getRouter())) 140 | } 141 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | var testRouter = getRouter() 13 | 14 | func TestDefaultHandler(t *testing.T) { 15 | server := httptest.NewServer(testRouter) 16 | defer server.Close() 17 | resp, err := http.Get(server.URL + "/") 18 | assert.Nil(t, err) 19 | defer resp.Body.Close() 20 | body, err := ioutil.ReadAll(resp.Body) 21 | assert.Nil(t, err) 22 | assert.Contains(t, string(body), "Buddy") 23 | assert.Contains(t, string(body), "Boy") 24 | } 25 | 26 | func TestHelloHandler(t *testing.T) { 27 | server := httptest.NewServer(testRouter) 28 | defer server.Close() 29 | resp, err := http.Get(server.URL + "/hello/friend") 30 | assert.Nil(t, err) 31 | defer resp.Body.Close() 32 | body, err := ioutil.ReadAll(resp.Body) 33 | assert.Nil(t, err) 34 | assert.Contains(t, string(body), "Hi FRIEND") 35 | } 36 | 37 | func TestHandlerWithError(t *testing.T) { 38 | server := httptest.NewServer(testRouter) 39 | defer server.Close() 40 | resp, err := http.Get(server.URL + "/broken/handler") 41 | assert.Nil(t, err) 42 | defer resp.Body.Close() 43 | body, err := ioutil.ReadAll(resp.Body) 44 | assert.Nil(t, err) 45 | assert.Contains(t, string(body), `html/template: "templates/missing.html" is undefined`) 46 | assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) 47 | } 48 | 49 | func TestNewTemplateWithMissingTemplate(t *testing.T) { 50 | assert.Panics(t, func() { newTemplate("missing", nil, nil) }) 51 | } 52 | 53 | func TestNewTemplateWithInvalidTemplate(t *testing.T) { 54 | assert.Panics(t, func() { newTemplate("invalid.html", nil, nil) }) 55 | } 56 | -------------------------------------------------------------------------------- /static/golang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmcfarlane/golang-templates-example/4769f6010770777c9bbee525eb71d90b0c9b76e5/static/golang.png -------------------------------------------------------------------------------- /templates/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /templates/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ .Title }} 4 | 5 | 6 | -------------------------------------------------------------------------------- /templates/hello.html: -------------------------------------------------------------------------------- 1 | {{ template "templates/header.html" . }} 2 | 3 | 4 | 5 | Hi {{ .Name | Upper }}! 6 | 7 | {{ template "templates/footer.html" . }} 8 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {{ template "templates/header.html" . }} 2 | 3 | - Buddy
4 | - Boy
5 | 6 | {{ template "templates/footer.html" . }} 7 | -------------------------------------------------------------------------------- /templates/invalid.html: -------------------------------------------------------------------------------- 1 | This is an intentionally invalid template: 2 | 3 | {{ .intentionallyUndefinedField }} 4 | 5 | --------------------------------------------------------------------------------