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.PiReturning 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.gomyapp/
├── 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) Software Design
Cohesion and Coupling
Every software system can be understood in two dimensions: cohesion and coupling. Cohesion is about how tightly related the code inside a module is; coupling is about how strongly that module depends on others. The goal is high cohesion within modules and low coupling between them.
Dependency Inversion
Dependency Inversion means high-level code should not depend directly on low-level implementations. Both should depend on stable abstractions. In Go, this is achieved naturally through interfaces defined by the consumer, not by the producer.
// consumer layer
package service
type UserStore interface {
Save(User) error
FindByEmail(string) (User, error)
}
type UserService struct {
store UserStore // depends on abstraction
}
func NewUserService(s UserStore) *UserService {
return &UserService{store: s}
}Here, the service depends only on UserStore. The real implementation - SQL, in-memory, or API-based - can change freely without breaking the service. This creates low coupling and strong module boundaries.
In idiomatic Go this leads to the rule: depend on interfaces, return concrete types. High-level code should accept small interfaces at its boundaries, while constructors like NewUserService return concrete structs so callers don’t need type assertions or downcasts.
There are rare but important cases where returning an interface is the right choice. A classic example is net.Listen(network, address) (net.Listener, error). Callers usually only care that it can Accept, Close and Addr(), not which concrete type it is.
Inversion in practice
5) Testing and Visibility
Two kinds of tests
package XinX_test.go=> White-box: test runs inside package X; can see unexported stuff.package X_testinX_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.