Hello, Go
The idea
Every language needs a translator between what you type and what the machine actually runs. Go's translator is the go tool itself: it reads your text, checks it's grammatically valid Go, and turns it into something your computer can execute directly. That checking step is why Go feels strict — it refuses to run anything it can't fully understand first, and it tells you exactly where it got confused.
A Go program starts from one fixed address: a function named main, inside a package named main. Think of package main as the building and func main() as its front door — the go tool always walks in through that door first, no matter how many other files or functions exist inside.
Installing Go
You need the go tool on your machine before any of this runs.
Linux and WSL (if you're on Windows, you're almost certainly working inside WSL already, so these are your steps too):
curl -LO https://go.dev/dl/go1.22.linux-amd64.tar.gz
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.22.linux-amd64.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
source ~/.bashrc
sudo runs a single command with administrator permissions — here, because writing into /usr/local is normally off-limits. PATH is the list of folders your shell searches when you type a command name; adding Go's bin folder to it is what lets you type go from anywhere instead of spelling out the full install path every time.
macOS: download the .pkg installer from go.dev/dl and run it — it sets up PATH for you.
Check it worked:
go version
Use Go 1.22 or newer: this course uses features added in 1.22, and go.dev/dl always offers the latest. If your computer has an ARM processor, pick the arm64 file instead of amd64.
You also need somewhere to write code. Visual Studio Code with its Go extension is a good free choice, and any text editor works. Inside a terminal, nano main.go opens a simple one.
package main and func main
package main
import "fmt"
func main() {
fmt.Println("Welcome aboard!")
}
package main tells the go tool "this is a program, not a library to be imported elsewhere." func main() is where execution begins — no arguments in, nothing returned out, just a block of code that runs top to bottom. import "fmt" brings in a toolbox of ready-made code — here, the fmt ("format") package, which knows how to turn values into printed text.
fmt.Println
fmt.Println prints its argument followed by a newline, so each call starts on its own fresh line:
fmt.Println("Building...")
fmt.Println("Done.")
prints:
Building...
Done.
Everything between the quotes is printed exactly as written — including spacing and punctuation. There's no hidden trimming.
Importing several packages and comments
When a program needs more than one package, put the imports in parentheses, one per line, instead of repeating the word import:
package main
import (
"fmt"
"math"
)
func main() {
fmt.Println(math.Pi)
}
3.141592653589793
math is another package from Go's standard library, the toolbox that ships with Go. Writing two separate import lines also works, but the parenthesised form is what you will see in nearly every Go file.
Anything after // on a line is a comment. Go ignores it completely, so you can leave notes for the next reader, or switch a line off for a moment:
// Greet the user
fmt.Println("Hi") // this part is ignored too
Hi
go run and go build
go run does two things in one step: it compiles your file into a temporary program and immediately runs it, then throws the compiled result away.
go run main.go
go run .
Both lines do the same thing — main.go names the file directly; . tells go run to compile whatever Go files are in the current folder. You'll use go run . far more once a program spans more than one file.
go build only compiles. It writes out a real, standalone executable file and stops — it doesn't run anything:
go build -o hello .
./hello
-o hello names the output file hello (no .exe — that's a Windows-specific extension, and inside WSL or Linux or macOS your executable has no extension at all). ./hello then runs it — the ./ tells your shell "run the file sitting right here," since shells don't search the current folder by default.
go run and go build compile the exact same source, so for the same program they always produce the exact same output — go run just skips the step of leaving a file behind.
go mod init
Before Go will build anything, it needs to know it's looking at a module — a named, versioned unit of code. You declare that once per project:
go mod init example.com/hello
This writes a file called go.mod. Look inside it:
cat go.mod
module example.com/hello
go 1.22
Two lines: the module's name, and the minimum Go version it expects (your number will match the Go you installed). You won't touch this file by hand yet — go mod init writes it, and it just needs to exist before go build or go run will work in a new folder.
Running tests and tidying code
Every lab in this course comes with a test file. You run it from the lab's folder:
go test ./...
./... means "this folder and every folder inside it". When everything passes, you get one short line:
ok example.com/hello 0.003s
When something is wrong, the test tells you what it expected and what it got:
--- FAIL: TestGreeting (0.00s)
main_test.go:9: Greeting() = "Hello", want "Hello!"
FAIL
FAIL example.com/hello 0.002s
FAIL
Read the first lines: which test failed, then the file and line, then got versus want. Before it runs your tests, go test also runs go vet, which flags suspicious code such as a Printf whose verbs do not match its values. If vet complains, the message names the line to fix.
Go also has one official code style, applied automatically. Running go fmt ./... rewrites your files with the standard tabs and spacing and lists the files it changed. Run it before you test, and your code will look like everyone else's Go.
Reading compiler errors
Go won't run a program it can't fully parse, and when it refuses, it tells you precisely where and why. A compiler error always has the same shape: file, line, column, then what's wrong.
Write a function name in the wrong case:
package main
import "fmt"
func main() {
fmt.println("shout")
}
./main.go:6:6: undefined: fmt.println
main.go line 6, column 6: fmt.println doesn't exist. Column 6 is where println starts, because the tab counts as column 1 and fmt. fills columns 2 to 5. Go is case-sensitive: the real function is Println with a capital P, and only names that start with a capital letter can be used from another package.
Import something and never use it:
package main
import "fmt"
func main() {
}
./main.go:3:8: "fmt" imported and not used
Go treats an unused import as an error, not a warning — it refuses to compile code that's carrying dead weight.
Forget a closing brace:
package main
import "fmt"
func main() {
fmt.Println("oops")
./main.go:7:1: syntax error: unexpected EOF, expected }
EOF means "end of file" — the compiler ran out of text while still waiting for something, here a } that never showed up. Line 7 is one line past the end of the file, which is where the compiler gave up looking.
In every case: find the line number, look at that line, and read what the compiler says it expected versus what it found.
Common mistakes
- Writing
hello.exeor./hello.exeout of Windows habit — inside WSL, Linux, and macOS, executables have no extension. - Forgetting
./before a locally built executable's name —helloalone won't run; your shell needs./hello. - Treating an unused import as something to ignore. Go won't compile until it's gone.
- Reading only the error's last line and missing the file:line:column it starts with — that prefix is the fastest way to find the actual problem.
- Running
go buildwithout-o nameand being surprised the output file is named after the module instead.
Try it
- Install Go using the steps above, then confirm it with
go version. - Make a folder,
cdinto it, and rungo mod init example.com/hello. Runcat go.modand read both lines. - Create
main.gowithpackage main,import "fmt", and amainfunction that printsWelcome aboard!. - Run it with
go run main.go, then again withgo run .— confirm they print the same thing. - Build it with
go build -o hello ., then run the result with./hello— confirm the output matchesgo runexactly. - Break it on purpose: write
Printlnin lower case asprintln, rungo build ., and read the error's file, line, and column before fixing it. - Break it a different way: add
import "os"without using it anywhere, rungo build ., and read that error too. - Rewrite your imports in the parenthesised form with
fmtandmath, printmath.Pi, and add a//comment above the line. - Run
go test ./...in a folder with no tests and read what it says. Then mess up the indentation of one line inmain.go, rungo fmt ./..., and open the file to see it fixed.