The Squirrel.Windows pattern for Go. External supervisor with versioned directories, crash-based rollback, and zero-dependency child integration.
Built-in native splash screen (macOS Cocoa/AppKit) -- no competing Go update library offers this.
Every Go auto-update library uses the same approach: self-surgery -- the running binary replaces itself on disk, then restarts. If the new version crashes at startup, recovery logic never executes. If the replacement is interrupted (power loss, OOM kill), the binary is corrupted with no rollback path.
Discord, Slack, and VS Code solved this years ago: a thin launcher manages versioned directories side-by-side. The old version stays intact until the new one proves stable.
We could not find a Go library that implements this pattern. go-launcher does.
go-launcher is a library you embed in a small launcher binary that supervises your actual application:
your-launcher (thin binary, ~40 lines of your code + go-launcher)
└── your-app (your actual application, spawned as a child process)
The launcher handles:
- Crash detection + automatic rollback -- if the new version crash-loops, the previous version comes back automatically
- Versioned directories --
versions/current/andversions/previous/side-by-side - Update orchestration -- download to staging, verify SHA-256 checksum, atomic rotation
- Probation period -- new versions must survive a configurable window before being marked stable
- Process supervision -- spawn, monitor, restart with configurable backoff
- Anti-oscillation -- prevents infinite swapping between two broken versions
- Bootstrap download -- if no child binary exists, download the latest version on first launch
- Self-relocation -- launcher copies itself from Downloads to a permanent install location on first run
- Singleton enforcement -- PID lockfile prevents duplicate instances
Single dependency (golang.org/x/sys). The child package imported by your application has zero transitive dependencies -- standard library only.
For a runnable end-to-end demo, see the
_example/directory.
package main
import (
"context"
"os"
"github.com/razvandimescu/go-launcher"
"github.com/razvandimescu/go-launcher/fetch"
"github.com/razvandimescu/go-launcher/ui/splash"
)
func main() {
l := launcher.New(launcher.Config{
AppName: "My App",
ChildBinaryName: "my-app",
DataDir: launcher.DefaultDataDir("MyApp"),
InstallDir: launcher.DefaultInstallDir("MyApp"),
EnvVarName: "MYAPP_LAUNCHER_STATE_DIR",
Fetcher: fetch.GitHubRelease("myorg", "myapp", fetch.AssetPattern("my-app-*")),
UI: splash.New(splash.Config{AppName: "My App"}),
})
os.Exit(l.Run(context.Background()))
}Version discovery is your application's concern -- poll your own API, check GitHub, read a config file. The child tells the launcher what to download:
package main
import (
"os"
"github.com/razvandimescu/go-launcher/child"
)
func init() {
child.SetEnvVar("MYAPP_LAUNCHER_STATE_DIR")
}
func main() {
// ... application init ...
// Signal healthy startup
if child.IsManaged() {
child.TouchHeartbeat()
}
// ... application runs ...
// When you detect a new version is available:
if child.IsManaged() {
child.RequestUpdate("1.2.0", "https://example.com/my-app-1.2.0", "sha256:abc123...")
os.Exit(0) // launcher handles download, rotation, and restart
}
}| Library | Approach | Rollback | Supervisor | Versioned dirs | Built-in UI | Windows |
|---|---|---|---|---|---|---|
| creativeprojects/go-selfupdate | Self-surgery | Apply-time only | No | No | No | Yes |
| minio/selfupdate | Self-surgery | No | No | No | No | Yes |
| sanbornm/go-selfupdate | Self-surgery | No | No | No | No | Yes |
| rhysd/go-github-selfupdate | Self-surgery | Apply-time only | No | No | No | Yes |
| jpillora/overseer | Master/child | No | Yes | No | No | No |
| fynelabs/selfupdate | Self-surgery | Apply-time only | No | No | No | Yes |
| go-launcher | External supervisor | Crash-based | Yes | Yes | Yes | Yes |
Apply-time rollback means the .old file is restored if the rename/copy fails during the swap. It does not help if the new version starts successfully but crashes 30 seconds later.
Crash-based rollback means the launcher detects that the new version is crash-looping and automatically reverts to the previous known-good version -- even if the new version ran briefly before crashing.
This table compares deployment architecture. Some of these libraries have strengths in other dimensions -- multi-backend support (GitHub/GitLab/S3), code signing verification, GOOS/GOARCH detection -- see each library's documentation for full feature sets.
$DATA_DIR/
launcher.json # persistent state (7 flat JSON fields)
launcher.lock # PID lockfile
heartbeat # touched by child after healthy init
pending_update.json # written by child when update is available
shutdown_requested # flag file for clean exit
versions/
current/ # active version (opaque directory)
previous/ # rollback target
staging/ # download in progress
Communication uses file-based IPC -- no sockets, no named pipes. The launcher sets an environment variable pointing to the data directory. The child writes files to signal state changes:
| Direction | Signal | Mechanism |
|---|---|---|
| Launcher → Child | "You are managed" | Environment variable |
| Child → Launcher | "I'm healthy" | Touch heartbeat file |
| Child → Launcher | "Update available" | Write pending_update.json + exit 0 |
| Child → Launcher | "Shut down" | Write shutdown_requested + exit 0 |
The launcher always restarts the child unless shutdown_requested exists with exit code 0. An unexpected exit 0 (without the file) is treated as a crash -- this avoids ambiguity from stray os.Exit(0) calls.
For the full supervisor loop, update flow, and rollback mechanics, see docs/architecture.md.
go-launcher is interface-driven. Provide your own implementations or use the built-in ones.
type Fetcher interface {
LatestVersion(ctx context.Context) (*Release, error)
Download(ctx context.Context, release *Release, dst io.Writer, progress func(float64)) error
}Built-in: fetch.GitHubRelease(), fetch.HTTP().
type UI interface {
ShowSplash(status string)
UpdateProgress(percent float64, status string)
HideSplash()
ShowError(msg string)
}Built-in: ui/splash provides native splash screens for macOS (Cocoa/AppKit) and Windows (GDI+) with animated spinner, progress bar, and configurable branding:
import "github.com/razvandimescu/go-launcher/ui/splash"
UI: splash.New(splash.Config{
AppName: "My App",
Logo: logoBytes, // PNG, or nil for text-only
AccentHex: "#2E67B2", // spinner + progress bar color
})Returns a silent no-op on Linux or when CGo is unavailable on macOS. Pass nil for fully headless operation.
type Registrar interface {
RegisterLoginItem(binaryPath string) error
UnregisterLoginItem() error
RegisterService(binaryPath string, args []string) error
UnregisterService() error
}Handles OS-level registration (login items, system services). No built-in implementations yet — provide your own or pass nil to skip.
launcher.Config{
// Required
AppName string // display name
ChildBinaryName string // binary filename in versions/current/
DataDir string // state, versions, IPC files
InstallDir string // where the launcher lives permanently
EnvVarName string // env var set on child process
// Optional (sensible defaults)
ChildArgs []string // args forwarded to child (default: none)
Backoff []time.Duration // restart delays (default: [2s, 5s, 15s])
CrashThreshold int // crashes before rollback (default: 3)
CrashWindow time.Duration // crash count resets after this (default: 5min)
ProbationDuration time.Duration // new version probation (default: 10min)
KillTimeout time.Duration // SIGTERM -> SIGKILL escalation (default: 30s)
// Pluggable (all optional except Fetcher if you want updates)
UI UI // nil = headless
Fetcher Fetcher // nil = no bootstrap/updates
Registrar Registrar // nil = skip OS registration
}| Platform | DefaultDataDir | DefaultInstallDir |
|---|---|---|
| macOS | ~/Library/Application Support/{appName}/ |
/Applications/ |
| Windows | %LOCALAPPDATA%\{appName}\ |
%LOCALAPPDATA%\{appName}\ |
| Linux | ~/.local/share/{appName}/ |
~/.local/bin/ |
go-launcher logs via log/slog. Configure slog.SetDefault() before calling Run().
go-launcher downloads binaries from the internet and executes them. The built-in fetchers enforce HTTPS. Downloaded artifacts are verified against SHA-256 checksums provided in the Release.Checksum field.
Code signing verification is not currently built in. If your threat model requires it, implement a custom Fetcher that verifies signatures before writing to the dst writer.
- Single-unit child. The child must be a single binary or a directory managed as an opaque unit.
- No self-update. The launcher does not update itself. This is deliberate -- the launcher is a thin, stable binary that changes rarely. Update it via your installer or a manual download.
- Full downloads only. No delta/incremental updates. For most Go binaries (5-30MB), full downloads complete in seconds.
- No download resumption. Interrupted downloads restart from the beginning.
MIT
Issues and pull requests are welcome. See the _example/ directory for a working launcher + child pair you can use for testing.