Module 2 · Lesson 2 · ~20 min read

Package Design & Naming

Go's unit of code organization is the package. The naming conventions are tighter than what you're used to — and more useful — and the visibility model is a single rule with no annotations.

Visibility — capitalization is the rule

type Client struct{}        // EXPORTED — visible to other packages
type client struct{}        // unexported — only this package

func Connect() *Client { ... }   // EXPORTED
func connect() *client { ... }   // unexported helper

One rule. No private, public, internal keywords. Identifiers starting with an uppercase letter are exported (visible outside the package); lowercase identifiers stay private to the package.

This applies to everything — types, functions, methods, fields, constants, variables. Even struct fields:

type Server struct {
    Addr     string     // exported field, accessible to consumers
    inflight int        // unexported, package-private
}

Package names

The single most important style rule, and the one most often broken by Java/Python expats:

Rule

Package names are short, lowercase, single-word, and never repeat the type they export.

Good: http, json, sql, token, ledger, crypto, grpc.
Bad: tokens (plural), token_utils (snake), TokenManager (Pascal), util (vague).

Package name + type name should read fluently:

AwkwardIdiomatic
tokenpkg.TokenManagertoken.Manager
ledger.LedgerClientledger.Client
util.JSONUtils.Marshaljson.Marshal

The package name is already in the call site. token.Manager reads naturally; token.TokenManager stutters. The standard library follows this religiously and so do high-quality Go projects you'll review.

Avoid util and helpers

"Util" is the universal name for "I didn't think hard enough about what this code is." If your code computes hashes, the package is hash. If it parses durations, duration. If it routes commands, router. There's always a better name than util.

Internal packages

If a package's import path contains /internal/, only code inside the same module subtree can import it.

example.com/myapp/
├── api/                       // public — anyone can import
├── internal/storage/          // private — only myapp/* can import
└── cmd/myapp/main.go

This is your only access modifier between "package-private" and "exported." Use it for things you want only your own modules to depend on.

One package per directory

A directory contains exactly one package. Files in the directory all start with package foo. There's no two-files-two-packages situation in idiomatic Go.

Test files (_test.go) get an exception: they can be in package foo (white-box tests, can see private identifiers) or package foo_test (black-box tests, only see exported). Lesson 4 covers this.

File naming inside a package

Receivers and naming

Receiver names are short — typically one or two letters, the first letter of the type:

func (c *Client) Connect() error { ... }
func (s *Server) ListenAndServe() error { ... }
func (req *Request) URL() *url.URL { ... }

Don't write (this *Client) or (self *Client). Use the same receiver name across all methods of a type — consistency matters more than expressiveness here.

Naming functions, variables, and parameters

Constructor convention

func NewClient(opts ...Option) *Client { ... }   // most common
func NewWithConfig(cfg Config) *Client { ... } // when you need a special form

NewX for "make a new X." If a package primarily exports one type and constructs it, New alone (no suffix) is acceptable for the primary constructor.

Functional options pattern

Where Java/TS use builder patterns or kwargs, Go uses functional options:

type Client struct { timeout time.Duration; retries int; tls bool }

type Option func(*Client)

func WithTimeout(d time.Duration) Option {
    return func(c *Client) { c.timeout = d }
}
func WithRetries(n int) Option {
    return func(c *Client) { c.retries = n }
}

func NewClient(opts ...Option) *Client {
    c := &Client{timeout: 30 * time.Second, retries: 3}  // defaults
    for _, opt := range opts {
        opt(c)
    }
    return c
}

// At the call site:
c := NewClient(WithTimeout(5 * time.Second))

Pros: extensible without breaking the constructor signature, defaults are explicit, options are documented and discoverable.

You'll see this pattern in basically every serious Go SDK. Canton-adjacent Go code uses it for client construction, retry policies, telemetry configuration.

Documentation comments

Every exported identifier should have a comment, starting with the identifier name:

// Client connects to a Canton participant's Ledger API.
// Use NewClient to construct one.
type Client struct { ... }

// Submit submits a single command and waits for completion.
// Returns the completion offset on success, or an error wrapping
// the underlying gRPC failure.
func (c *Client) Submit(ctx context.Context, cmd Command) (Offset, error) { ... }

godoc renders these. golangci-lint with the revive rule enforces them. They show up in IDE hover. They're not optional in real codebases.

What "package design" actually means in practice

When designing a new package, ask:

  1. What does this package do? One sentence, in plain English. If you can't, the package is too broad.
  2. What's the smallest exported surface? Hide every helper that callers don't need. Easier to expose later than to remove.
  3. Does the package depend on anything heavy? If a package imports a giant transitive dep, every importer pays for it. Keep packages light by default.
  4. Does the public API tell a story? A reader of just the exported docs (no source) should understand how to use the package.

Takeaways