# Go Development Mac Tahoe Setup Adding Go development on top of an Apple Silicon Mac already configured per [[General_Development_Mac_Tahoe_Setup]]. Most of the stack carries over — Ghostty, Starship, Git, GitHub CLI, VS Code, Claude Code, and the zsh setup all work identically. This guide only covers the Go-specific additions. > [!tip] AI assistance is optional > The reference to Claude Code above assumes you completed Part 7 of the General guide. If you skipped it (which is fine — your dev environment works without any AI) or use a different AI tool (Copilot, Cursor, Continue, etc.), 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 system without interfering. Each language's toolchain installs to its own prefix: `uv` under `~/.local/share/uv/`, `rv` under `~/.rv/`, Go under `~/go/` (module cache) and `/opt/homebrew/bin/go` (binary), Rust under `~/.rustup/` and `~/.cargo/`, C/CMake under Homebrew. 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** via Homebrew 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 without fuss, and when they don't, the official per-version installer handles it. If you really want a version manager (e.g., for CI reproducibility across many Go versions), `mise` and `asdf` both support Go. But for 95% of workflows, Homebrew's `go` is sufficient. --- ## Install Go ```bash brew install go ``` This installs the latest stable Go, native arm64 on Apple Silicon. Verify: ```bash go version ``` ### Configure paths Go uses two directories by default: - **`GOPATH`** — where modules are cached and user-installed binaries live. Defaults to `~/go`. - **`GOROOT`** — where Go itself is installed. Set automatically by Homebrew, don't override. The one thing you should set: add `$GOPATH/bin` to your `PATH` so `go install`-ed binaries are callable. Add to `~/.zshrc`: ```bash # Go — enable user-installed binaries export PATH="$HOME/go/bin:$PATH" ``` Reload with `source ~/.zshrc`. ### Installing older Go versions (when needed) If a legacy project requires, say, Go 1.21: ```bash # Install the 1.21.x downloader helper go install golang.org/dl/go1.21.13@latest # Download and install that version go1.21.13 download # Use it explicitly go1.21.13 version go1.21.13 build . ``` Each version is invoked by its versioned command name. They don't interfere with your default `go`. --- ## Global Go tooling Install these once — they're used across all Go projects: ```bash # 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 # golangci-lint — aggregator that runs many linters at once brew install golangci-lint # goimports — auto-manages imports, replaces `gofmt` for many workflows go install golang.org/x/tools/cmd/goimports@latest ``` All of these land in `~/go/bin/`, which is on your PATH from the earlier step. Verify: ```bash gopls version dlv version staticcheck -version golangci-lint --version goimports -h ``` --- ## VS Code extensions for Go ```bash # The official Go extension from the Go team (formerly from Microsoft) code --install-extension golang.go ``` That's it — one extension. After installing, open any `.go` file and VS Code will prompt to install Go's supporting tools (gopls, dlv, etc.). If you already ran the `go install` commands above, it'll detect them. > [!info] Why just one extension? > Unlike the C ecosystem (multiple extensions for formatting, linting, debugging, and build tools), Go's official extension bundles everything: gopls language server, Delve debugger, test runner, and integration with every standard Go tool. It's a model of cohesive tooling. --- ## VS Code settings Append to `~/Library/Application Support/Code/User/settings.json`: ```json { // ── 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`. Don't fight it. --- ## Full demo: A calculator program This builds the same four-function calculator as the C and Ruby versions, but Go-idiomatic. ### Create the project ```bash cd ~/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: `mkdir -p 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) } ``` ### `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-go /bin/ # Go test output *.out *.test coverage.html # IDE .vscode/settings.json ``` ### Build and run ```bash # Build (produces a binary named "calc" in the current directory) go build -o calc ./cmd/calc # Run ./calc 6 + 7 ./calc 10 - 3 ./calc 4 x 5 ./calc 20 / 4 # Or just run directly without producing a binary (fine for development) go run ./cmd/calc 6 + 7 # Install globally into ~/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 . ``` ### 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 is important for any concurrent Go code. ### Publish to GitHub Same workflow as any project (see [[General_Development_Mac_Tahoe_Setup#Publishing a repo to GitHub]]): ```bash 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. ### `CLAUDE.md` for this project ```markdown # Project: calc-go ## Purpose Four-function command-line calculator in Go — pedagogical example. ## Conventions - Go 1.23+, 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 - Build: `go build -o calc ./cmd/calc` - Run: `go run ./cmd/calc <a> <op> <b>` - Install to $GOPATH/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 — pure Go, no CGo system-library dependency issues, native-looking on macOS. ### Why Fyne? | Option | Pros | Cons | |--------|------|------| | **Fyne** | Pure Go, cross-platform, good macOS integration, Material-inspired widgets, simple API | Custom widget rendering (not native controls) | | **Wails** | HTML/JS frontend, Go backend — great for web devs | Bundles a Chromium-derived webview | | **Gio** | Immediate-mode, very fast, used in production by ElementsProject | Steeper learning curve | | **go-app** | PWA-style, runs in browser and as desktop | Needs a running server or WebAssembly | For a pedagogical calculator in Go, Fyne is the right default. Install with `go get`, build with `go build`, ship a single static binary. ### Add Fyne to the project ```bash go get fyne.io/fyne/v2@latest go mod tidy ``` This pulls Fyne into `go.mod` along with its dependencies. ### `cmd/calc-gui/main.go` — the GUI Create the directory first: `mkdir -p 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 ```bash # Run the CLI go run ./cmd/calc 6 + 7 # Run the GUI go run ./cmd/calc-gui # Run all tests (library + CLI + GUI state machine) go test ./... # Build standalone binaries go build -o calc ./cmd/calc go build -o calc-gui ./cmd/calc-gui ./calc-gui ``` > [!info] Shipping Fyne apps > For a distributable macOS `.app` bundle with an icon, install the Fyne CLI tool and use `fyne package`: > ```bash > go install fyne.io/tools/cmd/fyne@latest > fyne package -os darwin -icon Icon.png > ``` > This produces a proper `.app` bundle that can be code-signed and notarized for distribution. --- ## 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. No config needed, but to customize append to `~/.config/starship.toml`: ```toml [golang] format = "[$symbol($version )]($style)" symbol = " " style = "bold cyan" ``` Your prompt will now show something like: ``` ~/projects/calc-go main 1.23.4 ❯ ``` --- ## Installing CLI tools from other Go projects Go's `go install` is its equivalent of `uv tool install` — it installs binaries globally (to `$GOPATH/bin`) from any Go repository: ```bash # 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 `~/go/bin/` and are callable by name anywhere. --- ## Troubleshooting > [!warning] `gopls` or `dlv` command not found after `go install` > Your `$GOPATH/bin` isn't on PATH. Verify with `echo $PATH | grep go/bin`. If missing, add `export PATH="$HOME/go/bin:$PATH"` to `~/.zshrc` and open a fresh terminal. > [!warning] VS Code prompts to install tools every time > The extension auto-installs tools the first time. If it keeps prompting, run `Go: Install/Update Tools` from the command palette and pick all of them. Ensures consistent versions across tools. > [!warning] `go build` complains about `GOPROXY` or network issues > Go fetches modules from `proxy.golang.org` by default. If you're on a restrictive network, set `GOPROXY=direct` temporarily, or configure a local proxy like Athens for offline work. > [!warning] "cannot find package" for a module you just created > Go modules must have a canonical import path. If you ran `go mod init mycalc`, but your `import` uses `github.com/you/mycalc`, it won't resolve. Either match them, or use a relative path for the CLI's import of the library package. > [!warning] `gofmt -l` or `goimports -l` shows files that need formatting in CI > Always commit `gofmt`-clean code. Run `gofmt -w .` before committing, or configure a pre-commit hook. --- ## Summary — the one-shot Go addition > [!warning] This is a checklist, not a script > The block below is **not a shell script you can save and run.** It's a sequence of commands to **copy and paste into your terminal one block at a time**, in order. The `source ~/.zshrc` step and the `go install` commands that follow depend on the previous PATH addition having taken effect. After running these, you also need to **append the recommended Go settings block** from earlier in this guide to `~/Library/Application Support/Code/User/settings.json` — without that, gopls and the linter won't be properly configured. ```bash # Go itself brew install go golangci-lint echo 'export PATH="$HOME/go/bin:$PATH"' >> ~/.zshrc source ~/.zshrc # 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 # VS Code extension (just one — it bundles everything) code --install-extension golang.go # Verify go version && gopls version && dlv version && golangci-lint --version ``` About three minutes on top of the general setup. --- ## Related notes - [[General_Development_Mac_Tahoe_Setup]] - [[Python_Development_Mac_Tahoe_Setup]] - [[C_Development_Mac_Tahoe_Setup]] - [[Ruby_Development_Mac_Tahoe_Setup]] - [[Rust_Development_Mac_Tahoe_Setup]] - [Go Module Patterns](https://go.dev/ref/mod) - [Delve Debugging Cheat Sheet](https://github.com/go-delve/delve/tree/master/Documentation/cli)