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) 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.

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

Note

Inversion in practice

Define interfaces in the layer that uses them (the consumer). Implement them in the layer that provides the functionality (the producer).

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.