Your first Go program, and the four pieces you'll see in every .go file you ever read.
By the end you'll know why Go compiles instead of interprets, and you'll have run real code three different ways.
Every programming book starts here. Yours included. Save this as hello.go:
package main
import "fmt"
func main() {
fmt.Println("Hello, Go!")
}
Then in your terminal:
$ go run hello.go
Hello, Go!
▶ Open Go Playground (hover any code block above and click Copy, then paste)
package main = "Hey robot, this is a complete instruction sheet, not a recipe card stuck inside another book."import "fmt" = "I'm going to need the talking tool from the toolbox."func main() = "Start here. This is where you begin."fmt.Println("Hello, Go!") = "Use the talking tool to say the words inside the quotes."
Look at that 5-line program again. Three pieces appear in every Go file you'll ever see:
| Piece | What it does | Python analog |
|---|---|---|
package main |
Declares the file's package. main means "this is an executable." Library code uses other names. |
The if __name__ == "__main__": dance, but mandatory and at the top. |
import "fmt" |
Pulls in the formatting/printing package from the standard library. | import sys or from os import path. |
func main() |
The entry point. Go starts here. Returns nothing, takes no arguments. | The body inside if __name__ == "__main__":. |
fmt.Println(...) |
Calls Println from the fmt package. Prints + newline. |
print("..."). |
{ } braces? Go uses C-style braces instead of Python's indentation. The compiler does not care about whitespace. (But gofmt, the auto-formatter, does. Always run it.)
The same program in both languages. Read both, then read them again:
def main():
print("Hello, Go!")
if __name__ == "__main__":
main()
package main
import "fmt"
func main() {
fmt.Println("Hello, Go!")
}
| Difference | Why Go is like this |
|---|---|
Go requires package + import declarations always. | Go has no "script mode." Every file belongs to a package. This makes large codebases unambiguous - you always know what scope something lives in. |
Braces { } instead of indentation. | The compiler parses braces faster and more reliably than significant whitespace. Indentation is enforced by gofmt anyway. |
No if __name__ == "__main__": guard. | Only files in package main have a main() function. Library packages can't accidentally execute. Cleaner separation. |
Println capitalized. | In Go, an uppercase first letter means "exported / public." Lowercase means "private to the package." It's the access-control system. (More in Lesson 8.) |
This is the biggest mental shift coming from Python. Python is interpreted. Go is compiled. Watch what that means:
| Python | Go | |
|---|---|---|
| Edit-run cycle | Edit, run. (Fast loop, but slow execution.) | Edit, compile, run. (Tiny compile, then fast execution.) |
| Errors caught | At runtime, when the buggy line executes. | At compile time, before the program ever runs. |
| Deploying | Ship code + Python + venv + deps. | Ship one binary. |
| Runtime errors | Type errors crash mid-run. | Most type errors can't compile, so they can't happen. |
GOOS=linux GOARCH=amd64 go build hello.go and you get a Linux binary. No Docker, no VM. This is why Kubernetes is written in Go - one codebase, binaries for every OS.
"One static binary" sounds like magic. It isn't - it's just that Go packs everything your program needs into that one file. Here's what's inside:
| Section | What it does | Python equivalent? |
|---|---|---|
| Your code | The machine instructions for main() and any functions you wrote. | Your .py files (but compiled to native, not bytecode). |
| Standard library | Compiled code of every stdlib package you imported. Unused packages are stripped. | The CPython implementation of fmt, os, etc. |
| Go runtime | The garbage collector, scheduler, memory allocator. Always included. | The CPython interpreter itself. |
| Data section | Strings, type metadata, reflection info. Read-only at runtime. | Embedded constants in CPython bytecode. |
You'll use all three. Pick the right tool for the moment:
go runWhen: hacking on a single file, learning, quick experiments.
$ go run hello.go
Hello, Go!
Compiles + runs in one step, throws the binary away. Closest to python hello.py.
go buildWhen: shipping to production, packaging a real tool.
$ go build hello.go
$ ls
hello hello.go
$ ./hello
Hello, Go!
Produces a real executable. Copy this file to any machine of the same OS/arch and run it.
When: sharing a snippet, no Go installed, trying things from your phone.
# Open in browser:
go.dev/play
# Paste, hit Run.
# Runs server-side.
Zero install. Great for examples and sharing - all the links in this course go here.
gofmt -w hello.go after editing. It reformats your file to the official Go style automatically. Most editors (VS Code with Go extension) do this on save. Stop wasting brain cycles on whitespace.
Hello, World is cute. But that exact 4-line skeleton (package main + import + func main) is the start of every Go program ever shipped. Three real ones, each under 15 lines:
A web server that responds to any request with "Hello, Go!" - the entire program:
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, Go!")
})
http.ListenAndServe(":8080", nil)
}
Run it, then visit http://localhost:8080. That's a production-grade HTTP server in 12 lines. No Flask, no uvicorn. Just Go's stdlib.
A tiny CLI that greets whoever you pass via --name:
package main
import (
"flag"
"fmt"
)
func main() {
name := flag.String("name", "world", "who to greet")
flag.Parse()
fmt.Printf("Hello, %s!\n", *name)
}
$ go build -o greet
$ ./greet --name=Sunil
Hello, Sunil!
No argparse, no click. The flag stdlib package handles it. Real Go CLIs (kubectl, terraform, docker) start exactly this way.
A skeleton for a Kubernetes pod-counter - the same shape, just more imports:
package main
import (
"context"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
func main() {
cfg, _ := clientcmd.BuildConfigFromFlags("", "~/.kube/config")
cs, _ := kubernetes.NewForConfig(cfg)
pods, _ := cs.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{})
fmt.Printf("Hello, %d pods!\n", len(pods.Items))
}
Same 4-piece skeleton. The whole Kubernetes ecosystem - kubectl, operators, controllers, even k8s itself - is built on this shape.
package main + import + func main(). That's the only entry-point shape Go has. You've already learned it.
If you write import "fmt" and don't use it, the program won't build. Coming from Python where unused imports are a lint warning at worst, this feels harsh. It keeps codebases clean. (VS Code's Go extension auto-removes them on save.)
Declare x := 5 and never use x? Build fails. Use _ (underscore) if you want to throw away a value: _, err := f().
{ must be on the same line.
This is valid: func main() {
This is not: func main() then { on its own line.
Go auto-inserts semicolons at end-of-line, so a brace on a new line becomes a syntax error.
fmt.Println, not print.
There is a builtin print() in Go, but never use it. It's for early debugging of the runtime itself, undocumented, and goes to stderr in weird ways. Always use fmt.Println.
snake_case.go, not CamelCase.go.
Convention: lowercase with underscores. http_server.go, not HTTPServer.go. Inside the file, types and functions are CamelCase - just not file names.
| Term | Meaning |
|---|---|
package main | Marks a file as part of an executable program (vs a library). Required for any file with a main() function. |
func main() | The entry point of an executable. Takes no args, returns nothing. Exactly one per program. |
import | Brings in another package by import path. Single: import "fmt". Grouped: import ( "fmt"; "os" ). |
fmt | The standard formatting/printing package. Has Println, Printf, Sprintf, etc. |
go run | Compile and run in one step, then discard the binary. For development. |
go build | Compile to a real executable. For shipping. |
gofmt | Auto-formatter. Run gofmt -w yourfile.go or let your editor do it on save. |
| Binary | The compiled executable file. Self-contained: no runtime needed on the target machine. |
| Go runtime | The scheduler + GC + memory allocator that ships inside every Go binary. |
| Compile-time error | An error caught by go build before the program runs. Fix and rebuild. |
| Standard library (stdlib) | Packages that ship with Go: fmt, os, net/http, encoding/json, etc. |
Backstory and pitfalls that make this chapter stick.
%v (default value), %d (decimal), %s (string), %+v (with field names).package main + func main(). Libraries use other package names.go run, go build, go test, go mod, go vet. Same name as the language.This program compiles. What happens when you run it?
package main
import "fmt"
func main() {
var s []string
fmt.Println(len(s))
s[0] = "hello"
fmt.Println(s[0])
}The first Println prints 0 - nil slices have length 0 and that's totally valid. The crash is on s[0] = "hello" - you can't index into a nil slice. Panic: runtime error: index out of range [0] with length 0.
Fix: s = append(s, "hello") works because append handles nil slices. Or initialize: s := make([]string, 1).
A cron task processing access logs at Cloudflare had this innocent-looking pattern:
var err error
err = (*MyError)(nil)
if err != nil {
handleError(err) // called with nil internals
}The comparison err != nil was true, because the error interface had a type assigned even though the underlying pointer was nil. handleError then called methods on the nil pointer, panicking the binary. The cron daemon died silently and logs piled up for 6 hours.
Lesson: Returning a typed-nil from a function whose return type is the error interface does NOT compare equal to nil. Always return raw nil for the error path, never a typed pointer cast to error. Tools like staticcheck SA4023 catch this.
Don't peek. Think first, then click to reveal each answer.
1. What's the difference between go run and go build?
go run compiles + executes; go build only compiles to a binarygo run is interpreted; go build is compiledgo run is for dev; go build is for testsgo run compiles to a temp directory and immediately executes - convenient for one-shot scripts. go build emits a binary you can ship. Both go through the same compiler.
2. Every Go executable needs which two things?
package main and func main()package main and func init()import and func main()package and func start()package main declares this is an entry-point package; func main() is what the runtime calls. Both are required. func init() is optional (runs before main) - some programs have it, most don't.
3. What does gofmt do?
gofmt rewrites the file to the Go team's canonical style (tabs for indent, brace placement, gofmt-aligned struct field tags). Run automatically by most editors. Stops style bikeshedding before it starts.
4. The fmt.Println function comes from which standard-library package?
formatfmtprintiofmt for "format". Imported via import "fmt". The package also has Printf, Sprintf, Errorf, Fprintln, and friends.
5. A Go source file that starts with package mypkg (not main) is...
go run, must be importedpackage main makes an executable. Other packages are libraries, imported by main or other packages. go run on a non-main file will fail with "no Go files in package".