Skip to content
gohacks.dev
Basics

Your First Go Program and What Every Line of It Does

Write a Go hello world, run it with go run, build it into a binary, and learn why package main, the import and func main each have to be there.

6 min read
Summarize in AI
Flat illustration of an editor window full of code.

Hello world in Go is five lines long, and each of them is there because the compiler will not accept the program without it. That makes it a better first lesson than it looks. If you just installed Go, you can have this running in a minute, and what you learn from taking it apart carries straight into everything else in the basics path.

Tested with Go 1.25.6, macOS on Apple Silicon (arm64)

The program

Make a folder, initialize a module in it, and create main.go:

Shell
mkdir hello && cd hello
go mod init example.test/hello
main.goGo
package main

import "fmt"

func main() {
	fmt.Println("Hello, Go")
}
Text
Hello, Go

That is the whole program, and the output above came from running it. The module name example.test/hello is a placeholder. For code you plan to publish it would be a repository path, such as github.com/you/hello, but for learning anything works. What go mod init does, and why a module is needed at all, is the subject of how GOPATH gave way to Go modules.

Reading it line by line

package main names the package. Every Go file starts with one. The special name main tells the toolchain this package builds into an executable. Any other name makes a library that other packages import. A folder holds exactly one package, so you cannot put package main and package hello side by side in the same directory. I will show the exact error for that further down.

import "fmt" brings in the standard library's formatting package. Import paths are strings and the last element is the name you use in the file, so fmt.Println means "the Println function from the package I imported as fmt". If you import several packages, gofmt groups and sorts them for you:

Go
import (
	"fmt"
	"os"
)

func main() is the entry point. It takes no arguments and returns nothing. If the program needs command line arguments you read them from os.Args, and if it needs to exit with a status you call os.Exit. The runtime calls main after all package-level variables are initialized and every init function has run, and the program ends when main returns, even if other goroutines are still running.

fmt.Println("Hello, Go") calls an exported function. In Go, a name that starts with a capital letter is visible outside its package and a lowercase name is not. That convention is the entire visibility system. There is no public keyword. It matters here because fmt.println would not compile, and the error message will tell you the function is not exported.

The tab indentation is not a preference. It is what gofmt produces, and every Go file you read will look the same.

go run versus go build

go run . compiles the package in the current directory, puts the binary in a temporary location and runs it. It is what you use while you are experimenting. go build compiles and leaves the result in the current directory, named after the module's last path element:

Shell
go build -o hello .
./hello
Text
Hello, Go

On my machine that binary was 2,387,874 bytes, about 2.3 MB. That is a statically linked executable containing your code, the parts of the standard library it uses and the Go runtime, which includes the garbage collector and the scheduler. It does not depend on a Go installation, so you can copy it to another machine of the same operating system and architecture and run it. Cross compiling to a different target is a matter of setting GOOS and GOARCH before go build.

Two flags shrink it. -ldflags='-s -w' strips the symbol table and the DWARF debug information:

Shell
go build -ldflags='-s -w' -o hello-small .

That took the binary to 1,606,402 bytes, a 33 percent cut. The price is that stack traces still work, but a debugger and some profiling tools get less to work with, so I keep the full binary for development and strip only the release build.

The compiler is strict on purpose

The three errors below are ones you will hit in your first hour, so it is worth seeing them once. Each is real output from the toolchain.

An import you do not use stops the build. Add import "os" to the program above without calling anything from it:

main.goGo
package main

import (
	"fmt"
	"os"
)

func main() {
	fmt.Println("Hello, Go")
}
Text
# example.test/lab
./main.go:5:2: "os" imported and not used

Go treats an unused import as an error, not a warning, because dead imports used to be a major source of slow builds and misleading dependency graphs. Editors with Go support remove the line for you on save. The related error for unused local variables is covered in declared and not used.

A missing main function stops the link step. Rename main to Main and the compiler is fine with the file but the build fails:

main.goGo
package main

import "fmt"

func Main() {
	fmt.Println("hi")
}
Text
# example.test/lab
runtime.main_main·f: function main is undeclared in the main package

The message, function main is undeclared in the main package, is the one to remember. It does not say your capital M is wrong, so the usual cause is a typo like this one.

Two packages in one folder is the third:

Shell
go run .
Text
found packages hello (bad.go) and main (main.go) in /tmp/hh

I created a second file with package hello next to main.go to get that. The fix is to put the library code in its own subdirectory, which is how larger programs are laid out anyway.

Format and check as you go

Two tools ship with Go and you should run them out of habit. gofmt -l . lists files whose formatting differs from the canonical style, and gofmt -w . rewrites them. go vet ./... finds suspicious constructs that compile fine, such as a Printf call whose format string does not match its arguments. Your editor probably runs both on save already. If it does not, set that up before you write more code. The reasoning behind a single enforced style is in why gofmt settled the formatting argument.

Running a file instead of a package

You will see go run main.go in older tutorials. It still works, and it is handy for a single-file script, but it only compiles the files you name. The moment your program grows a second file, go run main.go stops seeing it, and the build fails with errors about undefined names. go run . compiles the whole package and is the habit to build.

Where this program stops

Hello world assumes a module-aware Go and a terminal. If you are in a restricted environment where you cannot install anything, the Go Playground at go.dev/play runs the same file, though it does not offer a filesystem or network access. And if your first goal is a web service rather than a command line program, the shape of the program is the same, and the basics path builds toward it one piece at a time. For the language reference behind each of these lines, the Go specification describes program initialization and execution, and the official tour of Go is a good companion.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.