gova-declarative-gui
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGova 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 , and produces one static binary. Internally powered by Fyne (BSD-3), but the public API is stable and renderer-independent.
Scopego build来自ara.so的技能——2026每日技能合集。
Gova是一款面向Go语言的声明式GUI框架,可通过单一代码库为macOS、Windows和Linux构建原生桌面应用。视图采用普通Go结构体实现,状态通过显式管理,可生成单个静态二进制文件。其内部基于Fyne(BSD-3许可)驱动,但公开API稳定且与渲染器无关。
Scopego buildInstall
安装
bash
go get github.com/nv404/gova@latestOptional CLI:
bash
go install github.com/nv404/gova/cmd/gova@latestPrerequisites:
- Go 1.26+
- C toolchain: Xcode CLT (macOS), +
build-essential(Linux), MinGW (Windows)libgl1-mesa-dev
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(Linux)、MinGW(Windows)libgl1-mesa-dev
Key CLI Commands
核心CLI命令
| Command | Purpose |
|---|---|
| Hot reload — watch |
| Compile to |
| Build and launch once, no file watching |
| Stripped binary (~23 MB for simple apps) |
| 命令 | 用途 |
|---|---|
| 热重载——监听 |
| 编译为 |
| 构建并启动应用一次,不监听文件变更 |
| 生成精简二进制文件(简单应用约23 MB) |
Core Concepts
核心概念
1. Components as Structs
1. 组件即结构体
A component is a Go struct implementing . Fields on the struct are typed props; zero values are defaults.
Body(s *g.Scope) g.Viewgo
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)
}组件是实现接口的Go结构体。结构体字段为类型化属性,零值即为默认值。
Body(s *g.Scope) g.Viewgo
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 passed to . No hidden scheduler, no hook-ordering rules.
*g.ScopeBodygo
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.Scopego
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
间距常量
| Constant | Use |
|---|---|
| Small gaps |
| Medium gaps |
| Large padding |
| 常量 | 用途 |
|---|---|
| 小间距 |
| 中等间距 |
| 大内边距 |
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.Titleg.Headlineg.Bodyg.Captiong.Monog.Titleg.Headlineg.Bodyg.Captiong.MonoState 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 signalgo
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) // dependenciesgo
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 , use to keep UI state across rebuilds:
gova devg.PersistedStatego
func (MyApp) Body(s *g.Scope) g.View {
// survives hot reload, lost on full restart
activeTab := g.PersistedState(s, "active-tab", "home")
// ...
}使用时,可通过在重建时保留UI状态:
gova devg.PersistedStatego
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.modmyapp/
├── main.go # g.Run入口文件
├── components/
│ ├── header.go
│ └── sidebar.go
├── views/
│ ├── home.go
│ └── settings.go
├── assets/
│ └── icon.png
└── go.modPlatform Support Matrix
平台支持矩阵
| Feature | macOS | Windows | Linux |
|---|---|---|---|
| Core UI | ✅ | ✅ | ✅ |
| Hot reload | ✅ | ✅ | ✅ |
| App icon | ✅ | ✅ | ✅ |
| Native dialogs | NSAlert/NSOpenPanel | Fyne fallback | Fyne fallback |
| Dock/taskbar | NSDockTile ✅ | Planned | Planned |
| 功能 | macOS | Windows | Linux |
|---|---|---|---|
| 核心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 buildbash
sudo apt install libgl1-mesa-dev xorg-devHot reload not picking up changes
- Ensure you're using , not
gova devgo run - Confirm files are in a directory watched by
.gogova 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 — mutating a slice/map in place without
count.Set(...)won't trigger re-renderSet - 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下因OpenGL错误失败
go buildbash
sudo apt install libgl1-mesa-dev xorg-dev热重载未检测到变更
- 确保使用而非
gova devgo 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
实用链接
- Docs: https://gova.dev
- Getting started: https://gova.dev/docs/getting-started/installation
- State & effects: https://gova.dev/docs/state/state
- Native dialogs: https://gova.dev/docs/overlays/native-dialogs
- CLI reference: https://gova.dev/docs/cli/overview
- pkg.go.dev: https://pkg.go.dev/github.com/nv404/gova