Go Setup and Your First Program
AI-assistedDrafted with AI assistance and reviewed before publishing; examples are version-pinned. Verify against official docs when in doubt.
Getting Go installed is easy. Knowing where your code lives and what go run actually does is where beginners stall. This chapter sorts out the toolchain and the smallest workflow that works.
Install and verify
Download the installer for your platform from go.dev/dl. On macOS the .pkg puts the toolchain in /usr/local/go; on Linux, unpack the tarball into /usr/local and make sure /usr/local/go/bin is on your PATH. On Windows, the MSI writes to C:\Program Files\Go and updates the system PATH — open a new terminal afterwards, or the old environment will still be in effect.
go version
# go version go1.24.0 darwin/arm64
Then check the three variables that matter:
go env GOPATH GOMODCACHE GOPROXY
GOPATH: the workspace root,~/goby default. It no longer holds your source code.GOMODCACHE: where downloaded modules are unpacked,$GOPATH/pkg/modby default.GOPROXY: the module proxy. If the default is slow on your network, point it at a mirror.
go env -w GOPROXY=https://proxy.golang.org,direct
go env -w persists the value in the Go environment file, so you do not have to export it in every shell.
Hello, module
Create a directory and initialise a module:
mkdir hello && cd hello
go mod init example/hello
That writes a go.mod:
module example/hello
go 1.24
Now main.go:
package main
import "fmt"
func main() {
fmt.Println("Hello, Go")
}
go run .
# Hello, Go
go run compiles into a temporary directory and executes immediately — no binary is left behind. Use go build when you want an artifact:
go build -o hello .
./hello
On Windows the output is hello.exe and you run .\hello.exe. A useful detail: go build . with no -o names the binary after the directory, and building a package that is not main produces nothing at all — it only type-checks and caches.
The daily three
go fmt ./... # rewrite files into canonical format
go vet ./... # static checks the compiler does not do
go test ./...
go fmt is non-negotiable in practice: tabs, spacing and line breaks are decided by the tool, so no team argues about style. Wire it into your editor’s save hook and forget it.
go vet catches things that compile fine but are almost certainly wrong — a Printf verb that does not match its argument, a self-assignment, a struct tag that is not a valid tag. Run it before every commit; go test does not run it for you.
Module mode: no more GOPATH projects
Older tutorials tell you to put code in $GOPATH/src. That was GOPATH mode. Since Go 1.16, module mode is the default:
- Any directory containing a
go.modis a module and can live anywhere on disk. - Dependency versions are recorded in
go.mod; checksums ingo.sum. Commit both. - You rarely edit
go.modby hand:
go get github.com/spf13/cobra@latest # fetch and record
go mod tidy # drop unused, add missing
go mod tidy is the command you will run most: delete a feature, run it, and the leftover dependencies disappear from your module file. go.sum must be committed — without it, other machines fail checksum verification.
A layout that scales
A single file needs no structure. Once the code passes a few hundred lines, split by responsibility:
hello/
├── go.mod
├── go.sum
├── main.go # package main: flags and wiring only
├── internal/
│ └── greet/
│ └── greet.go # package greet: the actual logic
└── greet_test.go
Two conventions worth adopting on day one:
internal/is import-restricted. Packages under it can only be imported by code in the same module. That is enforced by the toolchain, not by convention, so it is a real boundary.mainonly wires things together. Parse flags, build dependencies, call into a package. Logic inmaincannot be tested or reused.
Packages and imports
package greet
import (
"fmt"
"strings"
)
func Hello(name string) string {
return fmt.Sprintf("hello, %s", strings.TrimSpace(name))
}
- The import path is the module path plus the directory:
example/hello/internal/greet. - Package names are short, lower-case, no underscores or camelCase (
httputil, nothttp_util). The package name does not have to match the directory, but confusing readers is not worth it. - An unused import is a compile error, not a warning. This is deliberate: it keeps the dependency graph honest.
Common mistakes
1. go: command not found right after installing
The PATH in your current shell predates the install. Open a new terminal; on Windows also confirm the Go bin directory is in the system PATH.
2. “no required module provides package” outside $GOPATH/src
You skipped go mod init. Every project needs one.
3. Relative import paths such as "./greet"
Not allowed in module mode. Use the full module path.
4. Importing a package and not using it
Compile error. Remove the import. The blank identifier _ is only for side-effect imports.
5. Hand-editing versions in go.mod
Easy to desynchronise from go.sum. Let go get and go mod tidy do it.
6. Adding go.sum to .gitignore
Builds on other machines then fail with checksum errors. Commit it.
Summary
- Verify with
go version; setGOPROXYonce viago env -wif the default is slow. - Every project starts with
go mod init; dependencies go throughgo getandgo mod tidy. go runcompiles to a temp dir and executes,go buildleaves a binary.go fmt,go vetandgo testare the daily loop.- Keep implementation in
internal/, keepmainto wiring.
Next: variables, types and the zero-value rule that removes “uninitialised” from the language.