# Go Development Windows Native Setup Adding Go development on top of a Windows 11 machine already configured per [[General_Development_Windows_Native_Setup]]. Most of the stack carries over — Windows Terminal, PowerShell 7, Starship, Git, GitHub CLI, VS Code, Claude Code, and the `$PROFILE` setup all work identically. This guide only covers the Go-specific additions, and everything installs the same way on **ARM64 (Snapdragon, Surface Pro 11, Dev Kit 2023) and x64 (Intel/AMD)**. It's the Windows-native counterpart to [[Go_Development_Mac_Tahoe_Setup]] and [[Go_Development_Ubuntu_Setup]]. > [!tip] AI assistance is optional > The reference to Claude Code above assumes you completed Part 7 of the General guide. If you skipped it or use a different AI tool, this guide's instructions still work — the language toolchain doesn't depend on any AI being present. > [!info] Coexistence > C, Python, Ruby, Go, and Rust can all live on the same Windows install without interfering. Each language's toolchain installs to its own prefix: `uv` under `%USERPROFILE%\.local\`, Rust under `%USERPROFILE%\.cargo\` and `%USERPROFILE%\.rustup\`, Go under `C:\Program Files\Go\` (binary) and `%USERPROFILE%\go\` (module cache + user binaries), Ruby via RubyInstaller under `C:\Ruby34-x64\`, MSVC under the Visual Studio Build Tools prefix. No conflicts. --- ## No version manager needed (usually) Unlike Python or Ruby, Go has a much simpler version story. The official recommendation is: 1. **One current Go install** for day-to-day work 2. **Per-version installs** via `go install golang.org/dl/goX.Y.Z@latest` when you need a specific older version for a legacy project No `pyenv`/`rbenv`/`mise` equivalent is typically necessary. Go's backward compatibility guarantees mean most projects work on the current stable. --- ## Install Go There are two clean ways to get the Go toolchain on Windows. The **winget** path is the recommended default because it auto-picks the right architecture binary (ARM64 vs x64) and integrates with `winget upgrade` for subsequent updates. The **MSI installer** from go.dev is the equivalent of the official tarball on Linux — useful if you want a specific point release the moment it ships or if you're on a network without winget reach. ### Path A — winget (recommended) ```powershell winget install -e --id GoLang.Go ``` This installs Go to `C:\Program Files\Go\` and automatically adds `C:\Program Files\Go\bin` to the system PATH. Open a **new** PowerShell window so the updated PATH takes effect, then verify: ```powershell go version ``` You should see something like `go version go1.24.4 windows/arm64` (or `windows/amd64` on x64). > [!info] How winget knows the right architecture > The `GoLang.Go` manifest lists separate ARM64 and x64 installers; winget picks the one matching `$env:PROCESSOR_ARCHITECTURE`. Both ship via the Go team's official release channel — no third-party packaging. ### Path B — MSI installer from go.dev If you prefer to pull the installer directly, the Go team publishes `go1.x.x.windows-arm64.msi` **and** `go1.x.x.windows-amd64.msi` for every release. Browse to <https://go.dev/dl/>, pick the MSI matching your architecture, run it. Same end result: install lands under `C:\Program Files\Go\` and the installer edits the system PATH. Subsequent upgrades come from re-downloading and running a newer MSI, which is why winget tends to win on day-to-day ergonomics. > [!info] Don't use Chocolatey or Scoop *and* winget > Go via Chocolatey (`choco install golang`) or Scoop (`scoop install go`) works too. Pick **one** package manager for Go and stay there — running two on top of each other leaves you with duplicate installs and PATH confusion. The General guide standardizes on winget; this guide does the same. ### Configure paths Go uses two directories by default: - **`GOROOT`** — where Go itself is installed (`C:\Program Files\Go` after either install above). Don't override it — the installer already pointed Go at the right place. - **`GOPATH`** — where modules are cached and user-installed binaries live. On Windows this defaults to `%USERPROFILE%\go` (i.e. `$HOME\go` in PowerShell). Anything you `go install` lands in `%USERPROFILE%\go\bin`. The installer adds `C:\Program Files\Go\bin` to the **system** PATH, but it does **not** add `%USERPROFILE%\go\bin`. You'll want that on PATH so tools like `gopls`, `dlv`, `staticcheck`, `golangci-lint`, and any project binaries you `go install` are callable by name. Two ways to do it, pick one: **Option 1 — persist via the user PATH environment variable** (works for any shell, survives `$PROFILE` rewrites): ```powershell [Environment]::SetEnvironmentVariable( "PATH", $env:PATH + ";$HOME\go\bin", "User" ) ``` Open a new PowerShell window for the change to take effect. The new entry persists across reboots and is visible to cmd, VS Code's integrated terminal, and any GUI app you launch. **Option 2 — append to `$PROFILE`** (cleaner if you already manage PATH there): The General guide's `$PROFILE` already prepends `$HOME\.local\bin`. Append a matching line for Go's bin directory just below it: ```powershell # ── PATH additions ────────────────────────────────────────── # Per-user bin first so locally-installed tools win $env:PATH = "$HOME\.local\bin;" + $env:PATH # Go user-installed binaries (gopls, dlv, golangci-lint, project binaries) $env:PATH = "$HOME\go\bin;" + $env:PATH ``` Reload with `. $PROFILE` (or open a new tab). > [!info] Which option to pick > If your dev work is exclusively in PowerShell, the `$PROFILE` route is fine and easier to back up via dotfiles. If you ever launch a GUI app (a Fyne window, a separate `cmd.exe`, a build server) and expect it to find `gopls` or `air`, you need Option 1 — the user PATH env variable is the only place GUI processes look. Doing both is harmless. ### Installing older Go versions (when needed) The same versioned-binary mechanism that ships on Linux and macOS works on Windows. The downloader helper places the chosen Go release in `%USERPROFILE%\sdk\goX.Y.Z\`: ```powershell # Install the 1.21.13 downloader helper go install golang.org/dl/go1.21.13@latest # Download and install that version (one-time, ~80MB) go1.21.13 download # Use it explicitly go1.21.13 version go1.21.13 build . ``` Each version is invoked by its versioned command name (`go1.21.13`, `go1.22.5`, etc.); they don't interfere with your default `go`. Useful when a legacy project pins to an older language version in its `go.mod` directive. --- ## Global Go tooling Install these once — they're used across all Go projects. Every one of them lands in `%USERPROFILE%\go\bin`, which is on your PATH from the previous step. ```powershell # Official language server (powers editor intelligence) go install golang.org/x/tools/gopls@latest # Delve — the Go debugger go install github.com/go-delve/delve/cmd/dlv@latest # staticcheck — the de facto standard linter beyond `go vet` go install honnef.co/go/tools/cmd/staticcheck@latest # goimports — auto-manages imports go install golang.org/x/tools/cmd/goimports@latest ``` ### golangci-lint — pick winget on Windows On Linux the project's `install.sh` is the canonical path; on Windows that script doesn't apply natively. Two clean options: **Recommended — winget** (gets the official prebuilt binary release for your arch): ```powershell winget install -e --id golangci-lint ``` **Fallback — `go install` from source** (always works, doesn't need a separate package source, but compiles a chunky tool from source): ```powershell go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest ``` The Go compile is fast enough that the fallback is genuinely fine — pick winget for binary releases that match the upstream release cadence, pick `go install` if you're already in a shell and don't want to drop out for a package manager. ### Verify Open a new PowerShell window (so the freshly-installed binaries are picked up from PATH) and confirm: ```powershell gopls version dlv version staticcheck -version goimports -h golangci-lint --version ``` > [!info] Letting the VS Code Go extension install these for you > The Go extension (installed below) also offers a one-click installer for all of these via the Command Palette: `Ctrl+Shift+P` → "Go: Install/Update Tools". If you skip the manual `go install` commands above, accepting the extension's prompt when you first open a `.go` file does the equivalent work. The manual route is preferred because the same binaries are then available outside VS Code (in plain `pwsh`, in a build script, etc.). --- ## CGO and a C compiler A subset of Go packages — most notably **Fyne** (used in the GUI demo below) and any database driver that wraps a C library — use CGO and need a host C toolchain at build time. Plain `go build`/`go run` on a CGO-free package doesn't need any of this. Windows native CGO supports two toolchain families: | Toolchain | Origin | Best for | |-----------|--------|----------| | **MSYS2 ucrt64** (`mingw-w64-ucrt-x86_64-gcc`) | GCC under the MSYS2 packaging system | Fyne, most CGO packages out there (the upstream expects gcc-style flags) | | **MSVC** (Visual Studio Build Tools) | Microsoft's official compiler | Mixed projects where C dependencies are also MSVC-compiled | | **TDM-GCC** | Older mingw-w64 redistribution | Legacy projects pinned to it | For Fyne specifically — and as a sensible default across the broader Go ecosystem on Windows — **MSYS2 ucrt64 is the recommended path**. Fyne's build instructions historically assume gcc-style tooling, and the wider CGO ecosystem tends to test on mingw before MSVC. > [!info] If you've already done the C guide > The [[C_Development_Windows_Native_Setup|C guide]] installs Visual Studio Build Tools (MSVC). That toolchain *can* satisfy CGO too via `CC=cl`, but you'll hit rough edges with packages that pass gcc-only flags. Install MSYS2 alongside MSVC — they coexist fine. ### Install MSYS2 and the ucrt64 GCC ```powershell winget install -e --id MSYS2.MSYS2 ``` This drops the MSYS2 environment at `C:\msys64\` and adds Start menu shortcuts for several MSYS2 shells. The one you want for Go CGO is the **UCRT64** shell. Launch **MSYS2 UCRT64** from the Start menu. Inside that shell: ```bash # Update the package database (one-time) pacman -Syu # Install the gcc toolchain. On x64 hosts: pacman -S --needed mingw-w64-ucrt-x86_64-gcc make # On ARM64 hosts, install the clangarm64 equivalent: # pacman -S --needed mingw-w64-clang-aarch64-clang make ``` Then back in PowerShell, add the matching MSYS2 bin directory to your user PATH so Go's CGO can find `gcc`: ```powershell # x64 [Environment]::SetEnvironmentVariable("PATH", $env:PATH + ";C:\msys64\ucrt64\bin", "User") # ARM64 # [Environment]::SetEnvironmentVariable("PATH", $env:PATH + ";C:\msys64\clangarm64\bin", "User") ``` Open a new PowerShell window and verify: ```powershell gcc --version go env CGO_ENABLED # should be "1" by default ``` > [!warning] ARM64 caveat for Fyne > MSYS2 has excellent x64 coverage. ARM64 (`clangarm64`) coverage is improving but the package set is smaller and not every CGO library compiles cleanly there yet. If you hit a wall building Fyne on Snapdragon, the **Wails** alternative (covered later in this guide) uses Edge WebView2 with no CGO — it works on both architectures with zero compiler setup. --- ## VS Code extensions for Go ```powershell # The official Go extension from the Go team code --install-extension golang.go ``` That's it — one extension. After installing, open any `.go` file and VS Code will offer to install Go's supporting tools (gopls, dlv, etc.). If you already ran the `go install` commands above, it'll detect them and skip the install. > [!info] Why just one extension? > Go's official extension bundles everything: gopls language server, Delve debugger, test runner, and integration with every standard Go tool. The dev experience matches what you get on macOS and Linux. --- ## VS Code settings Append to `%APPDATA%\Code\User\settings.json`: ```jsonc { // ── Go ──────────────────────────────────────────────────── "[go]": { "editor.defaultFormatter": "golang.go", "editor.tabSize": 4, "editor.insertSpaces": false, "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.organizeImports": "explicit" } }, // Go tooling "go.useLanguageServer": true, "go.lintTool": "golangci-lint", "go.lintOnSave": "package", "go.formatTool": "goimports", "go.testOnSave": false, "go.coverOnSave": false, "go.toolsManagement.autoUpdate": true, "gopls": { "ui.semanticTokens": true, "ui.completion.usePlaceholders": true, "ui.diagnostic.staticcheck": true } } ``` > [!info] Tabs, not spaces, in Go > Go canonically uses tabs for indentation — it's enforced by `gofmt`. The `[go]` block above sets `insertSpaces: false` so VS Code doesn't fight `gofmt` on save. Don't change this; the rest of the toolchain assumes tabs. > [!info] Where does this live again? > On Windows VS Code stores user settings at `%APPDATA%\Code\User\settings.json`. Quickest way to open: in VS Code, `Ctrl+Shift+P` → "Preferences: Open User Settings (JSON)". The General guide covers the broader file. --- ## Full demo: A calculator program This builds the same four-function calculator as the other guides, but Go-idiomatic. ### Create the project ```powershell cd $HOME\projects mkdir calc-go cd calc-go # Initialize module — the path is by convention your GitHub path, # even if you haven't created the repo yet. Replace "yourusername". go mod init github.com/yourusername/calc-go ``` This creates `go.mod`, Go's dependency manifest. ### `calc.go` — the core logic ```go // Package calc implements a four-function calculator. package calc import ( "fmt" ) // Add returns a + b. func Add(a, b float64) float64 { return a + b } // Sub returns a - b. func Sub(a, b float64) float64 { return a - b } // Mul returns a * b. func Mul(a, b float64) float64 { return a * b } // Div returns a / b, or an error if b is zero. func Div(a, b float64) (float64, error) { if b == 0 { return 0, fmt.Errorf("division by zero") } return a / b, nil } // Calculate dispatches to the appropriate operation based on op. // Supported ops: "+", "-", "*", "x", "/". func Calculate(a float64, op string, b float64) (float64, error) { switch op { case "+": return Add(a, b), nil case "-": return Sub(a, b), nil case "*", "x": return Mul(a, b), nil case "/": return Div(a, b) default: return 0, fmt.Errorf("unknown operator %q", op) } } ``` ### `cmd/calc/main.go` — the command-line entry point Create the directory first: ```powershell mkdir cmd\calc ``` ```go // Command calc is a four-function command-line calculator. package main import ( "fmt" "os" "strconv" calc "github.com/yourusername/calc-go" ) func main() { if len(os.Args) != 4 { printUsage() os.Exit(1) } a, err := strconv.ParseFloat(os.Args[1], 64) if err != nil { fmt.Fprintf(os.Stderr, "Error: invalid number %q\n", os.Args[1]) os.Exit(1) } b, err := strconv.ParseFloat(os.Args[3], 64) if err != nil { fmt.Fprintf(os.Stderr, "Error: invalid number %q\n", os.Args[3]) os.Exit(1) } result, err := calc.Calculate(a, os.Args[2], b) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } // Print as integer if it came out whole if result == float64(int64(result)) { fmt.Println(int64(result)) } else { fmt.Printf("%g\n", result) } } func printUsage() { prog := os.Args[0] fmt.Fprintf(os.Stderr, "Usage: %s <number> <op> <number>\n", prog) fmt.Fprintln(os.Stderr, " op: + - * /") fmt.Fprintf(os.Stderr, "Example: %s 6 + 7\n", prog) } ``` > [!tip] PowerShell argument quoting for `*` > PowerShell expands `*` as a wildcard in some contexts. When you run `.\calc 4 * 5` later, it works because the args are passed as plain strings to the binary, but if you ever see odd behavior wrap the operator in quotes: `.\calc 4 "*" 5`. The `x` alternative the code accepts (`.\calc 4 x 5`) sidesteps the issue entirely. ### `calc_test.go` — tests using Go's built-in testing ```go package calc import ( "testing" ) func TestCalculate(t *testing.T) { tests := []struct { name string a float64 op string b float64 want float64 wantErr bool }{ {"add", 6, "+", 7, 13, false}, {"sub", 10, "-", 3, 7, false}, {"mul with *", 4, "*", 5, 20, false}, {"mul with x", 4, "x", 5, 20, false}, {"div", 20, "/", 4, 5, false}, {"div by zero", 5, "/", 0, 0, true}, {"unknown op", 1, "?", 2, 0, true}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { got, err := Calculate(tc.a, tc.op, tc.b) if (err != nil) != tc.wantErr { t.Fatalf("Calculate(%v, %q, %v) error = %v; wantErr %v", tc.a, tc.op, tc.b, err, tc.wantErr) } if !tc.wantErr && got != tc.want { t.Errorf("Calculate(%v, %q, %v) = %v; want %v", tc.a, tc.op, tc.b, got, tc.want) } }) } } ``` ### `.golangci.yml` — linter config ```yaml run: timeout: 2m linters: enable: - gofmt - goimports - govet - staticcheck - errcheck - revive - unused - ineffassign - gosimple issues: exclude-use-default: false ``` ### `.gitignore` ``` # Binaries calc calc.exe calc-go calc-go.exe calc-gui calc-gui.exe /bin/ # Go test output *.out *.test coverage.html # IDE .vscode/settings.json ``` > [!info] `.exe` suffixes on Windows > Go automatically appends `.exe` to binaries built on Windows. The Linux/macOS versions of this `.gitignore` only listed the unsuffixed name; the Windows version lists both so a binary built here, then committed by mistake from a Unix machine, is still caught. ### Build and run ```powershell # Build (produces calc.exe in the current directory) go build -o calc.exe .\cmd\calc # Run .\calc.exe 6 + 7 .\calc.exe 10 - 3 .\calc.exe 4 x 5 .\calc.exe 20 / 4 # Or just run directly without producing a binary go run .\cmd\calc 6 + 7 # Install globally into $HOME\go\bin (so you can call `calc` from anywhere) go install .\cmd\calc calc 6 + 7 # Tests go test ./... # Verbose tests with coverage go test -v -cover ./... # Lint golangci-lint run # Format all files in place gofmt -w . # or with imports auto-managed: goimports -w . ``` > [!info] Forward slashes also work > Go accepts `./cmd/calc` everywhere on Windows too — both `./cmd/calc` and `.\cmd\calc` mean the same thing to the toolchain. PowerShell handles either fine. Use whichever reads better; this guide uses backslashes to match the rest of the Windows native guides. ### Debug in VS Code The Go extension handles debugging automatically via Delve. Create `.vscode\launch.json`: ```json { "version": "0.2.0", "configurations": [ { "name": "Debug calc", "type": "go", "request": "launch", "mode": "auto", "program": "${workspaceFolder}/cmd/calc", "args": ["6", "+", "7"] }, { "name": "Debug tests", "type": "go", "request": "launch", "mode": "test", "program": "${workspaceFolder}" } ] } ``` Set a breakpoint in `calc.go`, hit **F5**, and you'll step through with full variable inspection. Delve handles goroutines correctly, which matters for any concurrent Go code. > [!info] Use forward slashes in `launch.json` > VS Code's `${workspaceFolder}` expands to a Windows path, but VS Code itself normalizes `/` and `\` in `program` paths. Forward slashes work everywhere and avoid the JSON escaping dance `"\\"` requires. ### Publish to GitHub ```powershell git init git add . git commit -m "Initial commit: four-function calculator" gh repo create calc-go --public --source=. --remote=origin --push ``` > [!tip] Make it public for Go > Go's module system fetches dependencies directly from public git repos. If you plan to ever import this module elsewhere (`go get github.com/yourusername/calc-go`), the repo must be public. Private modules work too but need extra auth config (`GOPRIVATE` env var, GitHub PAT, etc.). ### `CLAUDE.md` for this project ```markdown # Project: calc-go ## Purpose Four-function command-line calculator in Go — pedagogical example. ## Conventions - Go (current stable), standard module layout - Package structure: root package for library code, cmd/calc for CLI - Tests colocated with code (*_test.go) - Lint with golangci-lint (config in .golangci.yml) ## Commands (PowerShell) - Build: `go build -o calc.exe .\cmd\calc` - Run: `go run .\cmd\calc <a> <op> <b>` - Install to $HOME\go\bin: `go install .\cmd\calc` - Test: `go test -v -cover ./...` - Lint: `golangci-lint run` - Format: `gofmt -w .` or `goimports -w .` ## Style - Tab indentation (gofmt enforced) - Standard Go style — small interfaces, explicit errors as return values - Godoc comments on every exported identifier - Table-driven tests (see calc_test.go) ``` --- ## Full demo: A calculator GUI Same calculator, wrapped in a graphical interface. This uses **Fyne**, the most popular Go GUI toolkit in 2026 — cross-platform, native-feeling Win32 windows on Windows, and the same code runs unchanged on macOS and Linux. ### Why Fyne? | Option | Pros | Cons | |--------|------|------| | **Fyne** | Cross-platform, Material-inspired widgets, simple API, native Win32 window | Uses CGO (needs a C compiler) — see MSYS2 setup above | | **Wails** | HTML/JS frontend, Go backend, uses Edge WebView2 (already on Windows 11) | Bundles a webview into your binary; web-tech mental model | | **Gio** | Immediate-mode, very fast, no CGO | Steeper learning curve, smaller widget set | | **Walk** | Pure Win32 binding, no CGO at runtime | Windows-only, lower-level than Fyne | | **go-app** | PWA-style, runs in browser and as desktop | Needs a server or WebAssembly | For a pedagogical calculator that should match the other guides, Fyne is the right default. If you're on ARM64 Windows and don't want to fight CGO, jump to the **Wails** appendix at the end of this section — it uses Edge WebView2, ships natively for both arches, and needs no compiler. ### Prerequisites for Fyne on Windows Fyne uses CGO and renders through OpenGL. You need: 1. **A C compiler that Go's CGO can find** — set up the MSYS2 ucrt64 GCC per the **CGO and a C compiler** section above. 2. **`CGO_ENABLED=1`** — Go's default. Confirm with `go env CGO_ENABLED`. 3. **OpenGL drivers** — already present on any normal Windows 11 install; nothing to do. If `go env CC` returns empty after `gcc` is on PATH, set it explicitly: ```powershell [Environment]::SetEnvironmentVariable("CC", "gcc", "User") ``` Open a new PowerShell window and `go env CC` should now print `gcc`. ### Add Fyne to the project ```powershell go get fyne.io/fyne/v2@latest go mod tidy ``` This downloads Fyne and its transitive dependencies into the module cache and updates `go.mod`/`go.sum`. ### `cmd/calc-gui/main.go` — the GUI Create the directory first: ```powershell mkdir cmd\calc-gui ``` ```go // Command calc-gui is the graphical version of the four-function calculator. package main import ( "fmt" "strconv" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" calc "github.com/yourusername/calc-go" ) // CalculatorState holds the calculator's internal state. // Factored out so it can be unit-tested independently of Fyne. type CalculatorState struct { Current string Stored float64 PendingOp string HasStored bool Display string } func NewCalculatorState() *CalculatorState { return &CalculatorState{Display: "0"} } func (s *CalculatorState) HandlePress(label string) { switch { case label >= "0" && label <= "9", label == ".": s.Current += label s.Display = s.Current case label == "+" || label == "-" || label == "*" || label == "/": s.applyPending() s.PendingOp = label case label == "=": s.applyPending() s.PendingOp = "" } } func (s *CalculatorState) Clear() { s.Current = "" s.Stored = 0 s.PendingOp = "" s.HasStored = false s.Display = "0" } func (s *CalculatorState) applyPending() { if s.Current == "" { return } value, err := strconv.ParseFloat(s.Current, 64) if err != nil { s.Display = "Error" s.resetInternal() return } if !s.HasStored || s.PendingOp == "" { s.Stored = value s.HasStored = true } else { result, err := calc.Calculate(s.Stored, s.PendingOp, value) if err != nil { s.Display = "Error" s.resetInternal() return } s.Stored = result } if s.Stored == float64(int64(s.Stored)) { s.Display = strconv.FormatInt(int64(s.Stored), 10) } else { s.Display = fmt.Sprintf("%g", s.Stored) } s.Current = "" } func (s *CalculatorState) resetInternal() { s.Current = "" s.Stored = 0 s.PendingOp = "" s.HasStored = false } var buttonGrid = [][]string{ {"7", "8", "9", "/"}, {"4", "5", "6", "*"}, {"1", "2", "3", "-"}, {"0", ".", "=", "+"}, } func main() { a := app.New() w := a.NewWindow("Calculator") w.Resize(fyne.NewSize(280, 360)) state := NewCalculatorState() displayLabel := widget.NewLabel(state.Display) displayLabel.Alignment = fyne.TextAlignTrailing displayLabel.TextStyle = fyne.TextStyle{Monospace: true, Bold: true} // Build button grid var rows []fyne.CanvasObject rows = append(rows, container.NewPadded(displayLabel)) for _, row := range buttonGrid { var rowButtons []fyne.CanvasObject for _, label := range row { label := label // capture for closure btn := widget.NewButton(label, func() { state.HandlePress(label) displayLabel.SetText(state.Display) }) rowButtons = append(rowButtons, btn) } rows = append(rows, container.NewGridWithColumns(4, rowButtons...)) } clearBtn := widget.NewButton("Clear", func() { state.Clear() displayLabel.SetText(state.Display) }) rows = append(rows, clearBtn) w.SetContent(container.NewVBox(rows...)) w.ShowAndRun() } ``` ### Tests for the GUI state machine Since `CalculatorState` is pure logic (no Fyne types), it can be unit-tested without a display. Create `cmd\calc-gui\main_test.go`: ```go package main import "testing" func TestCalculatorState(t *testing.T) { tests := []struct { name string presses []string want string }{ {"initial display", []string{}, "0"}, {"single digit", []string{"5"}, "5"}, {"multi-digit", []string{"1", "2", "3"}, "123"}, {"addition", []string{"6", "+", "7", "="}, "13"}, {"subtraction", []string{"1", "0", "-", "3", "="}, "7"}, {"multiplication", []string{"4", "*", "5", "="}, "20"}, {"division", []string{"2", "0", "/", "4", "="}, "5"}, {"chained left-to-right", []string{"2", "+", "3", "*", "4", "="}, "20"}, {"division by zero", []string{"5", "/", "0", "="}, "Error"}, {"decimal", []string{"1", ".", "5", "+", "2", ".", "5", "="}, "4"}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { s := NewCalculatorState() for _, p := range tc.presses { s.HandlePress(p) } if s.Display != tc.want { t.Errorf("after presses %v, display = %q; want %q", tc.presses, s.Display, tc.want) } }) } } func TestCalculatorClear(t *testing.T) { s := NewCalculatorState() for _, p := range []string{"5", "+", "3"} { s.HandlePress(p) } s.Clear() if s.Display != "0" || s.Current != "" || s.HasStored { t.Errorf("Clear() did not reset state: %+v", s) } } ``` ### Run everything ```powershell # Run the CLI go run .\cmd\calc 6 + 7 # Run the GUI (a native Win32 window appears) go run .\cmd\calc-gui # Run all tests (library + CLI + GUI state machine) go test ./... # Build standalone binaries go build -o calc.exe .\cmd\calc go build -o calc-gui.exe .\cmd\calc-gui .\calc-gui.exe ``` > [!info] Shipping Fyne apps > For a distributable `.exe` with an icon and a proper Windows manifest, install the Fyne CLI and use `fyne package`: > ```powershell > go install fyne.io/tools/cmd/fyne@latest > fyne package -os windows -icon Icon.png > ``` > The output is a self-contained `.exe` that doesn't require MSYS2 on the target machine — CGO is statically linked. ### Hide the console window for GUI builds By default `go build` on Windows produces a binary that opens a console window alongside the GUI. To suppress the console for a release build: ```powershell go build -ldflags "-H windowsgui" -o calc-gui.exe .\cmd\calc-gui ``` Keep the console version for development — `fmt.Println` debug output goes there. `fyne package` already passes the `-H windowsgui` flag automatically. ### ARM64-friendly alternative: Wails If MSYS2 ucrt64 ARM64 packaging is giving you trouble, swap Fyne for **Wails**. Wails uses Edge WebView2 — which ships with Windows 11 on both arches — and writes the UI in HTML/CSS/JS with a Go backend over a JSON-RPC bridge. No CGO compiler required. Quick install and scaffold (in a fresh directory, outside `calc-go`): ```powershell go install github.com/wailsapp/wails/v2/cmd/wails@latest wails doctor wails init -n calc-wails -t vanilla cd calc-wails wails dev ``` The same `calc.Calculate` library you wrote earlier imports cleanly and the browser-rendered front-end calls it through Wails' generated bindings. This is the path to take if you want a desktop Go app on Windows ARM64 today with the least friction. --- ## Starship prompt — Go auto-detection Starship's built-in `[golang]` module shows the active Go version when a `.go` file or `go.mod` is in the directory. Your prompt will show something like: ``` ~\projects\calc-go main 1.24.4 ❯ ``` The General guide's `starship.toml` already has a `[golang]` block configured — no changes needed for Go. --- ## Installing CLI tools from other Go projects Go's `go install` is its equivalent of `uv tool install` and `cargo install` — it installs binaries globally (to `%USERPROFILE%\go\bin`) from any Go repository: ```powershell # Install hey (HTTP load testing tool) go install github.com/rakyll/hey@latest # Install air (live-reload for Go apps) go install github.com/air-verse/air@latest # Install gh-dash (fancier gh pr list) go install github.com/dlvhdr/gh-dash@latest ``` These land in `%USERPROFILE%\go\bin\` (with `.exe` suffixes on Windows) and are callable by name anywhere. --- ## direnv — auto-activate this project's environment direnv can keep a project's `go install` output and built binaries local when you `cd` in (see [[General_Development_Windows_Native_Setup]] for the PowerShell hook). ```bash # from the project root cat > .envrc <<'EOF' export GOBIN="$PWD/bin" PATH_add bin EOF direnv allow ``` > [!warning] `.envrc` is bash on Windows > direnv runs `.envrc` through bash and applies the result to PowerShell, so write bash syntax (forward slashes are fine) and keep Git for Windows on `PATH`. `PATH_add bin` makes `bin\calc.exe` callable as `calc`; you usually don't want direnv's `layout go`, which rewrites `GOPATH`. --- ## Troubleshooting > [!warning] `go: command not found` after installing > winget's PATH update only applies to **new** shells. Close and reopen Windows Terminal. If still missing, confirm `C:\Program Files\Go\bin` is in the system PATH: `[Environment]::GetEnvironmentVariable("PATH", "Machine") -split ';' | Select-String -Pattern 'Go'`. If absent, the MSI install was probably not run with admin rights — reinstall and accept the elevation prompt. > [!warning] `gopls` or `dlv` not found after `go install` > Your `%USERPROFILE%\go\bin` isn't on PATH. Add it via Option 1 in the "Configure paths" section, then open a new PowerShell window. Confirm with `[Environment]::GetEnvironmentVariable("PATH", "User") -split ';' | Select-String -Pattern 'go\\bin'`. > [!warning] Fyne build fails with "cgo: C compiler "gcc" not found" > `CC=gcc` is set but `gcc.exe` isn't reachable. Either you haven't added the MSYS2 ucrt64 bin directory to PATH yet (`C:\msys64\ucrt64\bin` on x64; `C:\msys64\clangarm64\bin` on ARM64), or you opened the shell before adding it. Open a new PowerShell window and try `gcc --version` first. > [!warning] Fyne build fails with "gl.h: No such file" or OpenGL link errors > The mingw GCC install was incomplete — re-run `pacman -S --needed mingw-w64-ucrt-x86_64-gcc make` inside the MSYS2 UCRT64 shell. On ARM64, the same step using `mingw-w64-clang-aarch64-clang`. > [!warning] CGO works but `fyne package` errors with "rsrc" or icon embed failures > `fyne package -os windows` needs the resource-compiler stub `rsrc`. Install it: `go install github.com/akavel/rsrc@latest`. Same `%USERPROFILE%\go\bin` PATH applies. > [!warning] Fyne window doesn't open inside a Hyper-V or Parallels VM > Fyne needs OpenGL. Enable 3D acceleration in the hypervisor. The CLI binary and the state-machine tests run fine without a display. > [!warning] `go build` complains about `GOPROXY` or network issues > Go fetches modules from `proxy.golang.org` by default. On a corporate network that blocks it, set `GOPROXY=direct` temporarily — `$env:GOPROXY = "direct"` — or point at an internal proxy: `$env:GOPROXY = "https://your-proxy/,direct"`. > [!warning] `go: cannot find main module` in a folder that has `.go` files > You haven't run `go mod init` yet. Modules are mandatory in modern Go. Pick a module path (typically your future GitHub path) and run `go mod init github.com/you/projectname`. > [!warning] golangci-lint complains "no Go files" or runs slowly the first time > The first run downloads the linter rule definitions and warms its cache. Subsequent runs are fast. If "no Go files" appears, you're running it outside a Go module directory — `cd` into one with a `go.mod`. > [!warning] Windows Defender flags freshly-built Go binaries > Some Defender heuristics get nervous about unsigned executables that include networking code. Add an exclusion for `%USERPROFILE%\go\bin` and your project's output directory under Windows Security → Virus & threat protection → Exclusions. Distributing your binary externally? Sign it with `signtool.exe` (covered in the C guide). --- ## Summary — the one-shot Go addition > [!warning] This is a checklist, not a script > Copy and paste one block at a time. The PATH addition steps and the `go install` commands depend on the previous step having taken effect — open a **new** PowerShell window between steps when the comments say to. After running these, **append the Go settings block** above to `%APPDATA%\Code\User\settings.json`. ```powershell # 1. Install Go itself (architecture-aware via winget) winget install -e --id GoLang.Go # 2. <Open a NEW PowerShell window so Go is on PATH> go version # 3. Add $HOME\go\bin to user PATH so `go install` binaries are callable [Environment]::SetEnvironmentVariable("PATH", $env:PATH + ";$HOME\go\bin", "User") # 4. <Open a NEW PowerShell window again so the new PATH applies> # 5. Global Go tooling go install golang.org/x/tools/gopls@latest go install github.com/go-delve/delve/cmd/dlv@latest go install honnef.co/go/tools/cmd/staticcheck@latest go install golang.org/x/tools/cmd/goimports@latest winget install -e --id golangci-lint # 6. CGO toolchain (for Fyne and other CGO packages) — MSYS2 ucrt64 winget install -e --id MSYS2.MSYS2 # Then in the MSYS2 UCRT64 shell: # pacman -Syu # pacman -S --needed mingw-w64-ucrt-x86_64-gcc make # Back in PowerShell, on x64: [Environment]::SetEnvironmentVariable("PATH", $env:PATH + ";C:\msys64\ucrt64\bin", "User") # On ARM64, use the clangarm64 path instead: # [Environment]::SetEnvironmentVariable("PATH", $env:PATH + ";C:\msys64\clangarm64\bin", "User") # 7. <Open a NEW PowerShell window so gcc is on PATH> gcc --version # 8. VS Code extension (just one — it bundles everything) code --install-extension golang.go # 9. Verify go version; gopls version; dlv version; golangci-lint --version ``` About five minutes on top of the general setup (most of which is MSYS2's first `pacman -Syu`). --- ## Related notes **Same setup on other platforms:** - [[Go_Development_Mac_Tahoe_Setup]] — the macOS counterpart - [[Go_Development_Ubuntu_Setup]] — the Linux counterpart **The Windows general setup this builds on:** - [[General_Development_Windows_Native_Setup]] **Other Windows native language guides:** - [[Python_Development_Windows_Native_Setup]] - [[C_Development_Windows_Native_Setup]] - [[Ruby_Development_Windows_Native_Setup]] - [[Rust_Development_Windows_Native_Setup]] **Topic references:** - [Go Module Patterns](https://go.dev/ref/mod) - [Delve Debugging Cheat Sheet](https://github.com/go-delve/delve/tree/master/Documentation/cli) - [Fyne — Getting Started on Windows](https://docs.fyne.io/started/) - [Wails — Quickstart](https://wails.io/docs/gettingstarted/installation) - [MSYS2 — UCRT64 Environment](https://www.msys2.org/docs/environments/)