Go Get Started (Hello World)

Every Go programmer’s journey starts with the same three lines of code: a program that prints Hello, World! to the screen. It looks trivial, but getting there touches almost every foundational idea in the language — packages, the compiler, the module system, and the one function every executable must define. This lesson walks through installing Go, creating your first module, and understanding exactly what happens when you type go run, so "Hello World" becomes a real foundation instead of a magic incantation.

Overview: How a Go Program Is Built and Run

Go (sometimes called Golang) is a statically typed, compiled language created at Google and released publicly in 2009. Unlike Python or JavaScript, which are interpreted or JIT-compiled at run time, Go source code is compiled ahead of time into a native machine-code binary. That binary is self-contained and statically linked — it bundles the Go runtime (including the garbage collector and goroutine scheduler) so it can run on a target machine without Go itself being installed there. This is one reason Go is popular for command-line tools and servers: you build once and ship a single executable file.

The go command is the single entry point to the whole toolchain. It wraps the compiler, the code formatter (gofmt), the dependency manager, the test runner, and more, all under one CLI. You will use it constantly: go run, go build, go test, go mod, go fmt.

Packages

Every Go source file begins with a package clause, and every file belongs to exactly one package. A package is Go’s unit of compilation and code organization — related files that live in the same directory and share the same package name. The package named main is special: when the compiler builds an executable, it looks inside package main for a function called main with no parameters and no return value. That function is the program’s entry point. Any package that is not named main is a library package: it can be imported by other code, but it cannot be run directly.

Modules

Since Go 1.11, Go projects are organized into modules rather than requiring your code to live inside a global GOPATH directory (the old, now-obsolete workflow). A module is simply a directory tree of Go source files with a go.mod file at its root. The go.mod file records the module’s import path, the minimum Go language version it needs, and the versions of any external dependencies it uses (a companion go.sum file records cryptographic checksums of those dependencies). Because modules are self-describing, a Go project can live anywhere on disk — your home folder, a repository clone, wherever — and still build reproducibly.

What happens when you build

When you run go run main.go, the toolchain compiles your files (and everything they import) into a temporary binary, executes it, streams its output to your terminal, and then discards the binary. This is the fast, convenient way to iterate while developing. go build instead compiles a binary and leaves it on disk in the current directory (or wherever you tell it with -o), ready to be run repeatedly or shipped to another machine. Either way, before main.main() ever runs, Go first initializes every package the program imports (running any top-level variable initializers and init functions), then finally calls your main function.

Setting Up: Installing Go and Creating a Module

Download and install Go from the official site for your operating system, then confirm the install from a terminal:

$ go version
go version go1.22.0 linux/amd64

Next, create a project directory and turn it into a module. The argument to go mod init is the module’s import path — for a real project this is usually a repository URL (like github.com/you/project), but for local practice any short name works:

$ mkdir hello && cd hello
$ go mod init example.com/hello

That command creates a go.mod file that looks like this:

module example.com/hello

go 1.22

With the module in place, create a file named main.go in the same directory and you are ready to write your first program. Go source files are always formatted with tabs for indentation and opening braces on the same line as the statement that introduces them (func main() {, never a brace on its own line); running gofmt or go fmt ./... enforces this automatically, so you rarely have to think about style by hand.

Syntax

Every runnable Go file follows the same general shape:

package main

import (
	"fmt"
	// additional imports as needed
)

func main() {
	// program logic goes here
	fmt.Println("some output")
}
Part Purpose
package main Declares this file belongs to the special executable package; required for anything you intend to run directly.
import (...) Lists packages this file uses. Each import must actually be referenced in the code, or the compiler rejects the file.
func main() The program’s entry point. Package main must define exactly one, with no parameters or return values.
fmt.Println(...) A call into the standard library’s fmt package to write text, followed by a newline, to standard output.

Examples

Example 1: The classic Hello World

package main

import "fmt"

func main() {
	fmt.Println("Hello, World!")
}

Output:

Hello, World!

The fmt package is imported because main calls fmt.Println. Println writes its arguments to standard output followed by a newline. When the compiler builds this file, it finds package main and a valid func main, so it produces a runnable binary; running that binary executes the one statement inside main and the program exits.

Example 2: Variables and formatted output

package main

import "fmt"

func main() {
	name := "Gopher"
	favoriteNumber := 7

	fmt.Println("Hello, World!")
	fmt.Printf("Welcome to Go, %s! Your favorite number is %d.\n", name, favoriteNumber)
}

Output:

Hello, World!
Welcome to Go, Gopher! Your favorite number is 7.

This example introduces :=, Go’s short variable declaration, which declares a new variable and infers its type from the value on the right (name becomes a string, favoriteNumber becomes an int). It also introduces fmt.Printf, which works like Println but substitutes format verbs in a template string: %s for a string and %d for an integer. Unlike Println, Printf does not add a trailing newline automatically, which is why the format string ends with \n.

Example 3: A more realistic variation

package main

import "fmt"

func main() {
	names := []string{"Ada", "Grace", "Alan"}

	for _, name := range names {
		fmt.Printf("Hello, %s!\n", name)
	}
}

Output:

Hello, Ada!
Hello, Grace!
Hello, Alan!

Real programs rarely print exactly one fixed string; here names is a slice of strings, and for _, name := range names loops over every element. The blank identifier _ discards the index that range would otherwise supply, since only the value is needed. Each iteration formats and prints a personalized greeting, showing how the same fmt functions from Example 1 and 2 scale up to real output.

How It Works Step by Step

Walking through go run main.go for Example 1:

$ go run main.go
Hello, World!

$ go build -o hello
$ ./hello
Hello, World!
  1. The go tool locates go.mod to determine the module root and Go version to target.
  2. It parses main.go, checking syntax, verifying every import is actually used, and verifying every declared variable is actually used.
  3. The compiler type-checks the file and translates it to machine code for your platform, linking in the small part of the fmt and runtime packages it actually needs.
  4. Package-level initialization runs first (none here), then main.main() is invoked.
  5. fmt.Println writes bytes to the process’s standard output stream, which your terminal displays.
  6. When main returns, the process exits with status code 0 (success). go run then deletes the temporary binary it built; go build instead leaves hello on disk so you can run it again directly with ./hello.

Common Mistakes

1. Forgetting to define func main

A file in package main that never declares main compiles as far as type-checking but fails at the link stage, because the toolchain has nothing to use as the program’s entry point:

package main

import "fmt"

func sayHello() {
	fmt.Println("Hello, World!")
}

Error:

./main.go: function main is undeclared in the main package

The fix is to actually call your logic from a main function:

package main

import "fmt"

func sayHello() {
	fmt.Println("Hello, World!")
}

func main() {
	sayHello()
}

Output:

Hello, World!

2. Importing a package you never use

Go treats an unused import as a compile error, not a warning — this keeps dependency lists honest and prevents dead weight from silently accumulating:

package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println("Hello, World!")
}

Error:

./main.go:5:2: "strings" imported and not used

Either remove the import, or actually use the package:

package main

import (
	"fmt"
	"strings"
)

func main() {
	message := "hello, world!"
	fmt.Println(strings.ToUpper(message))
}

Output:

HELLO, WORLD!

3. Declaring a variable you never use

The same strictness applies to local variables. This catches typos and leftover debugging code, but it surprises newcomers from languages where unused locals are merely a linter warning:

package main

import "fmt"

func main() {
	message := "Hello, World!"
	fmt.Println("Hello, World!")
}

Error:

./main.go:6:2: declared and not used: message

Use the variable you declared, or remove the declaration entirely:

package main

import "fmt"

func main() {
	message := "Hello, World!"
	fmt.Println(message)
}

Output:

Hello, World!

Best Practices

  • Start every project with go mod init <module-path> so dependencies and the Go version are pinned from day one, rather than adding modules as an afterthought.
  • Run gofmt (or let your editor run it on save) instead of hand-formatting; Go’s tooling and community both assume canonical formatting, and diffs stay minimal when everyone uses it.
  • Use go run while iterating locally and go build when you need a distributable binary — don’t build and delete a binary manually when go run already does that for you.
  • Treat "unused import" and "declared and not used" errors as the compiler doing you a favor, not an obstacle — they usually point at a real mistake, such as a half-finished refactor.
  • Keep package main files thin: put real logic in importable packages and let main mostly wire things together and call into them, which keeps code testable.
  • Commit both go.mod and go.sum to version control so builds are reproducible for every contributor.

Practice Exercises

  • Create a new module named example.com/greeter, add a main.go, and write a program that prints your name and a favorite hobby on two separate lines using fmt.Println.
  • Modify Example 2 so it also prints your age using fmt.Printf and the %d verb, in a single formatted sentence.
  • Take Example 3’s slice of names and change it to a slice of three numbers; loop over it with range and print each number’s square using %d. (Hint: for input [2, 3, 4] the expected output is three lines reading 4, 9, and 16.)

Summary

  • Go source compiles ahead of time into a native, self-contained binary via the go toolchain.
  • package main plus a parameterless func main() defines a runnable program; the compiler needs both to produce an executable.
  • Modern Go projects are organized as modules, declared with a go.mod file created via go mod init, replacing the older GOPATH workflow.
  • go run compiles and executes in one step for fast iteration; go build produces a persistent binary you can run or ship.
  • Go’s compiler rejects unused imports and unused local variables as errors, not warnings — this is intentional and catches real mistakes early.
  • fmt.Println and fmt.Printf are the everyday tools for producing output, with Printf supporting format verbs like %s and %d.