Skip to content
gohacks.dev
Basics

Packages in Go and How Names Get Exported or Hidden

One directory is one package, and a capital first letter exports a name. See imports, unexported names failing to compile, internal and import cycles.

7 min read
Summarize in AI
Flat illustration of an editor window full of code.

Go's module system has two ideas that carry nearly everything: a package is a directory, and a name's first letter says who may use it. There are no header files, no access modifiers and no namespaces to declare. Once those two rules are in your head, a lot of Go project layout stops being a convention you have to memorize. This article builds a small module with a second package, shows what the compiler lets you use and what it refuses, covers the internal directory and import cycles, and notes the places where the capitalization rule surprises people. It belongs in the Go basics path, after your first program and modules, and it leads into package naming.

Tested with Go 1.25.6, macOS on Apple Silicon (arm64)

A package is a directory

Every Go file starts with package name. All the files in one directory must declare the same name, with one exception, a _test suffix for external tests. The import path identifies the package to other code: the module path from go.mod, plus the directory relative to the module root. For a module example.test/lab with a directory greet, the import path is example.test/lab/greet. Here is a module with two packages:

greet/greet.goGo
package greet

import "strings"

const prefix = "hello, "

// Count is exported, so other packages can read and change it.
var Count int

type Greeter struct {
	Name  string
	calls int
}

func Hello(name string) string {
	Count++
	return prefix + title(name)
}

func title(s string) string {
	if s == "" {
		return s
	}
	return strings.ToUpper(s[:1]) + s[1:]
}

func (g *Greeter) Greet() string {
	g.calls++
	return Hello(g.Name)
}

func (g *Greeter) Calls() int { return g.calls }
main.goGo
package main

import (
	"fmt"

	"example.test/lab/greet"
)

func main() {
	fmt.Println(greet.Hello("ada"))

	g := &greet.Greeter{Name: "grace"}
	g.Greet()
	fmt.Println(g.Greet(), g.Calls())
	fmt.Println(greet.Count)
}
Text
hello, Ada
hello, Grace 2
3

The importing code uses the last element of the path as the package name, greet, and reaches everything exported through it: the function Hello, the type Greeter, its exported field Name, its methods and the variable Count. The counter reached 3 because Hello ran once directly and twice through Greet. The calls field is lowercase, so main cannot touch it, and the exported method Calls is how it reads the value. That is the pattern for every Go type with state: keep fields private when they have invariants, and expose methods.

What the compiler refuses

The rule is enforced with plain errors. Here is main trying to use the private parts of the same package:

greet/greet.goGo
package greet

type Greeter struct {
	Name  string
	calls int
}

func title(s string) string { return s }

const prefix = "hello, "

func Hello(name string) string { return prefix + title(name) }
main.goGo
package main

import (
	"fmt"

	"example.test/lab/greet"
)

func main() {
	g := greet.Greeter{Name: "ada"}
	fmt.Println(greet.title("x"), g.calls, greet.prefix)
}
Text
# example.test/lab
./main.go:11:20: name title not exported by package greet
./main.go:11:34: g.calls undefined (cannot refer to unexported field calls)
./main.go:11:47: undefined: greet.prefix

Three errors come back, one for each private name. The wording differs: one says the name is not exported by the package, one that the field cannot be referred to because it is unexported, and one says only that the name is undefined. That strictness is the reason a package author can rename title or reorder calls without checking a single importer. Only exported names are the contract, and everything else is implementation.

The capital-letter rule applies uniformly. It covers functions, methods, types, struct fields, constants, variables and interface methods. It also decides what encoding/json and reflect can see, as structs and their tags showed. It is purely lexical and based on Unicode: a name that begins with a letter that has no uppercase form, such as one in a script without case, is not exported.

Where things go

For a small program, one main package in the module root is enough. As code grows, the common layout is the main package at the top or under cmd/<name>/, and library code in subdirectories named for what they do. Here is how I would think about the directories:

DirectoryHoldsImportable by
/ or /cmd/apppackage main, the entry pointNothing; it builds a binary
/store, /billingPackages with a clear job and a small exported surfaceAnyone, including other modules
/internal/...Code that is shared inside the module but not part of its public APIOnly code in the parent of internal and below
/testdataFixture files for tests; ignored by the toolchainNot a package

The internal rule is enforced by the toolchain: a package at example.test/lab/internal/cache can be imported only from packages whose path begins with example.test/lab/. Another module that tries gets a compile error saying the use of an internal package is not allowed. That makes internal the right home for anything you do not want to promise to maintain, such as helpers and adapters, because the public API of a module is whatever importers can reach, and internal removes code from that set without making it private to a single package. The directories vendor and testdata, and any path element starting with . or _, are ignored by ./... patterns.

Import cycles

Package A importing package B, which imports package A, is not allowed. The compiler reports it with the chain:

a/a.goGo
package a

import "example.test/lab/b"

func A() string { return "a" + b.B() }
b/b.goGo
package b

import "example.test/lab/a"

func B() string { return "b" + a.A() }
main.goGo
package main

import (
	"fmt"

	"example.test/lab/a"
)

func main() { fmt.Println(a.A()) }
Text
package example.test/lab
	imports example.test/lab/a from main.go
	imports example.test/lab/b from a.go
	imports example.test/lab/a from b.go: import cycle not allowed

The error is import cycle not allowed, followed by the cycle's package list. It happens in real projects when two packages grow into each other: models calls a function in services, and services uses types from models. The fix is structural. Move the shared types into a third package that both import, or define the dependency as an interface in the package that needs it, so the dependency points one way. Accept interfaces, return structs covers that move, and it is the reason small consumer-side interfaces keep a code base free of cycles.

Imports in the wild

The import block supports a few spellings, and you will meet them all:

Go
import (
	"fmt"                       // normal
	str "strings"               // alias, use it as str.ToUpper
	_ "embed"                   // blank import: for the init side effects only
	. "math"                    // dot import: avoid it; names appear unqualified
	"example.test/lab/greet/v2" // package name is still greet, the /v2 is path only
)

An alias is the fix when two imports share a package name, such as two packages called errors. A blank import runs a package's init functions without using its names, which is how database/sql drivers register themselves, and the init function article covers when that is a good idea. A dot import hides where a name comes from and is for generated code and the odd test, and I avoid it. A major version suffix like /v2 is part of the path and not of the package name, so the code still says greet.Hello.

Surprises about exported names

Three things catch people. First, an unexported type can be returned by an exported function, and the caller can use the value without ever naming its type. That is legal and mildly annoying in documentation, since godoc hides the type. Second, an exported field of an unexported struct type, and the reverse, behave as you would expect: access depends on the name you write at the point of use. Third, a type's methods are exported by the method name and not by the type, so an exported method on an unexported type is reachable through an interface or a returned value, which is how many standard library internals work.

When to split a package, and when not to

New Go programmers tend to over-split, creating a models, a utils and a helpers directory on day one. A package should be a unit with a purpose and a short exported surface. If two files always change together and neither makes sense alone, they belong in one package. If a package has a name like utils or common, it is a sign that its contents have no shared reason to exist, as package naming in Go explains. Splitting too early also increases the chance of import cycles. Start with one package, and extract when a piece has a clear boundary and a second user. The official references are the specification on packages, the section on exported identifiers and the Go blog's article on package names.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.