Documentation and Doc Comments

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Go 1.27 (go1.27.2) and was checked on 2026-10-18. In Go, documentation is not a separate file format or a markup language bolted onto the source: it is the comment written directly above a declaration. The same text feeds go doc in the terminal, the editor hover from gopls and the web pages at pkg.go.dev, so there is one place to write it and one syntax to learn. This page covers that syntax, the commands that read it and the executable examples that complete it. It is the last page of the tooling group, after Formatting, vet, fix and Static Analysis, which explains the gofmt that also reformats doc comments, and it builds on Testing, which explains how example functions are run. Every example documents the shelf library-catalogue module used throughout this section.

What a doc comment is

A doc comment is a comment that appears immediately before a top-level package, const, func, type or var declaration, with no blank line between the comment and the declaration. Comments on exported struct fields, interface methods and the specs inside a grouped const/var block are also treated as documentation of that item. A comment separated from the declaration by a blank line is an ordinary comment and is invisible to the tools. Only exported (capitalized) names appear in the documentation by default; whether a name is exported is decided by the rules on Program Structure and Lexical Elements and declarations are scoped as in the spec’s Declarations and scope.

The convention, from Effective Go: Commentary, is that the comment is a complete sentence that starts with the name of the thing it describes (ParseISBN validates s and ...). The first sentence is the summary: go doc -short, package listings on pkg.go.dev and editor completion show it alone, so it has to stand on its own.

A package comment starts Package name ... and lives in one file (conventionally doc.go); a command’s starts Command name ... (or the program name) and holds the usage; a type’s comment says what an instance represents and whether it is safe for concurrent use; a function’s says what it returns, which errors it can return and how arguments are interpreted; and constants and variables take a comment on the declaration or on the whole group.

The shelf package documented

The package comment of catalog lives in its own file, doc.go. It uses a heading, a list, a code block, doc links and a plain URL, all described in the sections below. If several files of a package carry a package comment, the tools concatenate them, which is why a single doc.go is the usual choice.

// Package catalog models a library catalogue: books, their authors and the
// loans made against them.
//
// # Overview
//
// A [Store] persists [Book] values and is safe for concurrent use. Create
// one with [NewMemStore]; parse identifiers with [ParseISBN].
//
// # Lending
//
// A book moves through the states listed in [LoanState]:
//
//   - [Available]: on the shelf
//   - [OnLoan]: lent to a member
//   - [Overdue]: past its due date
//
// Errors are reported with the sentinel values [ErrNotFound] and
// [ErrInvalidISBN]; test for them with [errors.Is].
//
// # Example
//
// Looking a book up by ISBN:
//
//	s := catalog.NewMemStore()
//	b, err := s.Get(isbn)
//	if errors.Is(err, catalog.ErrNotFound) {
//		// not catalogued yet
//	}
//
// See https://example.com/shelf for the project home page.
package catalog

Doc comment syntax

The syntax is deliberately tiny and is described in Go Doc Comments. There are no tags, no @param and no HTML. Introduced in Go 1.19, links, lists and headings replaced the older convention of recognizing headings and code by heuristics (Go 1.19 release notes).

Element How to write it

Paragraph

Consecutive lines; a blank comment line (// alone) separates paragraphs. Line breaks inside a paragraph are not kept.

Heading

A line starting with `# ` and followed by a blank line, on its own paragraph. Only one level exists.

Code block

A span of lines indented (tab recommended) relative to the text. Rendered preformatted, not highlighted.

List

Indented lines starting with -, *, + or • (bullets) or a number followed by . or ) (numbers). gofmt normalizes them to a dash for bullets and N. for numbers.

Doc link

[Name], [Name.Method], [pkg.Name], [pkg.Name.Method] or [*pkg.Name]: a link to a declaration.

Link

[text] with a later [text]: URL line (a link definition); bare http:// and https:// URLs also link.

Directive

//go:name lines at the end of a doc comment are not part of the rendered text; gofmt moves them there.

Note

// BUG(who): text anywhere in the file; collected under a BUGS heading.

gofmt rewrites doc comments

Because the rendering rules are precise, gofmt reformats doc comments into their canonical form: it adds the # to a heading, normalizes list markers and indentation, indents code blocks with a tab and puts blank comment lines around them. Run on a deliberately untidy package comment (lines starting with - are what it removes, + what it writes; real output, and gofmt -w applies them):

$ gofmt -d messy.go
diff messy.go.orig messy.go
--- messy.go.orig
+++ messy.go
@@ -1,12 +1,13 @@
 // Package messy shows how gofmt rewrites doc comments.
 //
-// Overview
+// # Overview
 //
 // Lists with assorted markers:
 //
-//   * first item
-//   + second item
+//   - first item
+//   - second item
 //
 // Code is a block indented by spaces:
-//     x := 1
+//
+//	x := 1
 package messy

Two details matter. The one-line heading written the old way (a short capitalized line without punctuation) was promoted to # Overview automatically. And a list item or code block written flush with the margin is not recognized: * text at column zero stays an ordinary paragraph, so lists and code have to be indented first.

Seeing what the parser sees

The standard library exposes the exact parser and printers that go doc and pkgsite use as go/doc/comment. This program parses a comment, resolves doc links with a fake LookupPackage and LookupSym and prints the HTML rendering; it is the quickest way to check how a tricky comment will render.

package main

import (
	"fmt"
	"go/doc/comment"
	"os"
)

const src = `Lists and links.

# Overview

Lists:

  - bullet one
  - bullet two

Then:

  1. numbered one

Not a list:

* flush-left marker

See [Book], [catalog.Store], [*bytes.Buffer] and [Go site].

[Go site]: https://go.dev
`

func main() {
	var p comment.Parser
	p.LookupPackage = func(name string) (string, bool) {
		return "example.com/shelf/" + name, name == "catalog"
	}
	p.LookupSym = func(recv, name string) bool { return recv == "" && name == "Book" }
	doc := p.Parse(src)

	var pr comment.Printer
	pr.DocLinkURL = func(l *comment.DocLink) string {
		return "https://pkg.go.dev/" + l.ImportPath + "#" + l.Name
	}
	fmt.Println("--- HTML")
	os.Stdout.Write(pr.HTML(doc))
}
--- HTML
<p>Lists and links.
<h3 id="hdr-Overview">Overview</h3>
<p>Lists:
<ul>
<li>bullet one
<li>bullet two
</ul>
<p>Then:
<ol>
<li>numbered one
</ol>
<p>Not a list:
<p>* flush-left marker
<p>See <a href="https://pkg.go.dev/#Book">Book</a>, <a href="https://pkg.go.dev/example.com/shelf/catalog#Store">catalog.Store</a>, <a href="https://pkg.go.dev/bytes#Buffer">*bytes.Buffer</a> and <a href="https://go.dev">Go site</a>.

Source: go/doc/comment

Reading the output: [Book] became a link because LookupSym said the package has a Book; [catalog.Store] and [*bytes.Buffer] were resolved to import paths; the heading became an <h3> with an anchor; and [Go site] used the link definition. The flush-left star stayed text, as above.

A doc link [Name] names a declaration in the current package; [pkg.Name] names one in an imported package, written with the name the file imports it under. A leading star ([*bytes.Buffer]) is allowed and is ignored for linking. Links to methods and fields use [Type.Method] and [Type.Field] (the shelf package uses [Store.Get]). A bracketed word that does not resolve is simply printed in brackets, which is also how a slice index in prose survives. A link text needs a definition line; the definitions may be anywhere in the comment but are conventionally the last paragraph, as in the earlier example.

Lists and code blocks

Lists are rendered from indented blocks. A list ends at the first line that is not indented, so a numbered list placed directly after a bullet list becomes part of the last bullet instead of starting a new list. Separate the two with a paragraph, as the program above does. Code blocks are for display: they are not highlighted, not compiled and not checked, which is one reason executable examples (below) are preferred for anything that must stay correct.

Notes and directives

A marker such as BUG(alberto): text is collected by go doc -all (and by pkgsite) under a separate heading. A //go: directive at the end of a doc comment is not part of the rendered text. Both are real behaviours of go1.27.2:

// Package notes shows markers and directives in comments.
package notes

// BUG(alberto): Renew ignores the member's loan limit.

// Renew extends a loan by one week.
//
// This line is documentation.
//
//go:noinline
func Renew() {}
package notes // import "example.com/notes"

Package notes shows markers and directives in comments.

FUNCTIONS

func Renew()
    Renew extends a loan by one week.

    This line is documentation.


BUG: Renew ignores the member's loan limit.

Deprecated and common mistakes

A paragraph that begins with Deprecated: marks the declaration (or the whole package, when it is in the package comment) as deprecated. The paragraph should say what to use instead. Tools notice it: go doc prints it, pkg.go.dev marks deprecated identifiers, gopls and staticcheck report uses of deprecated names, and go vet does not. The paragraph must start the paragraph and be separated by a blank comment line from the text above it.

// Package detached shows comments that are not doc comments.
package detached

// Renew extends a loan.

func Renew() {}

//Reserve holds a book.
//
//Deprecated: use Return.
func Reserve() {}

// Close ends the session.
//
// Deprecated: Use Return instead.
func Close() {}
$ go doc -all .
package detached // import "example.com/detached"

Package detached shows comments that are not doc comments.

FUNCTIONS

func Close()
    Close ends the session.

    Deprecated: Use Return instead.

func Renew()
func Reserve()
    Reserve holds a book.

    Deprecated: use Return.

This listing demonstrates several rules. Close is correctly deprecated. Renew is detached by a blank line, so go doc prints no text for it, and no tool complains. Reserve was written without a space after //, which gofmt repairs on save; go doc still accepts it and, in this build, a lower-case use Return after Deprecated: too. A Deprecated: in the middle of a sentence is plain text and marks nothing. A go.mod can deprecate a whole module the same way, with a // Deprecated: paragraph above the module line (Go modules reference: module directive); that is covered on Workspaces, Versioning and Publishing.

Other mistakes to avoid: starting the comment with something other than the name (the web page still works, but the sentence reads badly); documenting every private helper at length while leaving the exported API bare; putting usage examples in comments instead of Example functions; and using HTML or Markdown, which are shown literally except for the constructs in the table. Packages are expected to document each exported name, but nothing enforces it.

Commands

For package main the comment describes the program, not an API. Conventionally it starts Command name ... and contains the usage as an indented code block. Plain go doc ./cmd/shelf prints only that comment; the exported identifiers of a command are hidden unless -cmd is given, because they are not importable. With -cmd the package clause and func Version() string appear as well:

// Command shelf searches, lends and returns books.
//
// Usage:
//
//	shelf [-store file] command [arguments]
//
// The commands are:
//
//	search  find books by title
//	lend    lend a book to a member
//	return  return a lent book
//
// Use "shelf help command" for more information about a command.
package main

import "fmt"

// Exported symbols of a command are hidden by go doc unless -cmd is given.
func Version() string { return "1.0" }

func main() { fmt.Println("shelf", Version()) }
$ go doc -cmd ./cmd/shelf | tail -n 3
package main // import "example.com/shelf/cmd/shelf"

Command shelf searches, lends and returns books.

Usage:

    shelf [-store file] command [arguments]

The commands are:

    search  find books by title
    lend    lend a book to a member
    return  return a lent book

Use "shelf help command" for more information about a command.

func Version() string

The go doc command

go doc prints the documentation of a package, a symbol, a method or a struct field, then a one-line summary of the items beneath it. With no argument it documents the package in the current directory. Arguments are matched loosely: lower-case letters match either case, a package may be given by a suffix of its path (json for encoding/json), and the first match in a breadth-first search wins (standard library first). The output of go help doc for go1.27.2 lists these flags:

Flag Effect

-all

Show all the documentation for the package (every exported item with its full comment).

-c

Respect case when matching symbols.

-cmd

Treat a command like a regular package and show its exported symbols.

-ex

Include executable examples (new in Go 1.27).

-http

Serve HTML docs over HTTP (new in Go 1.25).

-short

One-line representation per symbol; cannot be combined with -all.

-src

Show the full source of the declaration, including the function body.

-u

Show unexported symbols, methods and fields too.

Go 1.27 also lets the first argument carry a module version (go doc example.com/pkg@v1.2.3), so a package can be read without adding it to go.mod. Source: cmd/go: Show documentation for package or symbol and Go 1.27 release notes: go doc

Package, symbol, method and field

Here are the everyday forms against the shelf module: a function (package and symbol as two arguments) and a deprecated function. go doc ./catalog alone prints the package comment and a list of every exported item, go doc ./catalog.LoanState the type, its constants and methods, and go doc ./catalog.Book.Title one struct field, with the other fields elided.

$ go doc ./catalog ParseISBN
package catalog // import "example.com/shelf/catalog"

func ParseISBN(s string) (ISBN, error)
    ParseISBN validates s and returns it as an ISBN. Hyphens are ignored.

    It returns an error wrapping ErrInvalidISBN if s does not contain 13 digits
    or the check digit is wrong.
$ go doc ./catalog.Find
package catalog // import "example.com/shelf/catalog"

func Find(m *MemStore, title string) (Book, bool)
    Find looks a book up by title.

    Deprecated: Use Store.Get with an ISBN; titles are not unique.

Source: cmd/doc

The doc links in the comment ([ISBN], [ErrInvalidISBN]) are printed as plain names in the terminal. A symbol listing shows grouped constants as const Available LoanState = iota ..., without their comments; -all shows them.

-short, -all, -src and -u

$ go doc -short ./catalog
var ErrInvalidISBN = errors.New("catalog: invalid ISBN")
var ErrNotFound = errors.New("catalog: book not found")
type Author struct{ ... }
type Book struct{ ... }
    func Find(m *MemStore, title string) (Book, bool)
type ISBN string
    func ParseISBN(s string) (ISBN, error)
type LoanState int
    const Available LoanState = iota ...
type MemStore struct{ ... }
    func NewMemStore() *MemStore
type Store interface{ ... }
$ go doc -src ./catalog.NewMemStore
package catalog // import "example.com/shelf/catalog"

// NewMemStore returns an empty [MemStore].
func NewMemStore() *MemStore { return &MemStore{books: map[ISBN]Book{}} }

-src prints the comment and the real source, so it doubles as a quick way to read a standard-library implementation (go doc -src strings.Cut). -all adds every exported item’s full comment (with constants and their comments); -u reveals the unexported fields of MemStore, here the mutex and the map, which are otherwise filtered out.

-ex, examples and pkg@version

-ex (Go 1.27) lists the executable examples of a package or symbol. Passing an example’s own name (go doc ./catalog.ExampleMemStore_Get) prints its source as a runnable main program followed by its Output: line; in go1.27.2 it printed that program twice in this environment, which is best treated as a quirk of this build and not as documented behaviour. The listing form appends the example names under the symbol:

$ go doc -ex ./catalog.ParseISBN
package catalog // import "example.com/shelf/catalog"

func ParseISBN(s string) (ISBN, error)
    ParseISBN validates s and returns it as an ISBN. Hyphens are ignored.

    It returns an error wrapping ErrInvalidISBN if s does not contain 13 digits
    or the check digit is wrong.

    func ExampleParseISBN()
    func ExampleParseISBN_invalid()

The two other newer features need the network, because they fetch code from outside the local module. With GOPROXY=off (as used for every example on this page) they fail with these real messages; they were not run successfully, so what follows is what the release notes state, not something verified here.

$ go doc golang.org/x/tools/go/analysis@v0.52.0
doc: golang.org/x/tools/go/analysis@v0.52.0: module lookup disabled by GOPROXY=off
$ go doc strings@go1.27.2 Cut
doc: strings@go1.27.2: argument must not be a package in the standard library
$ go doc -http ./catalog
doc: go: golang.org/x/pkgsite/cmd/internal/doc@v0.0.0-20260722200430-7c7eca789259: module lookup disabled by GOPROXY=off
  • pkg@version looks the module up (here the lookup was refused by GOPROXY=off) and documents that version. It is rejected for standard-library packages, as the second message shows.

  • -http (Go 1.25) starts a local documentation server showing the page for the argument, using pkgsite code that the command downloads the first time, which is why it needs the proxy. See Go 1.25 release notes for the announcement.

pkg.go.dev, pkgsite and the old godoc

https://pkg.go.dev/ renders the same comments for every public module, with versions, licenses, imports and examples. Publishing a tagged version (see Workspaces, Versioning and Publishing) is enough; the site fetches it from the module proxy, and a request to the page of a version triggers the fetch (pkg.go.dev: About). The software behind the site is pkgsite, which can also be run locally against a private module tree; go doc -http uses it. The older godoc web server (golang.org/x/tools/cmd/godoc) was deprecated, with the reason that it cannot select the version of a package it displays, and v0.36.0 is the last golang.org/x/tools release that contains it; use go doc in the terminal, gopls hover in the editor and pkgsite for HTML.

pkgsite adds, on top of the comment text: anchors from headings, an index of the declarations, deprecation notices, constructors grouped under their types, and each example shown next to what it documents. All of it is derived from the comment and example files, so no extra configuration is needed.

Examples as documentation

An example function is a function in a _test.go file whose name starts with Example. It is compiled with the tests, and if it ends with an // Output: comment, go test runs it and compares what it prints with that comment. The tools also show it in the documentation, attached to the item named in the function name: ExampleParseISBN to the function, ExampleMemStore_Get to the method Get of MemStore, and a lower-case suffix (ExampleParseISBN_invalid) for an additional example of the same item. Example alone documents the package, and Example_suffix a variation of it. The examples live in the external test package (catalog_test) so that they read like code that imports the package, which is what a user would write.

package catalog_test

import (
	"errors"
	"fmt"

	"example.com/shelf/catalog"
)

func ExampleParseISBN() {
	isbn, err := catalog.ParseISBN("978-0-13-468599-1")
	fmt.Println(isbn, err)
	// Output: 9780134685991 <nil>
}

func ExampleParseISBN_invalid() {
	_, err := catalog.ParseISBN("123")
	fmt.Println(errors.Is(err, catalog.ErrInvalidISBN))
	// Output: true
}

func ExampleMemStore_Get() {
	s := catalog.NewMemStore()
	_, err := s.Get("9780134685991")
	fmt.Println(err)
	// Output: catalog: book not found
}
$ go test -v -run Example ./catalog
=== RUN   ExampleParseISBN
--- PASS: ExampleParseISBN (0.00s)
=== RUN   ExampleParseISBN_invalid
--- PASS: ExampleParseISBN_invalid (0.00s)
=== RUN   ExampleMemStore_Get
--- PASS: ExampleMemStore_Get (0.00s)
PASS
ok  	example.com/shelf/catalog	(cached)

An example cannot drift out of date silently: if ParseISBN changes what it returns, go test fails. An example without an // Output: comment is only compiled, never run, which suits examples that need the network or a clock. // Unordered output: compares lines without regard to order, which suits map iteration. The testing mechanics, -run Example and the naming rules checked by go vet (its tests analyzer reports an example that refers to a name that does not exist) are explained on Testing.

Writing guidance that tools cannot check

  • Say what the code promises, not how it works: concurrency safety, whether a returned slice may be modified, which sentinel errors can be returned (ErrNotFound) and the meaning of the zero value.

  • Keep lines about 80 columns wide, as the standard library does; gofmt does not rewrap prose.

Where the books are outdated

  • Bodner, chapter 9 (modules and packages), describes go doc and package comments as they were in 2021. Since then Go 1.19 added links, lists and headings (and gofmt reformatting) to the syntax, and Go 1.25 and 1.27 extended go doc with -http, pkg@version and -ex. The chapter’s publishing advice still sends readers to pkg.go.dev, which remains right.

  • Vivien, chapter 1, shows the godoc binary (godoc -http=:6060) for local documentation. That program is obsolete: documentation is read with go doc or pkgsite, as above.

  • Both books recommend golint to check that comments start with the name. golint is archived and no replacement in the standard tools enforces it; community linters (staticcheck, golangci-lint, revive) can, and gopls shows deprecations. See Formatting, vet, fix and Static Analysis.

  • Neither book covers Deprecated: paragraphs, doc links, go/doc/comment or testable examples as documentation.