Module 2 · Lesson 2 · ~20 min read
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.
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
}
The single most important style rule, and the one most often broken by Java/Python expats:
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:
| Awkward | Idiomatic |
|---|---|
tokenpkg.TokenManager | token.Manager |
ledger.LedgerClient | ledger.Client |
util.JSONUtils.Marshal | json.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.
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.
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.
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.
command_service.go, retry_policy.go.client.go, options.go, errors.go are all common patterns.doc.go sometimes holds the package-level doc comment if the package has many files.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.
i in a loop, err for error). Long for long scopes (a package-level config has its full name).userList, not userListSlice).HTTPSServer, URLPath, not HttpsServer.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.
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.
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.
When designing a new package, ask:
util and helpers — name the actual purpose.NewX. Functional options for configurable types.