In Go, documentation isn't a separate artifact you maintain on a wiki. It lives next to the code as plain comments, and the toolchain renders it into the API reference at pkg.go.dev automatically. Write the comments well and your package gets a complete documentation site. Skip them and your package looks abandoned to anyone browsing pkg.go.dev. This chapter covers the conventions, the rendering rules, the link syntax, runnable example functions, and the tools you'll use to read and publish docs.
A Go package's documentation is the comment block immediately above each exported identifier. There's no separate "docs file" or template. When someone visits pkg.go.dev/example.com/shop/cart, they're reading the comments you wrote in cart.go. The same comments show up in go doc cart.Cart on the terminal, in IDE hover tooltips, and in any third-party tool that consumes Go's documentation format.
That single source has consequences. Treat doc comments like part of your API. Changing a function's behavior means updating the comment. Renaming a parameter means updating the comment. Adding a new error return means updating the comment. The community expects this because the comment is what they read before deciding whether to import your package.
Here's the bare minimum for a cart package:
First, the package itself gets a comment block. Second, every exported identifier (Cart, New, AddItem) gets a comment. Third, each comment starts with the identifier's name. That last rule is what makes the docs read like prose: "Cart represents..." flows naturally into the doc page, while "Represents a customer's cart..." reads like a stub.
Skipping doc comments has no runtime cost, but it has a real adoption cost. Packages without docs rank poorly on pkg.go.dev's search and look unmaintained to anyone evaluating dependencies.
The rule "start the comment with the identifier name" exists because of how Go's tooling renders summaries. The first sentence (up to the first period followed by a space) becomes the synopsis line in pkg.go.dev's index, in go doc's short output, and in IDE quick-info popups. If your comment doesn't start with the name, the synopsis reads awkwardly out of context.
Effective Go (the official style guide at go.dev/doc/effective_go) reinforces this rule and it's checked by golint and revive. Once you've written a few packages this way, the form becomes automatic.
The convention extends to every exported identifier, not just functions. Types, methods, variables, constants, and even error sentinels follow it:
Each comment starts with the identifier. Each first sentence is something pkg.go.dev can show on its own, without surrounding context. This is the whole rule.
Package-level documentation is the comment block immediately above the package clause. For a small package, you put it above the package line in one of your source files (often the most central one). For a larger package, the convention is to put it in a dedicated doc.go file that contains nothing else.
That whole block becomes the landing page on pkg.go.dev for example.com/shop/payment. The first sentence ("Package payment processes credit card charges...") shows in search results and in any index that links to the package. Subsequent paragraphs render as the full overview.
doc.go is a convention, not a requirement. The compiler doesn't care which file holds the package comment, as long as exactly one file has one. Putting it in doc.go keeps it out of your way when you're editing real code and makes it easy to find when you're updating the docs. Many standard library packages do this: see net/http, encoding/json, database/sql.
A typical doc.go:
The # Configuration and # Example lines aren't decorative. They're headings, and Go's renderer treats them specially. The next section covers how that works.
Before Go 1.19, doc comments were rendered as plain text with a few special rules (blank lines became paragraph breaks, indented blocks became code blocks). Go 1.19 added a small markdown-like syntax for headings, links, and lists. The change was deliberate: the team wanted richer docs without inventing a new markup language or pulling in a full markdown parser.
The rules are intentionally narrow. Anything not on this list renders as plain text.
| Construct | Syntax | Renders as |
|---|---|---|
| Paragraph | Blank line between blocks | Paragraph break |
| Heading | Line starting with # then space | <h3> heading |
| Bullet list | Lines starting with - or * | <ul> list |
| Numbered list | Lines starting with 1. etc. | <ol> list |
| Code block | Indented by tab or spaces | <pre> block |
| Inline code | Backticks aren't special | Plain text |
| Doc link | [Name] or [pkg.Name] | Hyperlink |
Here's a comment that uses several at once:
pkg.go.dev renders that with a real heading for "Failure Modes", a bulleted list of error types, a numbered list of failure categories, and clickable links to ErrNetworkTimeout, ErrProviderUnavailable, ErrRateLimited, and Processor.Refund. None of that requires a separate documentation generator. It's all in the comment.
The indentation rules for code blocks deserve a mention. A line indented by a tab or by spaces beyond the comment's normal column becomes a code block. That's how you embed examples inside doc comments:
The indented lines render as a monospaced code block. Don't wrap them in backticks or markdown fences. Go's comment format doesn't recognize either.
The square-bracket link syntax is how you reference other identifiers from inside a doc comment. The renderer turns each link into a hyperlink on pkg.go.dev and into a clickable reference in IDE tooltips.
| Target | Syntax | Example |
|---|---|---|
| Same-package function or type | [Name] | [Cart], [New] |
| Method on a same-package type | [Type.Method] | [Cart.AddItem] |
| Field on a same-package type | [Type.Field] | [Cart.Total] |
| Other-package identifier | [pkg.Name] | [fmt.Stringer], [errors.Is] |
| Other-package method | [pkg.Type.Method] | [sql.DB.Query] |
| Full URL | [label] then // [label]: url | See below |
Here's a real example weaving several of these together:
Each [Name] becomes a link on pkg.go.dev that jumps to the relevant identifier's documentation. [errors.Is] links to pkg.go.dev/errors#Is. [order.Order] links to your own order package. [Cart.Validate] links to the Validate method on the same Cart type.
For external URLs that aren't Go identifiers, use the labeled-link form:
The [label]: url line at the bottom of the comment defines where the link points. The label can be any text; you can have several of them in one comment. pkg.go.dev renders the label as the clickable text and uses the URL as the destination.
Doc links don't slow anything down at runtime or build time. The only "cost" is that broken links (a method that was renamed, a package that was deleted) show up as plain text instead of hyperlinks. Run go vet and check pkg.go.dev after refactors.
Example functions are testable, runnable documentation. They look like tests, live in _test.go files, and go test executes them with the same machinery, but their primary purpose is to appear on pkg.go.dev as runnable code snippets users can click to expand.
The naming convention encodes what the example documents:
| Example function name | Documents |
|---|---|
Example | The whole package |
ExampleCart | The Cart type or Cart function |
ExampleCart_AddItem | The AddItem method on Cart |
ExampleCart_AddItem_negative | An extra variant of Cart.AddItem (the suffix is the variant name) |
ExampleNew | The New function |
The underscore between type and method is required. The optional suffix after a second underscore lets you have multiple examples for the same identifier, each highlighting a different scenario.
A package-level example:
That's a complete program inside a function. go test compiles it, runs it, captures its standard output, and compares the output against the // Output: comment at the bottom. If the output doesn't match, the example fails like any other test.
The // Output: comment is what makes the example a test. Without it, go test still compiles the function (catching syntax errors and broken imports) but doesn't run it. With it, the comparison is exact, including whitespace and newlines.
A method-specific example:
On pkg.go.dev, this snippet appears collapsed under the AddItem method, with a "Run" button that executes it in the playground. Readers see exactly how to call the method, what the output looks like, and they can edit and re-run it without leaving the docs page.
When the output order isn't deterministic (ranging over a map, for example), use // Unordered output: instead. The runner sorts both the actual output and the expected output before comparing, so order-independent outputs still pass.
Without // Unordered output:, this example would fail intermittently because map iteration order is randomized. With it, the runner compares the sorted lines and accepts either order.
The full pipeline from source code to rendered documentation has a few moving parts. Knowing the pieces helps you predict what pkg.go.dev will show.
The go/doc package in the standard library is what reads your source files, extracts comments above exported identifiers, and produces a structured representation. Every documentation tool, go doc on the terminal, pkgsite for self-hosting, pkg.go.dev for the public site, builds on that same parser. There's no separate doc compiler.
When you publish a module, pkg.go.dev pulls the source from the proxy (proxy.golang.org), runs the doc parser, runs the examples to verify their output, renders the result, and indexes it. The whole pipeline is automatic. You don't push to pkg.go.dev. You don't register your package. You publish a version tag to your module's repository, and within a few minutes the docs appear.
go doc is the terminal interface to the same data pkg.go.dev shows. Use it when you're offline, when you want a quick reminder of a function signature, or when you're working in a codebase without a browser handy.
The output shows the signature and the doc comment, nothing else. For more detail:
That dumps the full documentation for the entire fmt package, every exported identifier with its comment, plus all examples. It's overwhelming for fmt. For a small package like the cart package above, it's the exact view of what pkg.go.dev would show.
A few patterns that come up daily:
| Command | What it shows |
|---|---|
go doc | The current package's docs |
go doc cart | The cart package's docs |
go doc cart.Cart | Just the Cart type and its methods |
go doc cart.Cart.AddItem | Just the AddItem method |
go doc -all cart | Everything, including unexported names if -u is added |
go doc -src cart.Cart | The source code for the Cart declaration |
go doc works offline and works on packages you haven't published. If you're inside a module, go doc./internal/payment shows your local payment package's docs even though nothing has been pushed anywhere.
go doc -all for a large package like net/http prints thousands of lines. Pipe to a pager (go doc -all net/http | less) or scope to a specific identifier (go doc net/http.Client) when you only need part of it.
pkgsite is the program that powers pkg.go.dev itself. The Go team open-sourced it, so you can run the same site against your private modules:
That starts a local web server on port 8080 that serves docs for every module in your local Go cache. Browse to http://localhost:8080/example.com/shop/cart and you get the same rendering pkg.go.dev would show, including examples, links, and search.
This is what most companies use for internal documentation. Private modules never reach the public proxy, so they never appear on pkg.go.dev. A self-hosted pkgsite solves that without requiring any code changes: write the same doc comments, get the same rendering.
There's also an older tool called godoc (no site suffix) that was the original self-hosted documentation server. It still works, but pkgsite is the modern replacement and matches what the public site does. New projects should use pkgsite.
pkg.go.dev shows docs for every published version of your module. The version selector at the top of the page lets readers switch between v1.0.0, v1.1.0, v2.0.0, and so on. Each version pulls its docs from that version's source, which means docs for older releases stay correct even as you change the comments in newer code.
Publishing a version is one command:
Within a few minutes (sometimes seconds), pkg.go.dev/example.com/shop/cart@v1.2.0 appears with the docs at that tag. The version without @version always points to the latest non-prerelease, which is what most readers see by default.
The pkg.go.dev badge is the small image you embed in your README:
That renders as a clickable badge linking to your package's documentation page. Most Go libraries display it near the top of their README so visitors can jump to the API reference without scrolling through prose.
Search ranking on pkg.go.dev is influenced by a handful of signals: import count (how many other modules import yours), license clarity (an explicit LICENSE file helps), documentation completeness (more comments and examples rank higher), and recency (active modules outrank dormant ones). You can't game the ranking, but writing good docs and shipping releases regularly is what the algorithm rewards.
Two special comment patterns get rendered specially on pkg.go.dev.
A // BUG(author): comment at package scope lists known bugs in a dedicated "Bugs" section at the bottom of the package's docs page:
This appears under "Bugs" with the author label, the message, and a link to the file. The pattern comes from the standard library, where is it in packages like time (// BUG(rsc):...).
The Deprecated: marker is a paragraph inside a regular doc comment that signals "don't use this, use the alternative":
pkg.go.dev renders deprecated identifiers with a strikethrough and a warning banner. IDEs like GoLand and VS Code (with the Go extension) show a strikethrough in code that calls deprecated functions. The Deprecated: keyword must appear at the start of a paragraph, with a colon after it. That exact form is what the tooling recognizes.
The Refund page on pkg.go.dev now shows a clear "Deprecated" banner with the explanation and a link to the replacement. Callers know to migrate, and IDE warnings nudge them toward the new API.
A README.md at the module root and your doc comments serve different purposes. They overlap a little, but each has a job that the other can't do.
| Lives in | Purpose | Audience |
|---|---|---|
README.md | Why the project exists, install instructions, examples for tooling and CI, badges, contributing guide | Someone evaluating the project |
| Doc comments | The exact behavior of every exported identifier | Someone using the package |
Both render on pkg.go.dev. The README appears at the top of the package page. The doc comments appear below it, organized by identifier. A good split puts the elevator pitch and quick-start in the README and the API reference in the comments. Don't repeat the comments inside the README; readers will see them on the same page.
A minimal README structure that complements good doc comments:
The README is a few hundred words at most. The full API lives in the comments. Readers who land on pkg.go.dev see the README first, get oriented, and then examine the API reference below it.
Here's the full picture: a payment package with package docs, type docs, method docs, doc links, and a runnable example. This is what a well-documented Go package looks like end to end.
payment/doc.go:
payment/processor.go:
payment/example_test.go:
Run go test./payment and the example executes; its output must match exactly. Run go doc payment.Processor.Charge and the comment appears on the terminal. Push a tag and pkg.go.dev/example.com/shop/payment shows the full rendered docs with clickable links between Processor, Charge, Refund, ErrDeclined, and New.
Nothing in those three files is documentation tooling. It's all just Go code and comments. That's the point of how Go handles docs: there's no separate system to maintain.
A handful of mistakes show up over and over.
Comment doesn't start with the identifier name.
The first form makes the synopsis on pkg.go.dev read "Adds an item to the cart." with no context. The second form reads "AddItem adds qty units...", which is self-contained.
Comment is separated from the declaration by a blank line.
The doc parser walks backward from each declaration looking for a comment block. A blank line ends the block. With the blank line, the comment is orphaned and doesn't appear on the type's doc page.
Comment is on the wrong side of the declaration.
Trailing comments are valid Go but they don't become doc comments. Only the comment block directly above the declaration counts.
Example function has no `// Output:` line.
Without // Output:, go test compiles the example (catching broken imports) but doesn't run it. The example still appears on pkg.go.dev, but readers don't get the "Run" button, and you don't get a regression test if the function's behavior changes.
9 quizzes