Source File Structure
Understand the anatomy of every Go source file – the package clause, import declarations, and how to organize code inside a file for clarity and correctness.
A Go program is built from one or more .go source files. No matter how large or small the file is, every single one shares the same skeletal shape. The compiler demands a package clause on the very first line, then zero or more import declarations, followed by the rest of the code. Missing any of these rules means the file will not compile.
This document walks through each mandatory and conventional part of a Go source file. By the end you will be able to read any .go file and immediately understand why each line is where it is, and you will know the handful of mistakes that make the compiler reject a perfectly good‑looking file.
The Package Clause
Every Go source file must announce which package it belongs to as its first non‑comment, non‑blank line. This is the package clause.
package main
The keyword package is followed by an identifier – the package name. That name is the identity of the file for the rest of the program. If the file is part of the application entry point, the name must be main. In libraries and reusable code, the name typically matches the directory that contains the file.
Why is this mandatory? Go does not have header files or forward declarations. The package clause is the single declaration that ties a file to all the other files that share the same name. It is how the compiler knows “this function from math belongs over here, and that function from math belongs over there.” Without it, the compiler has no way to build a coherent unit of code.
How beginners should think about it
Think of a package as a folder in your file system that contains all the code for one responsibility. Every file inside that folder starts with the same package line. If you are writing a simple command‑line tool, you put everything in package main. If you are writing a reusable math library, the folder is called math and every file inside starts with package math. The directory name and the package name are almost always the same.
Missing package clause:
A .go file without a package clause at the top produces the compiler error syntax error: non-declaration statement outside function body. The error message is confusing, but it always means the file did not start with package.
Common mistakes
- Placing comments or blank lines before the package clause. The
packagekeyword must be the first code token. Comments that appear before it are allowed (the compiler skips them), but a newline or a blank line alone is not a problem – however, a random expression or import beforepackageis illegal. - Using a package name that does not match the directory. This is legal but highly confusing. If the directory is
transportand the file sayspackage net, someone importingtransportwill have to writenet.DoSomething. That mismatch makes the codebase difficult to navigate and is considered poor practice. - Forgetting that
package mainis special. Only amainpackage can produce an executable binary. A file in amainpackage must also contain afunc main()if it is the entry point file (but it does not have to be the only file in the package; other files in the samemainpackage can hold helper code).
Good package naming:
A package name that matches the last element of the import path (e.g., import "example.com/calc" and package calc) makes your code predictable for anyone reading it. The Go tools and community expect this.
Import Declarations
After the package clause, a file declares which other packages it needs with one or more import statements. Imports give the current file access to the exported identifiers (capitalized names) of those packages.
A single import looks like this:
import "fmt"
When you need more than one package, the idiomatic Go style is a factored import block:
import (
"fmt"
"os"
)
This block appears once, right after the package clause, and lists each imported package path on its own line. You can use either single imports or the block, but the Go formatter (gofmt) will always convert multiple single imports into the block form.
How imports work under the hood
The import path is a string literal that identifies a module or standard library package. The compiler resolves it from the module cache (for external dependencies) or from the Go root (for standard library packages). Once resolved, the current file can refer to the package by its declared package name – usually the last element of the path. import "fmt" makes the name fmt available.
Import paths, not file names:
You import the module path, not a file system location. Even if your package sits in a directory called utils, the import is the full module path, e.g., "example.com/project/utils".
Import naming and renaming
Sometimes the package name you want to use locally is not the last element of the path, or two packages have the same name. You can rename an imported package by providing an explicit name before the path:
import (
myfmt "mylib/fmt"
"fmt"
)
Now myfmt refers to the custom package and fmt refers to the standard library.
Blank imports (_ "package") are used when you need a package’s side effects (like registering a database driver in its init function) but never call any of its exported symbols. The blank identifier silences the “imported and not used” error.
import _ "github.com/lib/pq"
Dot imports (. "package") bring every exported name from that package directly into the current file’s scope, so you can write Println instead of fmt.Println. The Go community strongly discourages dot imports except in tests or very rare internal tooling, because they make it unclear where a name came from.
Dot imports obscure code:
A dot import means a reader cannot tell at a glance whether a function comes from the current package or an external one. They are banned by many style guides. Use them sparingly, and only when you can justify the reduction in clarity.
The “unused import” rule – and why it exists
Go will refuse to compile a file that imports a package and never uses it. This is not just fussiness. Unused imports were a historical source of pain in large C codebases, where dead imports would accumulate, slow down builds, and confuse maintainers. Go forces you to keep your imports clean.
If you are actively experimenting and want to temporarily silence the error without removing the import, use a blank import:
import _ "os"
But do not leave blank imports in committed code unless they are genuinely needed for side effects.
Import cycles are forbidden
If package A imports B, then B cannot import A (directly or transitively). The compiler will report import cycle not allowed. This forces a clean dependency structure and prevents the circular entanglement that makes code untestable. If you hit this, extract the shared code into a third package that both can import.
File Organization Conventions
After the package clause and import block, the rest of the file is a sequence of top‑level declarations: constants, variables, types, and functions. Go does not enforce a specific order among them, but decades of community practice have settled on a readable pattern.
What goes where inside a file
A well‑groomed Go file follows roughly this flow:
- Package clause – always first.
- Import block – always second, if any imports are needed.
- Constants and variables that are package‑level and used broadly (e.g., configuration defaults, sentinel errors).
- Type definitions (structs, interfaces, type aliases) that the functions will operate on.
- Functions – often starting with exported (public) functions, followed by unexported helpers.
This is not a strict rule, and the compiler does not care. However, every developer who opens a file expects to find the “what” (types and variables) before the “how” (functions). When the pattern is followed, scanning a file is fast.
Consider a tiny file that defines a configurable greeter:
package greeter
import "fmt"
const defaultGreeting = "Hello"
type Greeting struct {
Word string
}
func NewGreeting() Greeting {
return Greeting{Word: defaultGreeting}
}
func (g Greeting) Say(name string) {
fmt.Printf("%s, %s!\n", g.Word, name)
}
The constant sits near the top because it belongs to the whole package. The struct Greeting appears before any function that uses it, so a reader knows what the type looks like before they encounter NewGreeting. The exported constructor comes next, and the method last. That order is not accidental – it tells a story.
The role of gofmt:
The gofmt tool formats code automatically, but it does not reorder top‑level declarations. You are in control of the order, so choose an order that helps the reader.
Multiple files in the same package
A Go package can span multiple source files in the same directory. Every file in that directory must share the same package name. The compiler stitches them together into one unit, so a variable declared in a.go is visible in b.go automatically, without any header or inclusion mechanism.
This means you can separate concerns inside a package by file name. A common convention:
doc.go– package‑level documentation (only comments).types.go– core type definitions.helpers.go– unexported utility functions.- One file per logical feature or exported function set.
There is no import needed between files in the same package. The compiler merges them at build time. If a function in server.go calls a helper in helpers.go, it just works.
Don't create one file per type:
It can be tempting to create a file user.go for the User struct, order.go for the Order struct, and so on. This quickly fractures the package into tiny, disconnected pieces. Keep a file focused on a cohesive set of concerns, not a single type.
The internal directory convention
When your module grows, you may want to expose certain packages for import while keeping others truly private. Go provides a compiler‑enforced mechanism: any package whose import path contains the segment internal can only be imported by code in the tree rooted at the parent of internal. For example, a file in project/internal/auth can be imported by project/cmd/server, but someone outside your module who tries to import it will get a compile error.
This is a file organization convention that influences source file placement, not a file‑internal rule. But it is worth knowing now because it shapes how you decide what goes in which directory. Use internal for implementation details you never want to leak.
When internal works best:
If you are building a web service and have a handler package that should never be imported by other modules, place it under internal/handler. That way, the compiler enforces your design decision automatically.
The init function and file order
A source file can optionally define a function func init(). The Go runtime calls all init functions in a package before main runs, in the order the files are presented to the compiler (which is effectively alphabetical by file name). Init functions are used for one‑time setup – registering drivers, validating configuration, populating caches.
Multiple files in the same package can each have an init. The execution order across files is lexicographic, so a common convention is to name the file 0_init.go if you need the init to run before others. However, relying on init order is fragile and can make debugging painful. A better approach is to avoid implicit dependencies between init functions.
Practical Summary
A Go source file is a minimal, disciplined unit. It starts with package, follows with imports if needed, and then lays out declarations in an order that helps a human reader build a mental model. The rules are rigid – missing package clause, unused imports, cyclic imports – but each one exists to prevent real, observable maintenance problems that plague projects in other languages.
Once you understand these three sections, you can read any .go file with confidence.