The main Package and main Function
Understand the special role of the main package and the main function as the entry point of any executable Go program.
Every executable Go program needs a place to start. Go makes that place explicit and impossible to miss: a function named main inside a package named main. Together, they form the only doorway through which the Go runtime can launch your program as a standalone binary.
The Program's Entry Point
When you run a compiled Go binary, the operating system hands control to a small piece of startup code inside the Go runtime. That runtime code initializes internal structures, sets up the garbage collector, and eventually needs to hand control to the code you actually wrote. The question is: how does the runtime know which function to call first?
Other languages solve this in different ways. C looks for a function called main. Java searches for a class with a public static void main(String[] args) method. Go’s answer is a reserved package name paired with a specific function signature. There is exactly one combination the compiler accepts as the entry point of an executable:
- The package must be named
main. - That package must contain a function named
mainthat takes no arguments and returns nothing.
If either of these conditions is missing, the compiler will not produce an executable — you will get a build error.
The main Package
Every Go source file starts with a package clause. In most cases, the name you give the package reflects what that code does: package http, package json, package models. You can import those packages from other code because the compiler treats them as library packages — they provide reusable functionality.
A package named main is fundamentally different. It signals to the Go toolchain that this code is not a library. It is the entry point of a program, and the compiler should produce a binary that the operating system can directly execute.
// main.go
package main
import "fmt"
func main() {
fmt.Println("This program is an executable")
}
package main tells the compiler two things:
- This package is unimportable — no other package can write
import "yourmodule/main"and use its exported identifiers. - The build command (
go build) should link this package into an executable file, not a reusable archive (.afile).
If you try to import a main package, the compiler stops with an error similar to:
import "example.com/myapp" is a program, not an importable package
Fatal: Importing main:
The Go specification explicitly forbids importing the main package. This is not a convention — it is a compiler-enforced rule. Any attempt to do so will prevent your program from compiling.
A project that does not contain a main package is perfectly valid as a library. It will compile into a package archive and can be used by other Go programs, but you cannot run it directly. Only the presence of a main package makes a project executable.
The main Function
Inside a main package, exactly one function must have the signature:
func main() {
// program logic here
}
There are no parameters — no args []string, no argc int. The main function takes nothing and returns nothing. If you need command-line arguments, you access them through the os.Args slice in the standard library, not through function parameters.
When main returns, the program exits. The exit code is 0 (success). The runtime does not wait for any other goroutines to finish — if your main function returns while other goroutines are still running, those goroutines are terminated immediately.
package main
import (
"fmt"
"time"
)
func main() {
go func() {
time.Sleep(2 * time.Second)
fmt.Println("This will never print")
}()
fmt.Println("Exiting immediately")
}
If you run this program, you will see only Exiting immediately. The spawned goroutine never gets its two seconds because main returns first, and the program terminates.
Program exit kills all goroutines:
The Go runtime does not implicitly wait for background work when main returns. If you need to keep the program alive, you must explicitly block (using a select{}, a sync.WaitGroup, or similar) until all necessary goroutines have completed their work.
Why such a rigid signature? It forces a clean separation between the program’s entry point and the rest of your code. The entry point is only responsible for bootstrapping the application — parsing configuration, setting up dependencies, starting the main loop — not for returning values to the operating system. If you need a non-zero exit code, you call os.Exit(code) explicitly.
What Happens Before main()
The runtime does a significant amount of work before your main function ever runs. Understanding this sequence helps explain bugs that can occur during startup, especially when you use package-level variable initializations or the special init function.
The order is roughly:
- The runtime initializes global state (memory allocator, garbage collector, scheduler).
- All imported packages are initialized, in dependency order.
- Within each package, package-level variables are initialized in declaration order.
- Then, any
init()functions defined in that package are executed. - After all packages (including
main) have gone through steps 3 and 4, themainfunction is called.
A package can define multiple init functions, even across multiple files in the same package. They all run, in the order the files are presented to the compiler, before main starts.
package main
import "fmt"
var startupMessage = buildMessage()
func buildMessage() string {
fmt.Println("Variable initialization")
return "Ready"
}
func init() {
fmt.Println("First init in main")
}
func init() {
fmt.Println("Second init in main")
}
func main() {
fmt.Println(startupMessage)
fmt.Println("main executing")
}
The output of this program will be:
Variable initialization
First init in main
Second init in main
Ready
main executing
init() is for package setup, not application logic:
init functions are designed for one-time package-level initialization: registering database drivers, validating configuration constants, or setting up tables. They are not a substitute for a proper application bootstrap sequence inside main. Avoid putting long-running or blocking operations in init — they will delay the entire program startup.
A main package can have init functions, and they will run just before main. This is sometimes used to seed random number generators or set up logging, though many modern Go projects prefer to do this explicitly at the top of main for clarity.
Organizing Code with Multiple main Packages
A single Go project can produce multiple executables. This is common in larger codebases where you have a server binary, a CLI tool, a migration runner, and so on — all living in the same repository.
The standard convention is to place each executable’s main package inside a subdirectory of cmd/:
myproject/
├── cmd/
│ ├── server/
│ │ └── main.go # package main, func main()
│ ├── worker/
│ │ └── main.go # package main, func main()
│ └── cli/
│ └── main.go # package main, func main()
├── internal/
│ └── ... # shared library code
└── go.mod
Each subdirectory of cmd/ is its own main package, completely independent of the others. You can build each one individually:
go build ./cmd/server # produces a binary named server (or server.exe)
go build ./cmd/worker # produces a binary named worker
Correct structure for multi-binary projects:
If your project needs to ship multiple executables, the cmd/ pattern keeps boundaries clean. Each subdirectory under cmd/ is a separate compilation unit with its own main package and main function. Library code lives outside cmd/ in packages like internal/ or pkg/, and each executable imports only what it needs.
It is impossible to have two main functions in the same package, but having them in separate packages (separate directories) is not only allowed — it is the intended design for multi-binary projects.
File and Package Conventions
The file that contains the main function is conventionally named main.go. This is a naming convention only; the compiler does not require it. You could call the file entry.go, start.go, or anything else, as long as the package clause at the top says package main and a main() function exists somewhere in the package.
However, sticking with main.go makes it immediately obvious where the entry point lives when someone new opens the repository.
A main package can span multiple files, just like any other package. The only requirement is that every file in the same directory declares package main at the top, and collectively they contain exactly one main function.
myapp/
├── main.go // package main; func main()
├── helpers.go // package main; func helper()
└── config.go // package main; func loadConfig()
When you run or build a multi-file main package, you must include all the files. For example, running go run main.go alone would fail if main() calls functions defined in helpers.go:
./main.go:10:2: undefined: helper
You can either list all files explicitly:
go run main.go helpers.go config.go
or, far more commonly, use the directory wildcard to include everything in the current package:
go run .
The same applies to go build and go install. Using the directory form (go build .) is the idiomatic way and avoids forgetting files.
Forgotten files cause undefined errors:
When building a main package that spans multiple files, make sure you reference the directory (.), not a single file. Running go run main.go ignores other .go files in the same package, leading to "undefined" errors for any function or variable they define.
Common Pitfalls and Compiler Errors
A handful of mistakes come up repeatedly when developers first work with Go’s entry point system. Recognizing them early saves debugging time.
Defining main() in a Non-main Package
If you write func main() inside a package that is not named main, the compiler treats it as a perfectly ordinary function — it has no special significance. The program will compile as a library, but you cannot build an executable from it. If you try to run it, Go will tell you:
go run: cannot run non-main package
The fix is to ensure the package clause reads package main, not package mylibrary or similar.
Trying to Import a main Package
We covered this earlier: importing a main package is a compile-time error. If you need to share code between multiple executables, extract that code into a separate library package (outside cmd/) and import it from each main package.
Forgetting to List All Files
As shown above, pointing go run or go build at a single file when the main package spans several files produces undefined symbol errors. Use go run . or go build . to include the entire package.
Writing Multiple main() Functions in One Package
A single main package (a single directory) can have only one main function. If you accidentally define two — perhaps by copying a template and forgetting to remove the old one — the compiler will refuse to build:
main redeclared in this block
Expecting Program Flow After main Returns
Because program execution stops the moment main returns, any deferred cleanup in spawned goroutines will not run. If your program needs a graceful shutdown that waits for background tasks, your main function must explicitly wait on them (using channels or sync.WaitGroup). Deferred statements inside main itself do execute normally when main returns, but deferred statements in other goroutines are abandoned.
Summary
The main package and main function are not just conventions — they are the language-level mechanism that distinguishes an executable program from a library. The main package cannot be imported, must contain a single main() function with a fixed signature, and is the target of the go build command when producing a binary.
Before main runs, the Go runtime initializes all imported packages, evaluates package-level variables, and executes any init functions. Once main returns, the entire program exits, regardless of other running goroutines.
A project can contain multiple main packages — each in its own directory — to produce multiple executables from the same codebase. The standard cmd/ layout keeps these separate and clear.
Understanding the entry point is the foundation for structuring every Go program you will ever write. With this in place, you are ready to build your first executable, handle command-line arguments, and start composing real applications.