go
58 lines · 8 steps
Per-tenant HTML templates with Gin's renderer
A thread-safe registry gives each tenant its own template set while satisfying Gin's HTMLRender interface.
Explained by
highlit
1package theme
2
3import (
4 "fmt"
5 "html/template"
6 "path/filepath"
7 "sync"
8
9 "github.com/gin-gonic/gin/render"
10)
11
12type Registry struct {
13 mu sync.RWMutex
14 tenants map[string]*template.Template
15 funcs template.FuncMap
16 baseGlob string
17}
18
19func NewRegistry(baseGlob string, funcs template.FuncMap) *Registry {
20 return &Registry{
21 tenants: make(map[string]*template.Template),
22 funcs: funcs,
23 baseGlob: baseGlob,
24 }
25}
26
27func (r *Registry) Load(tenant, dir string) error {
28 tmpl := template.New(tenant).Funcs(r.funcs)
29
30 if _, err := tmpl.ParseGlob(r.baseGlob); err != nil {
31 return fmt.Errorf("parse base templates: %w", err)
32 }
33 if _, err := tmpl.ParseGlob(filepath.Join(dir, "*.html")); err != nil {
34 return fmt.Errorf("parse tenant %q templates: %w", tenant, err)
35 }
36
37 r.mu.Lock()
38 r.tenants[tenant] = tmpl
39 r.mu.Unlock()
40 return nil
41}
42
43func (r *Registry) Instance(name string, data any) render.Render {
44 tenant, _ := data.(map[string]any)["tenant"].(string)
45
46 r.mu.RLock()
47 tmpl, ok := r.tenants[tenant]
48 if !ok {
49 tmpl = r.tenants["default"]
50 }
51 r.mu.RUnlock()
52
53 return render.HTML{
54 Template: tmpl,
55 Name: name,
56 Data: data,
57 }
58}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Implementing Gin's HTMLRender interface lets you swap in fully custom, per-request template selection.
- 2An RWMutex lets many requests read the template map concurrently while loads take exclusive access.
- 3Layering ParseGlob calls composes shared base templates with tenant-specific overrides in one template set.
Related explainers
go
package streaming import ( "bufio"
Streaming NDJSON logs over HTTP in Go
http-streaming
channels
select
Advanced
10 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
go
package api import ( "crypto/sha256"
ETag conditional requests in Gin
http-caching
etag
conditional-requests
Intermediate
6 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
go
package logging import ( "context"
Deduplicating log attributes in Go's slog
decorator-pattern
structured-logging
immutability
Intermediate
8 steps
rust
use std::collections::VecDeque; use std::sync::{Arc, Condvar, Mutex}; use std::time::{Duration, Instant};
Building a counting semaphore in Rust
concurrency
synchronization
condition-variable
Advanced
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/per-tenant-html-templates-with-gin-s-renderer-explained-go-6293/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.