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.