Frontend Labs
⋯

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.


Generated from real-life-golang-guide.md · 8 Oct 2026 16:36