02 - Packages & Visibility

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_example.go
// 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.

shapes/shapes.go
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

imports.go
import (
	m "math"           // alias import
	_ "net/http/pprof" // blank import for init side-effect
)

var _ = m.Pi

Returning unexported types from exported functions

Note

Is it good or bad?

It’s a powerful encapsulation technique. You hand out a value without exposing its concrete type. Pros: you can change internals freely. Cons: callers can’t type-assert to the concrete. Tip: when consumers need mocking/extensibility, return an exported interface implemented by your hidden type.

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.

before.txt
myapp/
 ├── api/
 │   └── client.go
 ├── db/
 │   └── conn.go   # public to everyone
 └── main.go
after.txt
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

Note
We’ll cover application structuring patterns in another module.

4) Patterns for Encapsulation

Opaque Types

Expose a constructor, keep the concrete type unexported. Callers can use methods but can’t mutate internals directly.

counter.go
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 }
Note

Why hide the type?

1) Enforce invariants - an invariant is a rule that must always be true for your type (e.g. a bank account should never go negative). By hiding fields, you force all changes through safe methods that respect those rules. 2) Hide complexity - callers don’t need to care about your internals (like a hidden sync.Mutex). You can change them later without breaking users.

Exported interface + hidden concrete

Expose an interface; return it from your constructor. Implement it with an unexported struct. Public API stays stable; implementation can evolve.

counter_iface.go
package counter

type Counter interface {
	Inc()
	N() int
}

type counter struct { n int } // hidden

func (c *counter) Inc() { c.n++ }
func (c *counter) N() int { return c.n }

func New() Counter { return &counter{} }

Public type, private fields

Make the type name public (so callers can declare/return it) but keep fields private to maintain invariants.

config.go
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 }
Note

Why hide fields?

Callers can hold a 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 in X_test.go => White-box: test runs inside package X; can see unexported stuff.
  • package X_test in X_test.go => Black-box: test imports X as an outsider; only exported API is visible.

Example: White-box

mathx/mathx_test.go
package mathx

import "testing"

func TestInternalAdd(t *testing.T) {
	if secretAdd(2, 3) != 5 { // unexported but visible here
		t.Fatal("fail")
	}
}

Example: Black-box

mathx/public_test.go
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.