gova-declarative-gui

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Gova Declarative GUI Framework

Gova 声明式GUI框架

Skill by ara.so — Daily 2026 Skills collection.
Gova is a declarative GUI framework for Go that builds native desktop apps for macOS, Windows, and Linux from a single codebase. Views are plain Go structs, state is explicit via a
Scope
, and
go build
produces one static binary. Internally powered by Fyne (BSD-3), but the public API is stable and renderer-independent.

来自ara.so的技能——2026每日技能合集。
Gova是一款面向Go语言的声明式GUI框架,可通过单一代码库为macOS、Windows和Linux构建原生桌面应用。视图采用普通Go结构体实现,状态通过
Scope
显式管理,
go build
可生成单个静态二进制文件。其内部基于Fyne(BSD-3许可)驱动,但公开API稳定且与渲染器无关。

Install

安装

bash
go get github.com/nv404/gova@latest
Optional CLI:
bash
go install github.com/nv404/gova/cmd/gova@latest
Prerequisites:
  • Go 1.26+
  • C toolchain: Xcode CLT (macOS),
    build-essential
    +
    libgl1-mesa-dev
    (Linux), MinGW (Windows)

bash
go get github.com/nv404/gova@latest
可选CLI工具:
bash
go install github.com/nv404/gova/cmd/gova@latest
前置要求:
  • Go 1.26及以上版本
  • C语言工具链:Xcode CLT(macOS)、
    build-essential
    +
    libgl1-mesa-dev
    (Linux)、MinGW(Windows)

Key CLI Commands

核心CLI命令

CommandPurpose
gova dev ./path/to/app
Hot reload — watch
.go
files, rebuild and relaunch on save
gova build ./path/to/app
Compile to
./bin/<name>
static binary
gova run ./path/to/app
Build and launch once, no file watching
go build -ldflags "-s -w"
Stripped binary (~23 MB for simple apps)

命令用途
gova dev ./path/to/app
热重载——监听
.go
文件,保存时自动重建并重启应用
gova build ./path/to/app
编译为
./bin/<name>
静态二进制文件
gova run ./path/to/app
构建并启动应用一次,不监听文件变更
go build -ldflags "-s -w"
生成精简二进制文件(简单应用约23 MB)

Core Concepts

核心概念

1. Components as Structs

1. 组件即结构体

A component is a Go struct implementing
Body(s *g.Scope) g.View
. Fields on the struct are typed props; zero values are defaults.
go
package main

import g "github.com/nv404/gova"

type Greeting struct {
    Name string // prop with zero-value default ""
}

func (c Greeting) Body(s *g.Scope) g.View {
    name := c.Name
    if name == "" {
        name = "World"
    }
    return g.Text("Hello, " + name + "!").Font(g.Title)
}
组件是实现
Body(s *g.Scope) g.View
接口的Go结构体。结构体字段为类型化属性,零值即为默认值。
go
package main

import g "github.com/nv404/gova"

type Greeting struct {
    Name string // 属性,零值默认空字符串
}

func (c Greeting) Body(s *g.Scope) g.View {
    name := c.Name
    if name == "" {
        name = "World"
    }
    return g.Text("Hello, " + name + "!").Font(g.Title)
}

2. Reactive State with Scope

2. 基于Scope的响应式状态

State lives on the
*g.Scope
passed to
Body
. No hidden scheduler, no hook-ordering rules.
go
func (Counter) Body(s *g.Scope) g.View {
    count := g.State(s, 0) // typed signal, initial value 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)
}
状态存储在传递给
Body
*g.Scope
中,无隐藏调度器,无钩子顺序规则。
go
func (Counter) Body(s *g.Scope) g.View {
    count := g.State(s, 0) // 类型化信号,初始值为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)
}

3. Entry Point

3. 入口点

go
func main() {
    g.Run("My App", g.Component(MyComponent{}))
}

go
func main() {
    g.Run("My App", g.Component(MyComponent{}))
}

Layout Primitives

布局基础组件

go
// Vertical stack
g.VStack(child1, child2, child3).Spacing(g.SpaceMD).Padding(g.SpaceLG)

// Horizontal stack
g.HStack(child1, child2).Spacing(g.SpaceSM)

// Layered/overlapping stack
g.ZStack(background, foreground)

// Scaffold (app shell with nav, toolbar, etc.)
g.Scaffold(
    g.NavBar("Title"),
    content,
)
go
// 垂直栈
g.VStack(child1, child2, child3).Spacing(g.SpaceMD).Padding(g.SpaceLG)

// 水平栈
g.HStack(child1, child2).Spacing(g.SpaceSM)

// 分层/重叠栈
g.ZStack(background, foreground)

// 脚手架(带导航栏、工具栏等的应用外壳)
g.Scaffold(
    g.NavBar("Title"),
    content,
)

Spacing Constants

间距常量

ConstantUse
g.SpaceSM
Small gaps
g.SpaceMD
Medium gaps
g.SpaceLG
Large padding

常量用途
g.SpaceSM
小间距
g.SpaceMD
中等间距
g.SpaceLG
大内边距

Built-in Views

内置视图

go
g.Text("Hello").Font(g.Title)        // styled text
g.Text("body text").Font(g.Body)

g.Button("Click me", func() { /* handler */ })

g.TextField(value.Get(), func(s string) { value.Set(s) })

g.Toggle(enabled.Get(), func(b bool) { enabled.Set(b) })

g.Image("path/to/image.png")

g.Spacer() // flexible space
g.Divider()
go
g.Text("Hello").Font(g.Title)        // 样式文本
g.Text("body text").Font(g.Body)

g.Button("Click me", func() { /* 处理函数 */ })

g.TextField(value.Get(), func(s string) { value.Set(s) })

g.Toggle(enabled.Get(), func(b bool) { enabled.Set(b) })

g.Image("path/to/image.png")

g.Spacer() // 弹性占位空间
g.Divider()

Font Constants

字体常量

g.Title
,
g.Headline
,
g.Body
,
g.Caption
,
g.Mono

g.Title
,
g.Headline
,
g.Body
,
g.Caption
,
g.Mono

State Patterns

状态模式

Basic State

基础状态

go
count := g.State(s, 0)
count.Get()        // read
count.Set(42)      // write, triggers re-render
count.Format("Value: %d") // returns formatted string signal
go
count := g.State(s, 0)
count.Get()        // 读取
count.Set(42)      // 写入,触发重渲染
count.Format("Value: %d") // 返回格式化字符串信号

Derived / Computed State

派生/计算状态

go
doubled := g.Derived(s, func() int {
    return count.Get() * 2
})
go
doubled := g.Derived(s, func() int {
    return count.Get() * 2
})

Effects (side effects on state change)

副作用(状态变更时执行)

go
g.Effect(s, func() {
    fmt.Println("count changed to", count.Get())
}, count) // dependencies
go
g.Effect(s, func() {
    fmt.Println("count changed to", count.Get())
}, count) // 依赖项

Persisted State (survives hot reload)

持久化状态(热重载时保留)

go
name := g.PersistedState(s, "user-name", "")

go
name := g.PersistedState(s, "user-name", "")

Full Example: Todo App

完整示例:待办事项应用

go
package main

import g "github.com/nv404/gova"

type Todo struct {
    Text string
    Done bool
}

type TodoApp struct{}

func (TodoApp) Body(s *g.Scope) g.View {
    todos := g.State(s, []Todo{})
    input := g.State(s, "")

    addTodo := func() {
        if input.Get() == "" {
            return
        }
        todos.Set(append(todos.Get(), Todo{Text: input.Get()}))
        input.Set("")
    }

    rows := make([]g.View, 0, len(todos.Get()))
    for i, todo := range todos.Get() {
        i, todo := i, todo // capture loop vars
        rows = append(rows, g.HStack(
            g.Toggle(todo.Done, func(v bool) {
                list := todos.Get()
                list[i].Done = v
                todos.Set(list)
            }),
            g.Text(todo.Text),
        ).Spacing(g.SpaceSM))
    }

    return g.VStack(
        g.Text("Todos").Font(g.Title),
        g.VStack(rows...).Spacing(g.SpaceSM),
        g.HStack(
            g.TextField(input.Get(), func(v string) { input.Set(v) }),
            g.Button("Add", addTodo),
        ).Spacing(g.SpaceSM),
    ).Padding(g.SpaceLG)
}

func main() {
    g.Run("Todo", g.Component(TodoApp{}))
}

go
package main

import g "github.com/nv404/gova"

type Todo struct {
    Text string
    Done bool
}

type TodoApp struct{}

func (TodoApp) Body(s *g.Scope) g.View {
    todos := g.State(s, []Todo{})
    input := g.State(s, "")

    addTodo := func() {
        if input.Get() == "" {
            return
        }
        todos.Set(append(todos.Get(), Todo{Text: input.Get()}))
        input.Set("")
    }

    rows := make([]g.View, 0, len(todos.Get()))
    for i, todo := range todos.Get() {
        i, todo := i, todo // 捕获循环变量
        rows = append(rows, g.HStack(
            g.Toggle(todo.Done, func(v bool) {
                list := todos.Get()
                list[i].Done = v
                todos.Set(list)
            }),
            g.Text(todo.Text),
        ).Spacing(g.SpaceSM))
    }

    return g.VStack(
        g.Text("Todos").Font(g.Title),
        g.VStack(rows...).Spacing(g.SpaceSM),
        g.HStack(
            g.TextField(input.Get(), func(v string) { input.Set(v) }),
            g.Button("Add", addTodo),
        ).Spacing(g.SpaceSM),
    ).Padding(g.SpaceLG)
}

func main() {
    g.Run("Todo", g.Component(TodoApp{}))
}

Native Dialogs (macOS: NSAlert / NSOpenPanel; other platforms: Fyne fallback)

原生对话框(macOS:NSAlert / NSOpenPanel;其他平台:Fyne降级实现)

go
// Alert dialog
g.Button("Alert", func() {
    g.Alert(g.AlertOptions{
        Title:   "Warning",
        Message: "Something happened.",
        Style:   g.AlertWarning,
    })
})

// Open file dialog
g.Button("Open File", func() {
    path, err := g.OpenFileDialog(g.OpenFileOptions{
        Title:      "Choose a file",
        Extensions: []string{".txt", ".md"},
    })
    if err == nil && path != "" {
        filePath.Set(path)
    }
})

// Save file dialog
g.Button("Save", func() {
    dest, err := g.SaveFileDialog(g.SaveFileOptions{
        Title:           "Save As",
        DefaultFilename: "output.txt",
    })
    if err == nil && dest != "" {
        // write to dest
    }
})

go
// 警告对话框
g.Button("Alert", func() {
    g.Alert(g.AlertOptions{
        Title:   "Warning",
        Message: "Something happened.",
        Style:   g.AlertWarning,
    })
})

// 文件打开对话框
g.Button("Open File", func() {
    path, err := g.OpenFileDialog(g.OpenFileOptions{
        Title:      "Choose a file",
        Extensions: []string{".txt", ".md"},
    })
    if err == nil && path != "" {
        filePath.Set(path)
    }
})

// 文件保存对话框
g.Button("Save", func() {
    dest, err := g.SaveFileDialog(g.SaveFileOptions{
        Title:           "Save As",
        DefaultFilename: "output.txt",
    })
    if err == nil && dest != "" {
        // 写入目标路径
    }
})

Platform Integration (macOS Dock)

平台集成(macOS Dock)

go
// Dock badge (macOS)
g.DockBadge("3")
g.DockBadge("") // clear badge

// Dock progress (macOS)
g.DockProgress(0.75) // 0.0–1.0
g.DockProgress(-1)   // hide

// Dock menu (macOS)
g.SetDockMenu([]g.MenuItem{
    {Label: "New Window", Action: func() { /* ... */ }},
    {Label: "Preferences", Action: func() { /* ... */ }},
})

go
// Dock徽章(macOS)
g.DockBadge("3")
g.DockBadge("") // 清除徽章

// Dock进度条(macOS)
g.DockProgress(0.75) // 0.0–1.0
g.DockProgress(-1)   // 隐藏

// Dock菜单(macOS)
g.SetDockMenu([]g.MenuItem{
    {Label: "New Window", Action: func() { /* ... */ }},
    {Label: "Preferences", Action: func() { /* ... */ }},
})

Theming and Colors

主题与颜色

go
// Dark/light toggle
g.Button("Toggle Theme", func() {
    if g.CurrentTheme() == g.ThemeDark {
        g.SetTheme(g.ThemeLight)
    } else {
        g.SetTheme(g.ThemeDark)
    }
})

// Semantic colors in custom views
g.Text("Primary").Color(g.ColorPrimary)
g.Text("Secondary").Color(g.ColorSecondary)
g.Text("Danger").Color(g.ColorDanger)

go
// 深色/浅色主题切换
g.Button("Toggle Theme", func() {
    if g.CurrentTheme() == g.ThemeDark {
        g.SetTheme(g.ThemeLight)
    } else {
        g.SetTheme(g.ThemeDark)
    }
})

// 自定义视图中的语义化颜色
g.Text("Primary").Color(g.ColorPrimary)
g.Text("Secondary").Color(g.ColorSecondary)
g.Text("Danger").Color(g.ColorDanger)

Component Composition with Viewable

基于Viewable的组件组合

go
type Card struct {
    Title   string
    Content g.View
}

func (c Card) Body(s *g.Scope) g.View {
    return g.VStack(
        g.Text(c.Title).Font(g.Headline),
        g.Divider(),
        c.Content,
    ).Padding(g.SpaceMD)
}

// Usage
g.Component(Card{
    Title:   "My Card",
    Content: g.Text("Card body text"),
})

go
type Card struct {
    Title   string
    Content g.View
}

func (c Card) Body(s *g.Scope) g.View {
    return g.VStack(
        g.Text(c.Title).Font(g.Headline),
        g.Divider(),
        c.Content,
    ).Padding(g.SpaceMD)
}

// 使用方式
g.Component(Card{
    Title:   "My Card",
    Content: g.Text("Card body text"),
})

Navigation / Multi-View

导航/多视图

go
type NotesApp struct{}

func (NotesApp) Body(s *g.Scope) g.View {
    selected := g.State(s, "list")

    switch selected.Get() {
    case "detail":
        return g.Component(DetailView{OnBack: func() { selected.Set("list") }})
    default:
        return g.Component(ListView{OnSelect: func() { selected.Set("detail") }})
    }
}

go
type NotesApp struct{}

func (NotesApp) Body(s *g.Scope) g.View {
    selected := g.State(s, "list")

    switch selected.Get() {
    case "detail":
        return g.Component(DetailView{OnBack: func() { selected.Set("list") }})
    default:
        return g.Component(ListView{OnSelect: func() { selected.Set("detail") }})
    }
}

App Icon at Runtime

运行时设置应用图标

go
func main() {
    app := g.NewApp("My App")
    app.SetIcon("assets/icon.png") // set before Run
    app.Run(g.Component(MyComponent{}))
}

go
func main() {
    app := g.NewApp("My App")
    app.SetIcon("assets/icon.png") // 需在Run前设置
    app.Run(g.Component(MyComponent{}))
}

Hot Reload with PersistedState

结合PersistedState实现热重载

When using
gova dev
, use
g.PersistedState
to keep UI state across rebuilds:
go
func (MyApp) Body(s *g.Scope) g.View {
    // survives hot reload, lost on full restart
    activeTab := g.PersistedState(s, "active-tab", "home")
    // ...
}

使用
gova dev
时,可通过
g.PersistedState
在重建时保留UI状态:
go
func (MyApp) Body(s *g.Scope) g.View {
    // 热重载时保留,完全重启后丢失
    activeTab := g.PersistedState(s, "active-tab", "home")
    // ...
}

Project Structure (recommended)

推荐项目结构

myapp/
├── main.go          # g.Run entry point
├── components/
│   ├── header.go
│   └── sidebar.go
├── views/
│   ├── home.go
│   └── settings.go
├── assets/
│   └── icon.png
└── go.mod

myapp/
├── main.go          # g.Run入口文件
├── components/
│   ├── header.go
│   └── sidebar.go
├── views/
│   ├── home.go
│   └── settings.go
├── assets/
│   └── icon.png
└── go.mod

Platform Support Matrix

平台支持矩阵

FeaturemacOSWindowsLinux
Core UI
Hot reload
App icon
Native dialogsNSAlert/NSOpenPanelFyne fallbackFyne fallback
Dock/taskbarNSDockTile ✅PlannedPlanned

功能macOSWindowsLinux
核心UI
热重载
应用图标
原生对话框NSAlert/NSOpenPanel ✅Fyne降级实现 ✅Fyne降级实现 ✅
Dock/任务栏NSDockTile ✅规划中规划中

Troubleshooting

故障排查

cgo: C compiler not found
  • macOS:
    xcode-select --install
  • Linux:
    sudo apt install build-essential libgl1-mesa-dev
  • Windows: Install MinGW-w64 and add to PATH
go build
fails with OpenGL errors on Linux
bash
sudo apt install libgl1-mesa-dev xorg-dev
Hot reload not picking up changes
  • Ensure you're using
    gova dev
    , not
    go run
  • Confirm
    .go
    files are in a directory watched by
    gova dev ./path
Binary is large (~32 MB)
  • Strip symbols:
    go build -ldflags "-s -w" -o ./bin/myapp
  • Result: ~23 MB. This is expected — Fyne (OpenGL renderer) is bundled.
State not updating the UI
  • Always call
    count.Set(...)
    — mutating a slice/map in place without
    Set
    won't trigger re-render
  • For slices: copy, modify, then set:
    list := todos.Get(); list[i] = newVal; todos.Set(list)
API breakage (pre-1.0)
  • Pin a specific tag:
    go get github.com/nv404/gova@v0.x.y
  • Check the CHANGELOG before upgrading

cgo: C compiler not found
  • macOS:执行
    xcode-select --install
  • Linux:执行
    sudo apt install build-essential libgl1-mesa-dev
  • Windows:安装MinGW-w64并添加到PATH
Linux下
go build
因OpenGL错误失败
bash
sudo apt install libgl1-mesa-dev xorg-dev
热重载未检测到变更
  • 确保使用
    gova dev
    而非
    go run
  • 确认
    .go
    文件位于
    gova dev ./path
    监听的目录中
二进制文件过大(约32 MB)
  • 剥离符号:
    go build -ldflags "-s -w" -o ./bin/myapp
  • 结果:约23 MB。此为正常现象——Fyne(OpenGL渲染器)已被打包在内。
状态更新未同步到UI
  • 务必调用
    count.Set(...)
    ——原地修改切片/映射而不调用
    Set
    不会触发重渲染
  • 对于切片:复制、修改、再设置:
    list := todos.Get(); list[i] = newVal; todos.Set(list)
API变更(1.0版本前)
  • 固定特定版本标签:
    go get github.com/nv404/gova@v0.x.y
  • 升级前查看CHANGELOG

Useful Links

实用链接