Building Cross-Platform Apps with GoGui — A Practical TutorialBuilding cross-platform desktop applications can feel like walking a tightrope: you want native performance and look-and-feel, but also a single codebase that runs on Windows, macOS, and Linux. GoGui is a Go-based GUI toolkit designed to help developers strike that balance. This tutorial walks through planning, building, packaging, and distributing a cross-platform app using GoGui, with practical examples and actionable tips.
Why choose GoGui?
- Lightweight and fast: GoGui leverages Go’s performance and static compilation to produce compact binaries.
- Single language: Your UI and application logic are written in Go — no need to mix JavaScript, C#, or platform-specific languages.
- Cross-platform focus: GoGui abstracts common GUI patterns while exposing platform-specific capabilities when needed.
- Ease of deployment: Go’s static binaries simplify distribution and minimize runtime dependencies.
Prerequisites
- Go 1.20+ installed (adjust according to GoGui’s requirements).
- GoGui installed (follow its installation docs or
go get
if available). - Basic familiarity with Go (structs, interfaces, goroutines).
- A code editor and terminal; optional: Docker for build automation.
Project overview
We’ll build a small cross-platform note-taking app named “MemoGo” with these features:
- Create, edit, and delete notes
- Markdown preview
- Local storage (JSON file)
- Search and basic keyboard shortcuts
- Packaged executables for Windows, macOS, and Linux
Directory structure:
memogo/ cmd/ memogo/ main.go internal/ ui/ app.go components.go storage/ storage.go assets/ icons/ icon.png go.mod README.md
Step 1 — Initialize the project
Create module and basic files:
mkdir memogo && cd memogo go mod init github.com/youruser/memogo mkdir -p cmd/memogo internal/ui internal/storage assets/icons
Step 2 — Design the app architecture
Separation of concerns:
- cmd/memogo/main.go — app entry point, configuration, and startup.
- internal/ui — all GoGui UI components, window setup, event handlers.
- internal/storage — persistence layer (read/write JSON).
- assets — icons and static resources.
This separation keeps UI logic testable and storage replaceable (e.g., swap JSON for SQLite later).
Step 3 — Storage layer (internal/storage/storage.go)
We’ll store notes as JSON in the user’s config directory. Example data model:
package storage import ( "encoding/json" "os" "path/filepath" "sync" "time" ) type Note struct { ID string `json:"id"` Title string `json:"title"` Body string `json:"body"` UpdatedAt time.Time `json:"updated_at"` } type Store struct { path string mu sync.RWMutex Notes []Note `json:"notes"` } func New(path string) *Store { return &Store{path: path} } func (s *Store) Load() error { s.mu.Lock() defer s.mu.Unlock() f, err := os.Open(s.path) if os.IsNotExist(err) { s.Notes = []Note{} return nil } else if err != nil { return err } defer f.Close() return json.NewDecoder(f).Decode(s) } func (s *Store) Save() error { s.mu.RLock() defer s.mu.RUnlock() tmp := s.path + ".tmp" f, err := os.Create(tmp) if err != nil { return err } enc := json.NewEncoder(f) enc.SetIndent("", " ") if err := enc.Encode(s); err != nil { f.Close() return err } f.Close() return os.Rename(tmp, s.path) }
Notes:
- Use a mutex to make store safe for concurrent UI access.
- Save writes atomically using a temp file.
Step 4 — UI basics with GoGui (internal/ui/app.go)
Set up the main window, menus, and layout. The exact API depends on GoGui; here’s a representative pattern:
package ui import ( "github.com/youruser/memogo/internal/storage" "time" // import GoGui packages (example) gg "github.com/gogui/gogui" ) type App struct { win *gg.Window store *storage.Store } func New(store *storage.Store) *App { return &App{store: store} } func (a *App) Run() error { gg.Init() defer gg.Shutdown() a.win = gg.NewWindow("MemoGo", 900, 600) a.buildUI() a.win.ShowAndRun() // blocking return nil }
Step 5 — Building UI components
Create a three-pane layout: left sidebar (note list + search), center editor, right markdown preview.
Example component pseudocode (simplified):
func (a *App) buildUI() { // top-level layout root := gg.NewHBox() // left pane left := gg.NewVBox() search := gg.NewTextInput() search.OnChange(func(text string) { a.filterNotes(text) }) left.Add(search) noteList := gg.NewList() // each item shows Title and UpdatedAt left.Add(noteList) // center editor editor := gg.NewTextArea() editor.OnChange(func(txt string) { a.onEdit(txt) }) // right preview preview := gg.NewMarkdownView() root.Add(left, 250) // fixed width root.Add(editor, 1) // stretch root.Add(preview, 350) // fixed width a.win.SetContent(root) }
Important patterns:
- Debounce user input (e.g., 300ms) before saving to disk.
- Use goroutines for background tasks (saving, file IO) and marshal UI updates back to the main/UI thread using GoGui’s run-on-main function, if provided.
Step 6 — Keyboard shortcuts and UX details
Common shortcuts:
- Ctrl/Cmd+N = new note
- Ctrl/Cmd+S = save
- Ctrl/Cmd+F = focus search
- Ctrl/Cmd+W = close window
Implement predictable focus behavior and autosave. Example pseudocode:
a.win.SetShortcut("Ctrl+N", func() { a.createNote() }) a.win.SetShortcut("Ctrl+S", func() { a.saveCurrentNote() })
UX tips:
- Save on focus loss and at intervals (e.g., every 10s).
- Show transient “Saved” indicator on successful save.
- Provide undo/redo via an edit history buffer.
Step 7 — Markdown preview and rendering
Use a Go Markdown library (blackfriday, goldmark). Convert Markdown to sanitized HTML, then render in GoGui’s HTML or WebView component.
Example:
import "github.com/yuin/goldmark" func renderMarkdown(md string) string { var buf bytes.Buffer goldmark.Convert([]byte(md), &buf) return buf.String() }
Sanitize HTML output (e.g., bluemonday) before rendering to prevent any malicious content from user-supplied notes.
Step 8 — Handling platform differences
- File paths: use os.UserConfigDir or os.UserHomeDir + platform-specific subfolders.
- Menus: macOS expects an application menu (About, Quit) — implement conditionally.
- Shortcuts: use Cmd on macOS, Ctrl elsewhere. Detect runtime.GOOS or use toolkit helpers.
- Icons: include .ico for Windows, .icns for macOS, and PNG/SVG for Linux.
Step 9 — Packaging and distribution
For each platform produce an installer or package:
- Linux: static ELF binary (if no glibc compatibility issues) or AppImage/Flatpak/snap.
- Windows: .exe with resources embedded; create an installer with tools like Inno Setup or WiX.
- macOS: bundle into a .app and optionally notarize for distribution.
Cross-compilation tips:
- Use Go’s cross-compilation: GOOS and GOARCH environment variables. Example:
GOOS=darwin GOARCH=amd64 go build -o dist/memogo-macos-amd64 ./cmd/memogo GOOS=windows GOARCH=amd64 go build -o dist/memogo-windows.exe ./cmd/memogo GOOS=linux GOARCH=amd64 go build -o dist/memogo-linux ./cmd/memogo
- For macOS builds on non-macOS, consider using macOS CI runners or cross-compilation tools because GUI toolkits sometimes require platform SDKs.
- Use goreleaser to automate builds and create packages for all targets.
Step 10 — Testing and CI
- Unit test storage and business logic.
- UI tests: use GoGui’s testing hooks or integration tests that simulate user input if provided.
- CI pipeline:
- Run
go vet
andgolangci-lint
. - Run unit tests.
- Build artifacts for each target via goreleaser.
- Optionally code-sign and notarize macOS builds in CI.
- Run
Example: Core functions
A concise example for creating and saving a note:
func (a *App) createNote() { n := storage.Note{ ID: generateID(), Title: "Untitled", Body: "", UpdatedAt: time.Now(), } a.store.mu.Lock() a.store.Notes = append([]storage.Note{n}, a.store.Notes...) a.store.mu.Unlock() go func() { _ = a.store.Save() }() a.refreshUI() } func (a *App) saveCurrentNote(n *storage.Note) { n.UpdatedAt = time.Now() go func() { _ = a.store.Save() }() a.showSavedIndicator() }
Security and privacy considerations
- Store data in user-owned directories with proper file permissions.
- If syncing features are added, encrypt data in transit (TLS) and at rest (optional encryption before upload).
- Sanitize rendered Markdown HTML to prevent script injection.
Performance tips
- Avoid frequent synchronous disk writes; debounce and batch saves.
- Use efficient diffing when updating large lists to minimize re-rendering.
- Keep heavy processing (e.g., large Markdown conversions) off the UI thread.
Accessibility
- Provide keyboard navigation and focus indicators.
- Support high-contrast themes and system font-size scaling.
- Expose accessible names for controls for screen readers if GoGui supports accessibility APIs.
Example roadmap for features beyond the tutorial
- Cloud sync with optional end-to-end encryption
- Tags and notebooks
- Rich text editing (WYSIWYG)
- Plugin system for extensions
- Export/import (Markdown, PDF)
Conclusion
This tutorial outlined building a cross-platform note app with GoGui from architecture through packaging. The keys to success are clear separation of concerns, mindful handling of platform differences, responsive UI patterns (debouncing, background saves), and an automated build pipeline for distribution. With these practices, GoGui can enable fast, native-feeling apps while keeping a single Go codebase.
If you want, I can: provide a complete minimal working example repository, write platform-specific packaging scripts (Inno Setup, macOS .app structure, AppImage), or convert storage to SQLite. Which would you like next?
Leave a Reply