Gova is an early-stage Go GUI framework built around components, explicit scopes, state helpers, Fyne rendering, and native platform integrations.

Gova: declarative desktop UI with plain Go structs


Gova is an early-stage GUI framework for Go. It aims for a declarative component model while still producing native desktop apps from Go code. Under the hood it uses Fyne for cross-platform rendering, with macOS-specific integrations for native dialogs and dock behaviour.

The project is pre-1.0, so this is code to study and experiment with rather than a safe bet for a production desktop app. The interesting part is the API shape.

Components are plain Go values

The README counter example shows the basic model:

type Counter struct{}

func (Counter) Body(s *g.Scope) g.View {
	count := g.State(s, 0)
	return g.VStack(
		g.Text(count.Format("Count: %d")).Font(g.Title),
		g.HStack(
			g.Button("-", func() { count.Set(count.Get() - 1) }),
			g.Button("+", func() { count.Set(count.Get() + 1) }),
		),
	)
}

A component is a struct with a Body method. State is explicitly tied to a Scope. Views are built by composing functions such as VStack, HStack, Text, and Button.

That explicit scope is the main design choice. You can see where state comes from instead of relying on hidden hook ordering or stringly typed property bags.

Rendering translates view nodes into Fyne specs

The rendering path walks Gova’s internal view tree and produces Fyne bridge specs:

func toSpecWithScope(node *viewNode, scope *Scope) fyneBridge.ViewSpec {
	// convert Gova node to renderer spec
}

That gives Gova room to keep its public API stable while changing renderer details. The app code builds Gova views. The renderer decides how those views become Fyne widgets.

This is a useful pattern for any UI library: public component model on one side, rendering bridge on the other.

Modifiers are method chains

Gova uses small modifier methods for style and layout:

g.Text("Hello").Font(g.Title)
g.HStack(...).Spacing(g.SpaceMD)
g.VStack(...).Padding(g.SpaceLG)

In Go, this is often easier to read than large option structs. The tradeoff is API surface area: every modifier becomes a method the framework must maintain. For a UI framework, that may be worth it because the call sites stay clear.

Native integrations are isolated

Dialog APIs live in dialog.go behind builders:

g.NativeAlert("Delete file", "This cannot be undone").OK("Delete").Present(s)

macOS-specific implementations sit in Darwin files using cgo/Objective-C, while other platforms fall back to Fyne implementations. That keeps platform code out of the app-facing API.

Dock integration follows the same idea. macOS can use NSDockTile; other platforms can return no-op or fallback behaviour until taskbar support exists.

The CLI is deliberately small

The cmd/gova package supports dev, build, and run. The command code uses the standard flag package, signal-aware contexts, and simple exit codes.

ctx, cancel := signalContext()
defer cancel()

That fits the framework’s current stage. A GUI framework needs a development loop, but it does not need a huge CLI before the component model has settled.

What to take from Gova

The useful Go ideas are the explicit Scope, component structs with Body methods, method-chain modifiers, renderer isolation, and platform-specific files for native integrations.

Gova is still young. The best way to read it is as an experiment in making desktop UI feel like Go code rather than a wrapper around another ecosystem’s mental model.