Why Go, and a Compiling Project
Every phase of this project ends with a program you can run. This first one is short on features and long on foundation: a Go module, a main.go that can see its own command-line arguments, and a compiled binary sitting in your folder. Once you've watched go build turn source code into a single executable file, the rest of the project is filling that file with behavior.
Open a terminal. Let's make the project.
Check your Go
First, confirm the toolchain is there:
$ go version
go version go1.24.4 windows/amd64
What just happened: Go printed its version and your platform - the windows/amd64 part is your operating system and CPU architecture, a pair you'll meet again in phase 6 when you cross-compile for other platforms. Your version and platform will differ from mine; anything 1.22 or newer is fine. If the command isn't found, install Go from go.dev/dl and reopen your terminal.
Create the module
Make a folder and initialize a module in it:
$ mkdir til
$ cd til
$ go mod init til
go: creating new go.mod: module til
What just happened: go mod init til created a file called go.mod that declares "this folder is a Go module named til." The module name is how Go identifies your project - for a library you'd use a full path like github.com/you/til so others can import it, but for a personal tool a short name works. Open the file and look:
module til
go 1.24.4
That's the entire project configuration. When a project has dependencies, they're listed here too - ours never will, because everything we need ships with Go.
📝 Terminology: a module is Go's unit of versioning and dependency tracking (the folder with go.mod); a package is a folder of .go files that compile together. Our module contains exactly one package: main, the special package name that means "this compiles to an executable."
The first main.go
Create main.go next to go.mod:
package main
Two lines of real content, and one of them is the key to this whole project.
os.Args is the raw command line. It's a slice of strings holding everything the user typed, split on spaces. Index 0 is the program's own name; everything after that is what the user passed. Every CLI you've ever used - git, docker, npm - starts from exactly this slice. There's no magic underneath; subcommands and flags are all parsed out of these strings, and in the next phase you'll do that parsing yourself.
We print os.Args[1:] - everything except the program name - to see what arrives.
Run it
go run compiles and runs in one step, which is the fast loop you'll use while developing:
$ go run . add "my first note"
til - a tiny "today I learned" log
you passed: [add my first note]
What just happened: the . means "the package in the current folder." Go compiled main.go to a temporary binary, ran it, and passed along everything after the .. Notice what the slice looks like: add and my first note arrived as two elements, not four - your shell kept the quoted string together before Go ever saw it. That's why you quote multi-word arguments, and it's why os.Args is a []string and not one long string.
⚠️ Gotcha: run go run . add my first note without the quotes and you'll get [add my first note] printed the same way - but it's now four elements, and later phases would treat my as the note and lose the rest. The shell splits on spaces; quotes are how you tell it not to. If a note ever comes out truncated, this is the first thing to check.
Build the binary
go run is for development. The real artifact comes from:
$ go build
$ ls
go.mod main.go til.exe
What just happened: go build compiled the package into an executable named after the module - til.exe on Windows, plain til on macOS and Linux. No output on success is normal; Go is quiet when things work.
Run it directly:
$ ./til add "hello"
til - a tiny "today I learned" log
you passed: [add hello]
(On Windows PowerShell that's .\til add "hello" - the ./ or .\ prefix means "the one in this folder," since the folder isn't on your PATH. Yet. Phase 6 fixes that.)
That file is the entire program. It's a few megabytes because Go packs the runtime and every library the program uses inside it - and that's the trade Go makes deliberately: a bigger file, in exchange for no installation step ever. You could email til.exe to a colleague on Windows right now and it would run. No Go toolchain, no dependencies, nothing to set up. Compare that with shipping a Python script, and you understand why so much command-line tooling moved to Go.
💡 Key point: go run . while developing, go build when you want the artifact. Both compile the same way; the only difference is whether the binary sticks around.
What you have now
A Go module, a program that can read its command line, and proof that it compiles to a single self-contained binary. It doesn't do anything with add yet - os.Args is still raw, unparsed text. Next phase we give those strings meaning: a real subcommand dispatcher and the flag package, which is the skeleton every serious CLI hangs off.
Quick check before you move on:
[
{
"q": "What does go build produce for a CLI project like this one?",
"choices": [
"A folder of compiled files plus a runtime the target machine must install",
"One self-contained executable that runs without Go installed",
"Bytecode that needs the Go toolchain present to execute"
],
"answer": 1,
"explain": "Go statically compiles your code, its dependencies, and the runtime into a single binary. The machine running it never needs Go."
},
{
"q": "What is os.Args[0]?",
"choices": [
"The first argument the user typed after the program name",
"The name/path of the program itself",
"Always an empty string"
],
"answer": 1,
"explain": "Index 0 is the program's own name; user-supplied arguments start at index 1. That's why we print os.Args[1:]."
},
{
"q": "You run: go run . add my first note (no quotes). How does the note text arrive?",
"choices": [
"As one string, because Go rejoins the words",
"As three separate elements, because the shell split on spaces",
"It causes a compile error"
],
"answer": 1,
"explain": "The shell splits unquoted input on whitespace before the program starts. Quoting keeps multi-word arguments as a single element."
}
]
Check your understanding 3 questions
1. What does go build produce for a CLI project like this one?
2. What is os.Args[0]?
3. You run: go run . add my first note (no quotes). How does the note text arrive?