Hello, Go
Five minutes from "never wrote Go" to "wrote your first Go". The toolchain is one binary; the language compiles in seconds; the formatter has no options to argue about. The whole point of Go is the friction is gone — install, write, run, ship.
1 · The intuition
Go was designed at Google in 2007 by Robert Griesemer, Rob Pike, and Ken Thompson. Their complaint: large C++ programs took 45 minutes to compile, and the resulting binaries needed a dozen build flags to behave. Go's answer: compile every Go program ever written in seconds; only one way to format; one binary builds, tests, and ships.
That ethos shows up everywhere. There's no separate package manager — go mod
is built in. There's no formatter argument — gofmt has no options.
There's no test runner — go test is the runner. You'll use five commands
every day for the next year.
go run (build & execute),
go build (just build), go fmt (format), go vet
(static check), go test (run tests). Memorise these; the rest you'll
look up when you need them.2 · Try it — your first Go program
Open a terminal. Make a directory. Put this in a file called main.go.
// Every Go file declares which package it's part of.
// "main" is special — it's a program, not a library.
package main
// Imports go below the package line. fmt is the formatting + IO package.
import "fmt"
// "main" is the function the program runs. No arguments, no return.
func main() {
fmt.Println("Hello, Go!")
}Two things happened in that one command. Go compiled main.go to a
temporary binary, then ran the binary. The whole process takes about 200 ms
on modern hardware. The compile time is so fast Go treats it like an interpreted
language — but the output is native code.
3 · Build it up — three variations
Variation 1 — pass an argument
package main
import (
"fmt"
"os"
)
func main() {
// os.Args is a []string. Element 0 is the program name; 1+ are the args.
if len(os.Args) < 2 {
fmt.Println("Usage: go run main.go <name>")
return
}
fmt.Printf("Hello, %s!
", os.Args[1])
}Variation 2 — read stdin
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
fmt.Print("Name? ")
scanner := bufio.NewScanner(os.Stdin)
if scanner.Scan() {
fmt.Printf("Hello, %s!
", scanner.Text())
}
}Variation 3 — multiple files in one package
A real Go program rarely lives in one file. Make a second file
greet.go in the same directory:
package main
// Capitalised names are exported from the package.
// Lower-case names are package-private. This one is package-private.
func greet(name string) string {
return "Hello, " + name + "!"
}And change main.go:
package main
import "fmt"
func main() {
fmt.Println(greet("Go"))
}Run go run . (the dot means "all files in the current directory").
Same output. The package gathered both files automatically.
4 · The toolchain — what each command does
| Command | What it does | When to use it |
|---|---|---|
go run . | Compile + run, throw away the binary | Iterating during development |
go build . | Compile, leave a binary in the current dir | Shipping the program |
go fmt ./... | Reformat all .go files under the current dir | Before every commit |
go vet ./... | Static analysis — printf format mismatches, unreachable code, etc. | Before every commit (CI usually runs it) |
go test ./... | Find & run all tests under the current dir | Whenever you want to know if you broke something |
go mod init <name> | Create a new module (project) | Once, when starting a project |
go mod tidy | Add missing dependencies, remove unused ones | After changing imports |
go doc <pkg> | Show package documentation in the terminal | When you don't want to alt-tab to the browser |
5 · Common mistakes
- Forgetting
package main. Every Go file declares its package. The one that containsfunc main()must be in packagemain. Without it,go runerrors with "no main function". - Unused imports. Go refuses to compile if you import a package you don't use. This is a feature — keeps imports tidy — but newcomers hit it constantly. Editor with the Go extension auto-fixes on save.
- Unused variables. Same rule — declare a variable and don't use it? Compile error. Use
_(the blank identifier) to throw away values you don't care about:_, err := os.Open("foo"). - Capitalisation matters. A name starting with a capital letter is exported (visible outside the package). A lowercase name is private. The compiler enforces it; there is no
publicorprivatekeyword. - Missing braces. Go requires braces around every
if/forbody — no exceptions.if x { doIt() }is fine;if x doIt()is a parse error.
6 · From the wild — real Go
A snippet from a project you've heard of: etcd, the distributed key-value store powering Kubernetes. This is the actual entrypoint of the etcd binary, simplified. Notice — it's the same shape you just wrote.
package main
import (
"fmt"
"os"
"go.etcd.io/etcd/server/v3/etcdmain"
)
func main() {
etcdmain.Main(os.Args)
fmt.Println("etcd shutdown")
}Four imports, one main(), one library call, one print. Nothing
fancy. Production Go is mostly this shape, expanded — the boilerplate stays
small even at 100k-LOC scale.
7 · Coming from another language?
| If you know… | What's the same | What's different |
|---|---|---|
| Python | Simple, readable. fmt.Println ≈ print(). Both have batteries-included stdlibs. | Go is compiled — no __pycache__, no interpreter. Types are static; the compiler shouts before the program runs. |
| Java | Static typing. Packages. A clear main() entry point. A standard build tool. | No classes. No public static void main(String[]). The standard build tool (go) is one binary, not Maven + Gradle + Ant. |
| JavaScript / TypeScript | The language feels modern. := is like const. First-class functions. | Compiled and statically typed. No npm install — go mod tidy handles deps. No event loop — concurrency is goroutines. |
| C / C++ | Pointers exist. The runtime is small. The output is a single binary. | Garbage collected. No header files. No manual memory management for most code. No template metaprogramming. |
| Rust | Static typing. Single binary. Strong standard library. | No borrow checker — GC handles memory. Way faster to write small programs. Slower to run hot loops (~2× Rust on average). |
8 · Exercises (~10 min)
- Echo three times. Modify the program to print
"Hello, Go!"three times. Try aforloop — Go's only loop keyword. - Greet the OS. Use
runtime.GOOS(import theruntimepackage) to print"Hello from linux/darwin/windows". Hint: Println accepts multiple args. - The 30-second build. Run
go build .in your directory. What appears? Run that file directly (e.g../hello). Confirm: the binary doesn't need Go installed on the machine to execute. - Break it on purpose. Remove the
import "fmt"line. Run again. Read the compiler error. Re-import. Now remove thefmt.prefix fromPrintln. Read that error. The error messages are part of the experience.
9 · When it clicks
You can answer these questions without looking anything up:
- What does
go run .do thatgo build .doesn't? - If you have
main.goandutil.goin the same directory, do they share variables? - Why is the function called
Printlnand notprintln? - What happens if you import
fmtbut never call any of its functions?
If those are all easy, you've got Day 1 of Go. Move on.