DEV Community

Cover image for The Golang Masterclass: Singleton Structs Will Save Your Project.
Sk
Sk

Posted on

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

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 

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Ridiculously easy.

More Advanced Example(webui server utility)

How to Build a Robust Go Webview UI: with a Utility Server and NavStack

Webview UI that not only loads reliably but also navigates smoothly

favicon skdev.substack.com

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:

Coffee & Kernels

Top comments (1)

Collapse
 
dotallio profile image
Dotallio

Totally agree, singletons are a lifesaver for shared state in Go once projects get big. Have you ever hit issues with testing or hot reloads because of singletons?