Your First Program: Compile It, Run It, See What Happened
Last lesson you ran a program twice, with go run and then as a compiled binary, and you never actually read it. That was deliberate: the file was cargo for the toolchain tour. Today it's the whole subject. We're going to read hello.go the way the compiler reads it, every word, top to bottom, and then we're going to look at what the compiler did with it and what that 2 MB file sitting in your directory really is.
There's a frame to install before we start, and it's worth holding onto for the rest of the course: nothing in a Go program is magic. Five lines, and each one is load-bearing. There's no hidden ceremony, no boilerplate that exists "because the framework needs it." When Go code looks dense later in the course, the move is always the same one we're about to practice: read it word by word, and every word will earn its place.
Here's the file again:
package main
import "fmt"
func main() {
fmt.Println("hello, go")
}Five lines of code. Let's go word by word.
Line one: package main
Every Go source file starts by declaring which package it belongs to. A package is the unit Go organizes code in: a directory of .go files that all open with the same package declaration and behave as one body of code. When you imported fmt last lesson, you were importing a package. When feedstack grows sub-folders in module six, each folder will be a package. It is the only code-organization concept Go has, and you've now seen all of it.
So why main? Because that one package name is special. Most packages are libraries: code that exists to be imported by other code. The package named main means something different to the toolchain: this compiles to an executable program. It's the difference between a book and a person who reads books aloud. Libraries get used; main gets run.
You can watch the toolchain enforce this. Change the first line to package hello and try to run it:
go run .
# package hello is not a main packageThe compiler isn't being pedantic. It genuinely cannot do what you asked: there's no program here to run, only a library nobody imports. And if you try go build instead, it succeeds silently and produces nothing, because building a library has nothing to hand you. Change it back to package main and the executable comes back.
Line two: import "fmt"
The import declaration pulls another package into scope so this file can use it. fmt (officially pronounced "fumpt" by the Go team, though everyone understands "format") is the formatting and printing package, and it comes from Go's standard library: roughly 150 packages that installed with the toolchain when mise pulled Go 1.27 onto your machine. File handling, HTTP servers, JSON, cryptography, testing: all in the box, all imported exactly like this. This course goes remarkably far before touching anything outside the standard library.
Now for a famous bit of Go strictness. Add a second import the file doesn't use:
import (
"fmt"
"os"
)go run .
# ./hello.go:5:2: "os" imported and not usedThat's not a warning. The program will not compile until the unused import is gone. Most languages let dead imports pile up for years; Go refuses to start. The first time it bites you mid-edit it feels strict, but think about it from the reader's side, the side lesson one said Go always takes: every import statement in every Go file you will ever read is real. The import list at the top of a file is an honest, machine-enforced summary of what the file actually depends on. (Your editor's gopls setup from last lesson adds and removes imports on save, so in practice you'll rarely manage this list by hand.)
Line three: func main()
func declares a function, and a function named main inside package main is the entry point: the place execution begins. The contract is rigid and worth saying precisely. main takes no parameters and returns nothing, and the compiler holds you to that signature. There's exactly one per program. When you ran ./hello, the operating system loaded the binary, Go's runtime set itself up, and then the runtime called your main. When main returns, the program exits.
Exits how, exactly? With status code 0, the Unix convention for "everything went fine." Check it:
./hello
# hello, go
echo $?
# 0$? holds the exit code of the last command. Falling off the end of main means success. Later, when feedstack needs to signal failure to a shell script, you'll meet os.Exit(1), but the default story is this simple: main returns, program over, exit 0.
And those braces, { and }, mark the function body. Everything between them runs when main is called. Here, that's one line.
Line four: fmt.Println("hello, go")
Read the qualified name first: fmt.Println means "the Println function from the fmt package." Anything you use from an imported package is spelled this way, package.Thing, so when you read unfamiliar Go you always know where a name came from. No mystery functions floating in from wildcard imports; the origin is in the spelling.
Now look closely at the capital P, because it's doing real work. In Go, capitalization is visibility. A name that starts with an uppercase letter is exported: visible to code outside its package. A lowercase name is private to the package that declared it. That's the entire access-control system. There is no public, no private, no protected keyword; the case of the first letter is the rule, enforced by the compiler. Println has a capital P precisely because you, standing outside the fmt package, are allowed to call it. Try fmt.println and the compiler tells you the name doesn't exist (and current compilers will even hint about the exported one). One more decision made for the reader: you can tell a package's public surface from its internals at a glance, in any codebase, forever.
Println itself does what it says: print the arguments, then a newline. But here's a detail worth filing away. Println returns two values, the number of bytes it wrote and an error, and our program silently ignores both:
n, err := fmt.Println("hello, go")
// n is 10: nine characters plus the newline
// err is nil unless stdout itself failedEven printing to the screen can fail (imagine stdout piped to a program that died), and Go's answer is the one from lesson one: the failure is a value, returned to you, visible on the page. Nobody checks Println's error in practice, and Go lets you ignore return values like this. But notice the shape: two return values, the result and the error, side by side. That shape is everywhere in Go, and module three is largely about it.
The semicolons you never see
Time for the lesson's best secret. Look at the program again: not a semicolon in sight. Now try something. Move the opening brace onto its own line, the way C or Java programmers often format:
func main()
{
fmt.Println("hello, go")
}go run .
# ./hello.go:6:1: syntax error: unexpected semicolon or newline before {Unexpected semicolon? You didn't write a semicolon. But Go did. The grammar of Go actually requires semicolons to terminate statements, just like C. You don't see them because the lexer, the very first stage of the compiler, inserts them automatically: whenever a line ends in something that could plausibly end a statement (an identifier, a literal, a closing parenthesis or brace), the lexer quietly appends a semicolon before the compiler proper ever looks at the code.
Walk the broken version with that rule in mind. The line func main() ends in ), which can end a statement, so the lexer inserts a semicolon there: func main();. By the time the parser arrives, the declaration has already ended, so the { on the next line makes no sense to it. One invisible semicolon, and the program no longer parses.
This is why the opening brace must sit on the same line in Go, and it's the mechanism behind something from last lesson. When go fmt snapped your mangled file back into the one true style, brace placement included, that wasn't only aesthetics. The one true brace style is the only brace style; the alternative doesn't parse. What looked like a formatting opinion is grammar, settled at the lexer, which is part of why gofmt can afford to have no options at all.
What the compiler actually did
You've now read every word of the source. So what happened when you typed go build?
The compiler read hello.go and took it through the classic stages: break the text into tokens (that's the lexer, semicolons and all), parse the tokens into a tree, check every type, then generate machine code, the raw instructions your CPU executes, and link them into an executable. The thing to register is what's not in that pipeline. There's no bytecode. There's no virtual machine. There's no interpreter anywhere in the story.
One line each on the alternatives, so the contrast is concrete. When you run a Python program, the Python interpreter reads your source and executes it, every run, which is why Python must be installed wherever the program goes. When you run Java, the compiler has produced bytecode, an intermediate language that the Java Virtual Machine translates to machine code while the program runs, which is why the JVM must be installed wherever the program goes. When you ran ./hello, nothing stood between the file and the CPU. The instructions in that file are the instructions the processor executed.
This also collapses go run and go build into one idea, tying off last lesson's two verbs: they perform the same compilation. go build drops the executable in your directory; go run drops it in a temporary directory, executes it, and cleans up. The iterate verb and the ship verb differ only in where the binary lands.
What that binary really is
Which brings us to the file itself. Look at it:
ls -lh hello
# -rwxr-xr-x 1 karn staff 2.3M ... hello
file hello
# hello: Mach-O 64-bit executable arm642.3 megabytes. For five lines of code. Where did it all come from?
The honest answer is the most interesting fact in this lesson: your five lines are a rounding error in that file. The bulk of it is the Go runtime, compiled in alongside your code. The garbage collector that will manage memory so you never free a byte by hand. The goroutine scheduler that module five is built on, present and idle even in a program that never starts a goroutine. The machinery that set everything up before calling your main. All of it, statically linked into this one file.
That's the trade, stated plainly: every Go binary carries a couple of megabytes of runtime, and in exchange the binary is complete. It does not need Go installed on the machine that runs it. It does not need an interpreter, a VM, or a node_modules directory shipped alongside. Lesson one told you Docker and Kubernetes ship as Go programs you can drop onto a server and run; this is the property they were buying, and you just built a file with the same property using one command.
The party trick: cross-compilation
One more experiment, and it's the one that makes the "binary you can hold" idea visceral. You're (probably) on a Mac. Your servers are (almost certainly) Linux. Watch:
GOOS=linux GOARCH=amd64 go build -o hello-linux .
file hello-linux
# hello-linux: ELF 64-bit LSB executable, x86-64, statically linkedTwo environment variables: GOOS is the target operating system, GOARCH the target CPU architecture. With those set, your Mac just produced a Linux server binary. No Linux machine involved, no Docker, no extra toolchain downloaded, no flags beyond the two variables. file confirms it: an ELF executable (Linux's format) for x86-64, statically linked, ready to copy to any Linux box and run. Cross-compiling to every OS and architecture pair Go supports works the same way, out of the same toolchain you installed with one mise command.
This is how a single laptop ships to any cloud machine, and it's the end of the chain this lesson walked: readable source, a transparent compile, and a self-contained artifact you can put anywhere.
Module one, done
Take stock of where you are. You can read every word of a Go program and say why it's there: the package declaration and why main is special, the import list the compiler keeps honest, the entry-point contract, qualified names and the capitalization rule, even the semicolons the lexer hides from you. And you know exactly what the toolchain does with those words: straight to machine code, runtime included, runnable anywhere, for any platform you name.
That's module one. The module quiz is next, and then the course changes gear: module two is the building blocks, starting with values, variables, constants, and Go's quietly great idea about what a variable holds before you assign anything, the zero value. It's also where feedstack stops being a promise: the first data your aggregator will ever hold gets declared there.