Go File Structure Explained

package, import, and func

Go files use package declarations to group code, import statements to reuse external libraries, and function definitions to execute logic.

Go File Structure: Packages, Imports, and Functions

You save a Python script as app.py and run it. The file is the module. In Go, saving app.go does nothing until you declare the package and define the entry point. The file is just text. The compiler ignores your filename. It cares only about the first line and the structure inside. Go organizes code into packages, not files. A file is a convenience for your editor. The compiler merges all files in a directory that share the same package name into a single compilation unit.

This model changes how you think about organization. You don't import files. You import packages. You don't split code by file boundaries. You split code by logical boundaries, and the compiler handles the rest.

The package is the atom

Think of a package as a team. Every member of the team wears the same name badge. The files are just the desks where team members write their notes. When the manager reviews the work, they don't look at desks individually. They gather all notes from everyone with the same badge and treat it as one big document.

In Go, the package declaration is that badge. Every .go file must start with package name. If you have two files in the same folder, they must declare the same package name. The compiler groups them together automatically. Any function in file_a.go can call any function in file_b.go without any import or include statement. The connection is implicit because they share the package.

This design eliminates the "include hell" found in other languages. You never worry about circular includes between files in the same package. The compiler sees the whole package at once. It resolves all symbols across all files before checking for errors.

Minimal example

Here is the smallest runnable Go program. It demonstrates the three core elements: package declaration, import, and function.

package main

import "fmt"

// main is the entry point for executable programs.
// The compiler requires this exact signature to start execution.
func main() {
    // fmt.Println writes to standard output and adds a newline.
    fmt.Println("Hello, Go")
}

The package main line tells the compiler this package produces an executable binary. The import "fmt" line brings in the formatting package from the standard library. The func main() block defines the logic that runs when the program starts.

Save this as hello.go. Run go run hello.go. The compiler scans the file, sees package main, finds func main(), resolves fmt, and generates a temporary binary to execute.

How the compiler merges files

When you run go build, the tool performs a precise sequence of steps. It scans the directory and groups files by their package declaration. It rejects any directory containing files with mixed package names. If one file says package main and another says package utils, the build fails with package main; expected package utils.

Once grouped, the compiler parses every file into an abstract syntax tree. It merges the trees into a single representation of the package. It resolves imports by checking the module cache or downloading dependencies. It generates object code for the package. If the package is main, it links the object code into a final binary. If it is a library package, it archives the object code into a .a file for other packages to use.

The build cache stores the result. If you modify one file, the compiler recompiles only that package. It checks the content hash, not the timestamp. This makes builds fast and reproducible.

Initialization order follows a strict rule. If a package defines func init(), the compiler runs it after imports are initialized. Within a package, init functions run in file order based on the sorted filenames. This is deterministic but fragile. Relying on init order between files is a bad practice. Keep initialization logic self-contained.

Imports are explicit and strict

Go has no implicit globals. If you use a symbol from another package, you must import it. The compiler checks every import. If you import a package and don't use it, the build stops with imported and not used. This rule forces clean code. You cannot have dead dependencies cluttering the file.

Imports use the full module path. import "fmt" works because fmt is in the standard library root. For third-party code, you use the full path: import "github.com/user/repo/pkg". The compiler uses this path to locate the code.

You can alias imports to resolve name collisions or shorten long paths.

import (
    // Alias prevents collision when two packages share the same name.
    alias "example.com/pkg/alias"
    
    // Short alias improves readability for long paths.
    db "example.com/myapp/database"
)

The blank identifier _ allows importing a package for side effects. import _ "database/sql/driver" runs the package's init function without accessing any exported symbols. This pattern registers database drivers. Do not use this for regular code. It hides dependencies and makes the code harder to audit.

Convention dictates grouping imports. Standard library imports come first, followed by a blank line, then third-party imports, then local imports. gofmt enforces this grouping automatically. Most editors run gofmt on save. Trust the tool. Argue logic, not formatting.

Functions and visibility rules

Functions define executable logic. The signature is func Name(params) (returns). Go functions can return multiple values. This is idiomatic for returning results and errors together.

Visibility is controlled by capitalization. This is a hard rule with no keywords.

  • Names starting with a capital letter are exported. Other packages can access them.
  • Names starting with a lowercase letter are unexported. Only the current package can access them.

If you try to call a lowercase function from another package, the compiler rejects it with undefined: privateFunc. Inside the package, capitalization does not affect access. It only controls the boundary.

// Package calculator provides math helpers.
package calculator

// Add returns the sum of two integers.
// The capital A makes this function visible to other packages.
func Add(a, b int) int {
    return a + b
}

// validate checks bounds.
// The lowercase v keeps this function private to the package.
func validate(n int) bool {
    return n >= 0
}

// Subtract uses the private helper.
func Subtract(a, b int) int {
    if !validate(a) || !validate(b) {
        panic("negative numbers not supported")
    }
    return a - b
}

Doc comments must appear immediately before the declaration and start with the name of the item. // Add returns... not // This function adds.... The go doc tool extracts these comments to generate documentation. Follow the convention so the tooling works correctly.

Receiver names for methods should be short, usually one or two letters matching the type. (c *Config) is idiomatic. (self *Config) or (this *Config) is not. The community expects brevity here.

Realistic library structure

A real Go project splits code into multiple files within a package when the code grows. The files share the package name. The compiler merges them. You can define types in one file and methods in another. The compiler treats them as one unit.

// Package storage handles data persistence.
package storage

import (
    "context"
    "errors"
)

// Item represents a stored entity.
type Item struct {
    ID   string
    Data []byte
}

// Store provides methods for managing items.
type Store struct {
    items map[string]*Item
}

// NewStore creates a new Store instance.
// The receiver is implicit in the return value.
func NewStore() *Store {
    return &Store{
        items: make(map[string]*Item),
    }
}

// Get retrieves an item by ID.
// Context is always the first parameter, named ctx.
func (s *Store) Get(ctx context.Context, id string) (*Item, error) {
    // Check context cancellation before doing work.
    if err := ctx.Err(); err != nil {
        return nil, err
    }
    
    item, ok := s.items[id]
    if !ok {
        return nil, errors.New("item not found")
    }
    return item, nil
}

// Save stores an item.
// The underscore discards the error from a hypothetical side effect.
// Use this sparingly and only when the error is truly irrelevant.
func (s *Store) Save(ctx context.Context, item *Item) error {
    s.items[item.ID] = item
    // log.Save(item) // Hypothetical call where error is ignored.
    return nil
}

This example shows several conventions. The package name matches the directory. Types and methods are grouped logically. context.Context is the first parameter. Receiver names are short. Errors are returned explicitly. The code compiles as a single package regardless of how many files contain these definitions.

Common pitfalls and compiler errors

Go's strictness catches mistakes early. Here are the most common issues when working with file structure.

Mixed packages in a directory cause an immediate failure. The compiler reports package main; expected package foo. Ensure all files in a folder declare the same package.

Unused imports are syntax errors. The compiler stops with imported and not used. Remove the import or use the symbol. The blank identifier _ is the only exception, and it must be used intentionally.

Case sensitivity breaks visibility. Calling storage.get instead of storage.Get results in undefined: storage.get. Check the capitalization.

Missing entry points in main packages fail at link time. If you have package main but no func main(), the compiler says runtime.main: funcΒ·main is not defined. Every executable needs exactly one main function.

Filename mismatches confuse humans but not the compiler. Naming a file main.go while declaring package utils compiles fine, but it violates convention. The filename should reflect the content, and the package name should match the directory.

When to split files and packages

Organizing code requires judgment. Use these rules to decide how to structure your project.

Use a single file when the logic is small and cohesive. One file is easier to read than three tiny files. Keep related types, methods, and helpers together.

Use multiple files in one package when the code grows too long for a single editor window, or when you want to group related concepts logically. The compiler merges files automatically. Split by topic, not by size.

Use a new package when you need a clear boundary for visibility, or when the code serves a distinct purpose that other packages might reuse independently. Packages are the unit of compilation and testing. A new package creates a new namespace and enforces access control.

Use package main only for the entry point of an executable. Every binary needs exactly one main package with a main function. Do not put library code in main. Keep libraries in separate packages and import them.

The file is a container. The package is the truth. Capitalization controls the world. Lowercase stays local. Imports are explicit. The compiler checks every line. Split by logic, not by file size.

Where to go next