DEV Community

Cover image for Built Gova, a declarative GUI framework for Go
Naman vyas
Naman vyas

Posted on

Built Gova, a declarative GUI framework for Go

I spent the last few months building Gova. It is a declarative GUI framework for Go that compiles native desktop apps to a single static binary on macOS, Windows, and Linux. Struct-based components, reactive state, real native dialogs where the platform offers them. No Electron, no embedded webview, no JavaScript runtime.

Here is a full Counter app:

package main

import g "github.com/nv404/gova"

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) }),
        ).Spacing(g.SpaceMD),
    ).Padding(g.SpaceLG)
}

func main() {
    g.Run("Counter", g.Define(func(s *g.Scope) g.View {
        return Counter{}
    }))
}
Enter fullscreen mode Exit fullscreen mode

go run . and a real native window appears. No scaffolding, no config file, no build step beyond the Go toolchain.

What ships today

  • Components as plain Go structs with typed prop fields, composed with function calls.
  • Reactive primitives: State, Signal, Store, PersistedState. All keyed by call site, which means no hook rules and no string keys.
  • Real native dialogs on macOS through cgo: NSAlert, NSOpenPanel, NSSavePanel, NSDockTile badge, progress, and menu. Fyne fallbacks on Windows and Linux so portable code keeps running.
  • gova dev CLI with hot reload. UI state optionally survives the reload via PersistedState.
  • One codebase, three targets. 32 MB static binary for Counter, 23 MB stripped.
  • Headless testing via TestRender so you can assert on the view tree without opening a window.

A slightly bigger slice

A todo row with a reactive model, a delete button, and a text field that grows to fill the width:

gova.List(todos,
    func(t Todo) int { return t.ID },
    func(i int, todo Todo) gova.View {
        return gova.HStack(
            gova.Toggle(todo.Done).OnChange(func(done bool) {
                model.Update(func(m Model) Model {
                    return toggleTodo(m, todo.ID, done)
                })
            }),
            gova.Text(todo.Text),
            gova.Spacer(),
            gova.Button("Delete", func() {
                model.Update(func(m Model) Model {
                    return removeTodo(m, todo.ID)
                })
            }).Color(gova.Red),
        )
    },
)
Enter fullscreen mode Exit fullscreen mode

Modifier order does not matter. Defaults are Go zero values. The compiler checks your UI.

Install

go get github.com/nv404/gova@latest
Enter fullscreen mode Exit fullscreen mode

Optional CLI for a hot-reload workflow:

go install github.com/nv404/gova/cmd/gova@latest
gova dev ./examples/counter
Enter fullscreen mode Exit fullscreen mode

Requires Go 1.26+ and a C toolchain. One go get pulls Fyne and its native dependencies transitively.

Why another Go GUI framework

Go already has Fyne, Wails, and Gio. I used all three and wanted a different mental model: struct-based components with typed props, reactive state that survives refactors, and real platform widgets where the platform cares about them. Gova sits on top of Fyne for rendering so I did not have to rewrite a toolkit, but the public surface is its own.

Where to look next

  • Repo: github.com/NV404/gova
  • Docs and examples: gova.dev
  • Runnable examples in the repo: counter, todo, fancytodo, notes, themed, components, dialogs

It is pre-1.0. The API will move before v1.0.0, and I would rather hear honest critique than polite silence.

If the project looks useful, a star on the repo helps it surface to other Go devs looking in the desktop space.

Top comments (0)