Packages and Imports
Every Go file begins with a package declaration, and the rule connecting files to packages is stricter than in most languages: one folder is one package. Every .go file in a folder must declare the same package name, and no subfolder is part of it.
myapp/
├── main.go package main
├── config.go package main ← same folder, same package
└── storage/
├── store.go package storage ← different folder, different package
└── query.go package storageThat constraint does more work than it appears to. It means the import path of a package is always derivable from its location, and it means you can see a package's entire contents by listing one directory.
Files in a package share everything
There is no file-level privacy. A lowercase name in config.go is visible from main.go without any import, because they are the same package.
// config.go
package main
var defaultPort = 8080
func loadConfig() Config { /* ... */ }// main.go
package main
func main() {
cfg := loadConfig() // no import, same package
fmt.Println(defaultPort)
}Splitting a package into several files is purely organisational. Do it when a file gets long or when a group of functions has a clear theme, not because you want to hide something.
Importing
import "fmt"
import (
"fmt"
"net/http"
"os"
)The string is the import path, which for standard library packages is just the package name or a path within it. The identifier you use in code is the package name, declared in the imported files.
Usually these match, and occasionally they do not:
import "math/rand" // path is math/rand, package name is rand
rand.Intn(100)
import "gopkg.in/yaml.v3" // path ends in yaml.v3, package name is yaml
yaml.Unmarshal(data, &cfg)When they differ, your editor will tell you, and goimports will get it right automatically.
Import grouping
gofmt sorts imports within a group but does not create groups. The universal convention is two groups, standard library first:
import (
"context"
"fmt"
"net/http"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/yourorg/yourapp/internal/config"
"github.com/yourorg/yourapp/internal/storage"
)Three groups, separated by blank lines: standard library, third party, then your own module. goimports maintains this if you configure it with -local github.com/yourorg, and most editors do it for you.
Import aliases
Rename an import when there is a collision or when the name is unhelpful:
import (
"math/rand"
crand "crypto/rand" // both would be "rand"
)import (
pb "github.com/yourorg/proto/gen/orders/v1" // generated names are often long
)Two special forms exist and both should be used sparingly.
The blank import
import _ "github.com/lib/pq"Imports a package purely for its init side effects, without using any of its names. Database drivers are the canonical case: importing lib/pq registers a driver called postgres with database/sql, and your code then refers to it by string.
import (
"database/sql"
_ "github.com/lib/pq"
)
db, err := sql.Open("postgres", dsn) // the driver registered itselfAlways add a comment explaining why, since a bare underscore import looks like a mistake:
_ "github.com/lib/pq" // registers the postgres driverThe dot import
import . "fmt"
Println("no prefix needed")This dumps every exported name into your file's scope. It makes code unreadable, because a reader cannot tell where Println came from, and it breaks the moment two dot-imported packages share a name.
The only accepted use is inside test files for BDD-style assertion libraries. Avoid it everywhere else.
Unused imports are errors
import (
"fmt"
"os" // not used anywhere
)./main.go:5:2: "os" imported and not usedNot a warning. The build fails. This keeps import blocks honest, and it is one more reason to let goimports manage them: it adds what you use and removes what you stopped using, on save.
init functions
A package can have init functions that run before main, after all package level variables are initialised.
package config
var settings map[string]string
func init() {
settings = make(map[string]string)
settings["env"] = os.Getenv("APP_ENV")
}The rules:
- A file can have several
initfunctions, and a package can have many across its files - They take no arguments and return nothing
- They run in file name order within a package, which is fragile enough that you should never depend on it
- All of a package's dependencies are fully initialised before its own
initruns
import dependencies initialised
↓
package level vars initialised
↓
init() functions run
↓
main() runsinit is easy to overuse. Code that runs implicitly is hard to test, impossible to run twice with different settings, and produces startup failures with no obvious cause. Prefer an explicit New or Setup function that main calls. Reserve init for genuine registration, like the database drivers above.
No circular imports
Go forbids import cycles outright.
package a imports package b
package b imports package a
→ build error: import cycle not allowedThere is no workaround, no forward declaration, and no lazy import. This is a deliberate constraint that keeps the dependency graph acyclic, which is why Go builds are fast and why the compiler can be sure of initialisation order.
When you hit a cycle, one of three things is true.
The two packages are really one package. Merge them.
A shared type belongs in a third package. Extract it.
Before After
────── ─────
user ⇄ order user → model
order → modelThe dependency should be inverted with an interface. This is the most common fix and it follows the design rule from the interfaces section: the consumer declares what it needs.
// package notify -- does not import user
type UserLookup interface {
FindEmail(ctx context.Context, id string) (string, error)
}
func Send(ctx context.Context, l UserLookup, id string, msg Message) error {
email, err := l.FindEmail(ctx, id)
// ...
}// package user -- does not import notify
func (s *Service) FindEmail(ctx context.Context, id string) (string, error) { /* ... */ }Neither package imports the other. main wires them together.
Package naming
The name is what callers type at every use, so it matters more than most identifiers.
| Do | Avoid |
|---|---|
| short, lowercase, one word | myUtilPackage, user_service |
| a noun describing what it provides | verbs, or vague words |
http, json, user, storage | utils, helpers, common, base |
| singular | plural, usually |
The name combines with what it exports, so avoid stuttering:
// Stutters
http.HTTPServer
user.UserService
config.ConfigLoad
// Reads well
http.Server
user.Service
config.Loadutils, helpers, and common are the packages that eventually contain everything and depend on everything. They have no coherent purpose, so nothing can be excluded from them. Name packages after what they do, and if a function has no obvious home, that is usually a sign the design has a gap rather than that you need a junk drawer.
Documenting a package
A comment block immediately before the package declaration, in any one file, is the package documentation. By convention it lives in a file called doc.go when it is long.
// Package storage provides persistence for orders and customers.
//
// The package assumes a PostgreSQL database and expects the schema in
// migrations/ to be applied before use. All functions take a context
// and respect its cancellation.
//
// Basic usage:
//
// store, err := storage.Open(ctx, dsn)
// if err != nil {
// return err
// }
// defer store.Close()
package storageCheck what it looks like:
go doc ./internal/storage
go doc ./internal/storage.OpenIndented lines in a doc comment render as code blocks, which is how examples get formatted on pkg.go.dev.
Next, let's look at the capitalisation rule that decides what any of this is actually visible to.
How is this guide?
Last updated on
