Documentation is part of the API. A Go symbol's contract lives in its doc comment, and go doc surfaces it to every caller who never opens the source. This chapter makes that contract complete: a GoDoc comment on every exported symbol, written as full sentences starting with the name; package comments, testable examples, and explicit statements of the things a signature cannot show — concurrency safety, cleanup duty, and the errors a function returns. Comments explain why; the code shows what. Dead code and untracked TODOs do not survive review.
// Package hotel provides a client for the hotel search service.
//
// The primary entry point is Client, which executes search and booking
// operations against the remote API.
package hotel
// Client executes API operations against the hotel service.
//
// Client is safe for concurrent use by multiple goroutines.
type Client struct {
timeout time.Duration // maximum wait for a single request
}
// NewClient creates a new Client. The caller must call Close when finished
// to release HTTP connections and stop background refresh goroutines.
func NewClient(opts ...Option) (*Client, error) {
// ...
}
// Search returns hotels matching the given [SearchParams].
//
// Returns [ErrNotFound] if no hotels match.
// Returns [BadRequestError] if the params are malformed.
func (c *Client) Search(ctx context.Context, params SearchParams) ([]Hotel, error) {
// TODO(omar): Add pagination. Tracked in HOTEL-412.
// Use %v (not %w) here: callers must not depend on the transport error type.
resp, err := c.do(ctx, params)
if err != nil {
return nil, fmt.Errorf("search hotels: %v", err)
}
return resp.Hotels, nil
}
func ExampleClient_Search() {
client, _ := NewClient(WithTimeout(30 * time.Second))
hotels, err := client.Search(context.Background(), SearchParams{City: "Seattle"})
if err != nil {
log.Fatal(err)
}
fmt.Println(hotels[0].Name)
// Output: Grand Hotel
}The package comment sits flush against the package clause (11.3); every exported symbol carries a name-led doc comment in full sentences (11.1, 11.2); Client states its concurrency guarantee (11.13) and NewClient its cleanup duty (11.14); Search documents its returned errors with [Type] links (11.12, 11.15) and is exercised by a testable Example (11.5); the struct-field comment is an allowed fragment (11.2); the TODO has an owner and a ticket (11.9); the %v deviation is signal-boosted (11.6) and explains why, not what (11.7); the wrapped error message is a lowercase fragment (11.16).
Reasoning, step by step:
- Every exported function, type, method, constant, and variable must have a GoDoc comment. It is the only contract a caller reading
go docever sees. - Start the comment with the symbol name so the rendered documentation reads as a sentence about that symbol. An article (
A,An,The) may precede the name.
// Client executes API operations against the hotel service.
type Client struct { ... }
// A Request represents a search query sent to the hotel service.
type Request struct { ... }
// Search returns hotels matching the given search parameters.
func (c *Client) Search(ctx context.Context, params SearchParams) ([]Hotel, error) {- Unexported symbols with non-obvious behavior should follow the same convention, starting with the symbol name. This eases future exporting.
Enforcement: golint; review.
Reasoning, step by step:
- Doc comments must be complete sentences — capitalized and ending with a period. They are public prose, so they read as prose.
// ParseResponse reads the HTTP response body and decodes it into the target type.- End-of-line struct field comments may be sentence fragments, assuming the field name is the subject:
type Config struct {
Timeout time.Duration // maximum wait for a single request
Retries int // number of retry attempts
}- Sentence fragments elsewhere — short inline comments explaining a single line — have no punctuation requirements.
- Exception: a doc comment may begin with an uncapitalized identifier name when the rest remains clear, e.g.
// io.Reader wraps....
Enforcement: golint; review.
Reasoning, step by step:
- Every package must have a package comment. Place it in
doc.gofor packages with multiple files. Only one package comment per package — keep it in a single file. - The package comment must appear immediately above the
packageclause with no blank line between them.
// Package transport provides HTTP transport implementations for the SDK.
//
// The primary interface is Transport, which abstracts over the actual HTTP
// client implementation.
package transport- For
mainpackages, the comment describes the binary. Acceptable forms:
// Binary seedgenerator creates a new seed for the random source.
// Command seedgenerator creates a new seed for the random source.
// The seedgenerator command creates a new seed for the random source.- Capitalize the binary name even when its command-line form is lowercase.
- Multi-line
/* */package comments are acceptable, especially when the comment contains content the reader is expected to copy — config snippets, URLs. - Comments intended only for package maintainers — not surfaced in
go doc— go after the import block, not in the package comment.
Enforcement: golint; review.
Reasoning, step by step:
- Use named result parameters when they make the signature clearer: two or more results of the same type (disambiguating which is which); a result the caller must act on, like
cancel func(); when named results materially improve the godoc; or when the result must be set inside a deferred closure. - Avoid named results when they only add redundant repetition (
func (n *Node) Parent1() (node *Node)— just use*Node). - Avoid named results when they enable naked returns in non-trivial functions; naked returns are acceptable only in very short functions.
- Avoid named results that exist only to save declaring a local variable.
Enforcement: golangci-lint; review.
Reasoning, step by step:
- Write testable examples for all public functions. They run as tests and appear in GoDoc, so the documentation cannot drift from working code.
func ExampleClient_Search() {
client := NewClient(WithTimeout(30 * time.Second))
hotels, err := client.Search(context.Background(), SearchParams{City: "Seattle"})
if err != nil {
log.Fatal(err)
}
fmt.Println(hotels[0].Name)
// Output: Grand Hotel
}- Examples must compile and pass. They are tests, not decorations.
Enforcement: go vet; review.
Reasoning, step by step:
- When code subtly deviates from a common pattern, add a comment to boost the signal so readers do not miss the distinction.
// Good -- unusual pattern flagged for the reader
if err == nil { // if NO error
useResult(result)
}
// Good -- explains why we intentionally break convention
// Use %v (not %w) here: callers must not depend on the internal gRPC error type.
return fmt.Errorf("fetch config: %v", grpcErr)- This matters most around:
err == nilchecks, when readers expecterr != nil; assignments that look like comparisons (=vs==); negated conditions (!) that are easy to misread; and%vused where%wis expected. - If a single-character difference is structurally important, consider refactoring instead of relying on the comment.
Enforcement: review.
Reasoning, step by step:
- Inline comments explain why, not what. The code already shows what.
- If you need a "what" comment, simplify the code instead.
// Good -- explains why
// Retry because the token endpoint rate-limits aggressively.
// Bad -- restates the code
// Increment the counter by one.Enforcement: review.
Reasoning, step by step:
- Every TODO must have an owner and a tracking reference. Untracked TODOs rot.
// TODO(username): Description. Tracked in JIRA-123.Enforcement: review.
Reasoning, step by step:
- Every source file must have the license header.
- Automate enforcement in CI — do not rely on developers remembering.
Enforcement: golangci-lint; review.
Reasoning, step by step:
- Delete code instead of commenting it out. Version control remembers.
- Commented-out code is dead weight that misleads readers.
Enforcement: golangci-lint; review.
Reasoning, step by step:
- Use
[Type]syntax in GoDoc comments to link to other symbols. The Go documentation server renders these as clickable links.
// Search returns hotels matching the given [SearchParams].
// It uses the underlying [http.Client] for HTTP calls.
// Returns [ErrNotFound] if no hotels match.
func (c *Client) Search(ctx context.Context, params SearchParams) ([]Hotel, error) {- Use
[pkg.Type]for cross-package references:[context.Context],[fmt.Stringer].
Enforcement: review.
Reasoning, step by step:
- Every exported type must document its concurrency guarantees.
// Client is safe for concurrent use by multiple goroutines.
type Client struct { ... }
// TokenCache is NOT safe for concurrent use. Callers must synchronize access.
type TokenCache struct { ... }- If a type does not document this, callers must assume it is NOT safe. When in doubt, state it explicitly.
Enforcement: review.
Reasoning, step by step:
- If a type holds resources that must be released, document it on the constructor so the caller knows the obligation at the call site.
// NewClient creates a new Client. The caller must call Close when finished
// to release HTTP connections and stop background refresh goroutines.
func NewClient(opts ...Option) (*Client, error) {Enforcement: review.
Reasoning, step by step:
- Document which errors a function may return, especially sentinel errors and typed errors. A returned error is part of the contract; the signature alone does not name it.
// Translate converts an EGAID to an EG User ID.
//
// Returns [NoEGUserFoundError] if no mapping exists.
// Returns [BadRequestError] if the EGAID format is invalid.
// Returns [InvalidPrincipalJwtError] if the JWT is expired or malformed.
func (t *Translator) Translate(ctx context.Context, egaid string, jwt string) (string, error) {Enforcement: review.
Reasoning, step by step:
- Error messages are lowercase, carry no terminal punctuation, and describe what failed.
| Good | Bad |
|---|---|
"authenticate with token endpoint" |
"Failed to authenticate with the token endpoint!" |
"parse response body" |
"Error: could not parse the response body." |
"open config file" |
"Unable to open configuration file" |
- Error messages are fragments that compose through wrapping. They are not user-facing sentences.
Enforcement: golangci-lint; review.
- Error wrapping with
%wand lowercase message style: chapter 03. - Concurrency guarantees and goroutine lifecycles: chapter 04.
- Constructor and API surface conventions: chapter 05.
Examplefunctions as tests: chapter 06.- Package layout and
doc.goplacement: chapter 07. Closeand resource release obligations: chapter 10.