# 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/)