1) Packages 101
What is a Package?
A package in Go is a directory of related .go
files that share the same package
name. Think of it as a room or library shelf for your code. All files in that folder form one logical unit and are compiled together. By convention, the package name is the same as the directory (short, lowercase). For example, all files in utils/
might start with package utils
, allowing functions like utils.Add()
to be shared. Every package declares its namespace with package <name>
at the top.
// utils/add.go
package utils
func Add(a, b int) int {
return a + b
}
// utils/subtract.go
package utils
func Subtract(a, b int) int {
return a - b
}
Packages let you group related code. Without them, you’d have one monolithic program. Treat each package like a toolbox: one might handle math, another networking, another storage. This keeps code organized and reusable.
- Organize: group related functions, types, and constants in one package.
- Encapsulate: expose a clear API, keep details inside.
- Reuse: import the package in other parts or other projects.
Why do we need packages?
Packages enforce modularity and encapsulation. They split programs into clean components and give each a safe namespace, preventing name collisions as code grows.
- Organization: focused, tidy units of functionality.
- Control: exported names define your public surface; everything else stays private.
- Reuse: once isolated, the code is easy to share and import.
2) Who Can See What: Visibility Rules
The Capitalization Trick
Visibility is determined by the first letter: Uppercase = exported (public), lowercase = unexported (package-private). Applies to consts, vars, funcs, types, fields, methods.
package shapes
// Exported (public)
const Pi = 3.14159
// Unexported (private)
var version = "0.1.0"
type Circle struct {
Radius float64 // exported field
color string // unexported field
}
// Exported constructor
func NewCircle(r float64) *Circle {
return &Circle{Radius: r}
}
// Exported method
func (c *Circle) Area() float64 {
return Pi * c.Radius * c.Radius
}
// Unexported method
func (c *Circle) setColor(s string) {
c.color = s
}
- Uppercase identifiers are visible to importing packages.
- Lowercase identifiers are only visible inside the same package.
- Within a package, all files see both uppercase and lowercase names.
Imports: alias & blank
import (
m "math" // alias import
_ "net/http/pprof" // blank import for init side-effect
)
var _ = m.Pi
Returning unexported types from exported functions
Is it good or bad?
3) Organizing Real Projects: internal/, pkg/, cmd/
internal/ - your private workshop
internal/
is enforced privacy: only code within the same module can import it. Move volatile or sensitive packages here to prevent external coupling.
myapp/
├── api/
│ └── client.go
├── db/
│ └── conn.go # public to everyone
└── main.go
myapp/
├── api/
│ └── client.go
├── internal/
│ └── db/
│ └── conn.go # private to this module
└── main.go
- Prevents external imports of your internals.
- Lets you refactor internals without breaking users.
pkg/ - public library shelf (optional)
pkg/
is a convention (not enforced). Use it to signal: 'these packages are safe for others to import and we’ll keep them stable.' If you don’t need that signal, skip it.
- Use when you intend external reuse and a stable API.
- If you’re building a concrete, app-only repo, you can skip
pkg/
and structure the code however fits the app.
cmd/ - entrypoints (binaries)
Each subfolder under cmd/
builds one executable (e.g., cmd/server/main.go
). Keep main.go
thin; call into library packages for real logic.
- One folder per binary; folder name becomes the binary name.
- Thin mains: parse flags, wire deps, call into libraries.
Note
4) Patterns for Encapsulation
Opaque Types
Expose a constructor, keep the concrete type unexported. Callers can use methods but can’t mutate internals directly.
package counter
type counter struct { n int } // hidden
func New() *counter { return &counter{} } // exported factory
func (c *counter) Inc() { c.n++ }
func (c *counter) N() int { return c.n }
Why hide the type?
Public type, private fields
Make the type name public (so callers can declare/return it) but keep fields private to maintain invariants.
package config
type Config struct {
url string
timeout int
}
func NewConfig(url string, timeout int) Config {
return Config{url: url, timeout: timeout}
}
func (c Config) URL() string { return c.url }
func (c Config) Timeout() int { return c.timeout }
Why hide fields?
Config
but can’t corrupt it. They must use your methods/constructor, so invariants hold and internals remain flexible.5) Testing and Visibility
Two kinds of tests
package X
inX_test.go
=> White-box: test runs inside package X; can see unexported stuff.package X_test
inX_test.go
=> Black-box: test imports X as an outsider; only exported API is visible.
Example: White-box
package mathx
import "testing"
func TestInternalAdd(t *testing.T) {
if secretAdd(2, 3) != 5 { // unexported but visible here
t.Fatal("fail")
}
}
Example: Black-box
package mathx_test
import (
"testing"
"github.com/me/mathx"
)
func TestSquare(t *testing.T) {
if mathx.Square(3) != 9 { // only exported API visible
t.Fatal("fail")
}
}
6) Summary
- Export as little as possible - every exported name is a long-term promise.
- Hide internals with
internal/
when you don’t want external deps. - Prefer exported interfaces over exported concretes when designing public APIs.
- Write both white-box and black-box tests to cover internals and the public contract.