44. Building a Fyne desktop app: a live system-resource monitor
Goal: a small, real Fyne app — a window showing live CPU/goroutine counts, updating on a ticker, exactly the kind of "wrap a Go backend in a native window" task Fyne is best at.
go get fyne.io/fyne/v2@latest
For a long-lived project, replace @latest with the release you chose
after testing and commit the resulting go.mod/go.sum.
// main.go
package main
import (
"fmt"
"runtime"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func main() {
a := app.New()
w := a.NewWindow("System Monitor")
w.Resize(fyne.NewSize(360, 200))
goroutines := widget.NewLabel("")
heapAlloc := widget.NewLabel("")
cpus := widget.NewLabel(fmt.Sprintf("CPUs: %d", runtime.NumCPU()))
content := container.NewVBox(
widget.NewLabelWithStyle("Live System Stats", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
cpus,
goroutines,
heapAlloc,
)
w.SetContent(content)
// Fyne widgets must only be updated from the main/UI goroutine's
// event loop — this ticker goroutine only computes values; it hands
// the actual widget update back via fyne.Do (Fyne 2.5+) / a channel.
go func() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for range ticker.C {
var mem runtime.MemStats
runtime.ReadMemStats(&mem)
g := runtime.NumGoroutine()
heap := mem.HeapAlloc
fyne.Do(func() {
goroutines.SetText(fmt.Sprintf("Goroutines: %d", g))
heapAlloc.SetText(fmt.Sprintf("Heap: %.2f MB", float64(heap)/1024/1024))
})
}
}()
w.ShowAndRun()
}
go run .
The one rule that matters for any GUI toolkit, not just Fyne: never
mutate UI widgets from a background goroutine directly. GUI toolkits
assume single-threaded access to their widget tree; Fyne's fyne.Do
(and, in older Fyne, a channel back to the main loop) is exactly the same
"hand data across a boundary, let the owning thread do the mutation"
discipline you already know from channels in Chapter 16 — the UI thread
is just another goroutine with an exclusivity rule.