Understanding the main Package and main Function in Go

The main package and main function define the entry point for a standalone Go executable program.

The entry point and the executable boundary

You write a Go file. You type go run main.go. The terminal blinks, waits a fraction of a second, and returns to the prompt. No output. No crash. Just silence. You check the code. The logic looks fine. The problem isn't the algorithm. It's the package declaration. Or perhaps you tried to import your executable into another project and the compiler screamed. Go draws a hard line between code that runs and code that gets imported. That line is the main package.

Every standalone Go binary must have exactly one main package containing a main function. This is not a convention. The compiler enforces it. If the package name is missing or wrong, the toolchain refuses to produce an executable. If the function is missing, the build fails. Go treats executables and libraries as distinct categories from the very first line of source code.

Think of a Go binary like a stage play. The main package is the stage itself. The main function is the curtain rising. Everything elseβ€”the actors, the props, the scriptsβ€”are other packages. They can be reused in other plays, but the stage and the opening moment belong to this specific performance. You cannot import a stage into another play. You build a new stage. This separation keeps the dependency graph clean and makes it obvious which code produces binaries and which code provides functionality.

The minimal executable

Here's the skeleton of every Go executable. It declares the package, defines the entry point, and does nothing.

package main

func main() {
	// execution starts here when the binary runs
}

The first line is package main. This tells the compiler that the files in this directory belong to the executable root. The function main has no arguments and no return values. The runtime looks for this exact signature. If you change it, the build fails. The body of main is where your program logic begins. When main returns, the program terminates.

Trust gofmt. The formatting is automatic. Most editors run gofmt on save, so you never argue about indentation or brace placement. The tool decides. You focus on the structure.

From source to binary

When you run go build, the tool scans your directory. It checks the package declaration. If it sees package main, the compiler treats the files as candidates for an executable. It gathers all files in that directory with package main, resolves imports, compiles each package, and links them into a single binary. The binary contains your code, the Go runtime, and all dependencies.

If the package name is anything else, go build produces a library archive instead. The archive ends in .a and can be imported by other packages. You cannot run a library archive. You can only import it. This distinction prevents accidental execution of library code and keeps the build artifacts predictable.

The runtime doesn't care about the source code once the binary exists. It just finds the main function symbol and jumps to it. The OS loader executes the binary, the Go runtime initializes the scheduler and garbage collector, and then control transfers to main.main.

The compiler enforces the entry point. You provide the logic.

Startup sequence and init functions

Before main runs, Go executes init functions. Every package can define an init function. The runtime calls init after the package's imports are initialized. The order is strict: imports first, then init, then main.

package main

import "fmt"

// init runs before main to set up global state
func init() {
	// prints during startup, before main executes
	fmt.Println("Initializing...")
}

func main() {
	// prints after init completes
	fmt.Println("Running...")
}

The init function has no arguments and no return values. You can have multiple init functions in a package. They run in the order the compiler sees them, which is usually the order of files in the directory. Use init for setup that must happen before main starts, like registering drivers or loading configuration. Avoid complex logic in init. It makes the startup order harder to reason about.

init is a hook, not a substitute for main. Keep initialization simple and explicit.

Realistic entry point

Here's a realistic entry point: a web server that listens for requests. It registers a handler, starts the server, and blocks until the server stops.

package main

import (
	"fmt"
	"log"
	"net/http"
)

// handleRoot responds to requests at the root path
func handleRoot(w http.ResponseWriter, r *http.Request) {
	// writes a simple response to the client
	fmt.Fprint(w, "Server is running")
}

func main() {
	// registers the handler for the "/" path
	http.HandleFunc("/", handleRoot)

	// starts the server on port 8080
	// log.Fatal stops the program if the server fails to start
	log.Fatal(http.ListenAndServe(":8080", nil))
}

The main function sets up the server and calls ListenAndServe. That function blocks. It runs until the server shuts down. If the server fails to start, ListenAndServe returns an error. log.Fatal prints the error and calls os.Exit(1). The program terminates with a non-zero exit code.

The community accepts the boilerplate of error handling because it makes the unhappy path visible. You see every error at the call site. There are no hidden exceptions. If you want to handle the error differently, you write the check explicitly. Verbose is better than ambiguous.

Context is plumbing. Run it through every long-lived call site. In a real server, you'd pass a context.Context to handlers and background tasks so you can cancel work when the server shuts down. The context package is the standard way to carry deadlines and cancellation signals. Functions that take a context should respect it.

Pitfalls and compiler errors

You can't have two main functions in the same package. The compiler rejects this with main redeclared in this block. You also can't change the signature. func main() int triggers main must be declared as func main(). The runtime expects zero arguments and no return values. If you need to return an exit code, call os.Exit(code).

You also can't name a library package main. The compiler blocks it with package main: cannot use main as package name. This prevents confusion between executables and libraries. If you try to import a main package, you get import cycle not allowed or package main is not a library. The toolchain treats main as a terminal node in the dependency graph.

Public names start with a capital letter. Private names start lowercase. The main function is lowercase because it's private to the package, yet the runtime knows to call it by convention. There are no keywords like public or private. Capitalization is the rule.

The worst goroutine bug is the one that never logs. When main returns, the entire program exits. Every other goroutine is killed instantly. There is no graceful shutdown of background tasks unless you manage it yourself. If you launch a worker goroutine and main finishes, the worker vanishes. This catches many developers off guard. You must block in main until the work is done, usually with a channel or a wait group. If a goroutine waits on a channel that never gets closed, it leaks. Always have a cancellation path.

Decision matrix

Use package main when you are building a standalone executable that runs directly. Use a named package like package server when you are writing code intended to be imported by other projects. Use the cmd/ directory convention when your repository contains multiple binaries, placing each binary's main function in its own subdirectory. Use go run for quick testing of a main package, but rely on go build for production artifacts. Use os.Exit when you need to terminate with a specific exit code. Use log.Fatal when a startup failure should stop the program immediately.

Package names define boundaries. Respect them.

Where to go next