The Golang Masterclass: Singleton Structs Will Save Your Project.
At some point, your project outgrows the usual patterns. It starts to split into mini-projects, not big enough for separate libraries (which are a pain to manage), but too isolated to share logic cleanly.
But the real headache begins: when two or more of these mini-projects need the same behavior. Which is a full-blown object with state.
The answer? Singleton structs, objects you create once and reuse across your app, with state and all.
Hoist it. Literally. It’ll save you from half the complexity.
I hit this exact problem while working on my agentic project. The modelservice
embeds a tiny utility server as a webhook for a browser extension. Until I embedded a frontend using WebView, which also needs a server to serve static assets (because, of course, paths are hell).
Project layout:
modelservice // handles agentic calls
frontend // webview UI
maskot // OpenGL mascot renderer
observer // native + web observation tools
utils/systray // system tray stuff
Now the question:
How do you start a server in these isolated, self-contained modules? Start it in modelservice
? Sure, but how do you pass the server instance to frontend
? Or vice versa?
The fix is simple: hoist the server to a shared singleton.
hoisted server
├── modelservice
└── frontend
So both modules share a single server instance, and it solves the even nastier problem of: who starts the server, and where do you register routes?
Game devs were onto something, there’s a reason they love singletons.
-
Singletons in Go
- Task Management Singleton
- More Advanced Example(webui server utility)
Singletons in Go
You’d be surprised how dead simple Go makes singletons, even as a low-level language.
Let’s build a basic task manager as an example(the pattern is the same even for complex projects).
Task Management Singleton
import (
"sync"
)
type task struct {
Id string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
}
type TaskManager struct {
tasks []task
mu sync.Mutex // for synchronization
}
var (
TaskInstance *TaskManager
once sync.Once
)
func GetTaskManager() *TaskManager {
once.Do(func() {
TaskManInstance = &TaskManager{
}
})
return TaskManInstance
}
func (t *TaskManager) AddTask(task string) error {}
// other methods below
The trick lives in GetTaskManager
. Thanks to once.Do
, the TaskManager
struct is instantiated exactly once. After that, it just returns the same object, state, logs, tasks, everything included and maintained.
Access it anywhere in your project:
man := GetTaskManager() // Keeps its state across modules
Ridiculously easy.
More Advanced Example(webui server utility)
Singletons are a legit pattern to have in your Go toolbox. I’ve used this same approach in C++, and the utility holds across languages.
Whenever you’re unsure how to handle shared state or common behavior, hoist it. Don’t duplicate it. Game devs figured this out long ago for a reason.
But remember with great power comes great responsibility.
I’ll be posting more deep dives on backend topics, Golang, and low-level systems on Substack. Would love to have you there; come say hi: