Go Setup and Your First Program

Chapter 1 of 16

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, ~/go by default. It no longer holds your source code.
  • GOMODCACHE: where downloaded modules are unpacked, $GOPATH/pkg/mod by 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.mod is a module and can live anywhere on disk.
  • Dependency versions are recorded in go.mod; checksums in go.sum. Commit both.
  • You rarely edit go.mod by 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:

  1. 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.
  2. main only wires things together. Parse flags, build dependencies, call into a package. Logic in main cannot 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, not http_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; set GOPROXY once via go env -w if the default is slow.
  • Every project starts with go mod init; dependencies go through go get and go mod tidy.
  • go run compiles to a temp dir and executes, go build leaves a binary.
  • go fmt, go vet and go test are the daily loop.
  • Keep implementation in internal/, keep main to wiring.

Next: variables, types and the zero-value rule that removes “uninitialised” from the language.