Skip to content
gohacks.dev
Basics

Doc Comments in Go and How Godoc Reads and Renders Them

A comment directly above a declaration becomes its documentation. See the sentence form, headings, links, code blocks, Deprecated notes and the go doc output.

7 min read
Summarize in AI
Flat illustration of an editor window full of code.

Go has no separate documentation language. The text above a declaration is the documentation, and the same comment feeds go doc in your terminal, the hover text in your editor and every page on pkg.go.dev. That tight loop is why Go libraries tend to be documented: the cost of writing a doc comment is writing a sentence where the code is. The format has a few rules, and they are small. This how-to shows a complete example with the rendered output, the features added in Go 1.19, and the places where documentation quietly fails. It belongs to the Go basics path, builds on naming conventions and packages, and pairs with gofmt, which also reformats doc comments.

Tested with Go 1.25.6, macOS on Apple Silicon (arm64)

A package with every feature

Here is a small package with a package comment, a heading, a link, a list, a code block and a deprecation. After it comes the output of go doc -all, which is how a user sees it:

shapes.goGo
// Package shapes computes the area of simple figures.
//
// A [Rect] and a [Circle] both satisfy [Shape], so a mixed list can be
// summed without knowing the concrete types.
//
// # Units
//
// Lengths are unitless. Areas are in the square of whatever unit the caller
// used, so mixing meters and feet gives a meaningless total.
//
// # Limits
//
//   - Negative lengths are not rejected.
//   - Results are float64, so very large totals lose precision.
package shapes

import "math"

// Shape is implemented by every figure in this package.
type Shape interface {
	// Area returns the area of the shape.
	Area() float64
}

// Rect is an axis-aligned rectangle.
//
// The zero value is an empty rectangle with area 0.
type Rect struct {
	W, H float64 // width and height
}

// Area returns the width times the height.
func (r Rect) Area() float64 { return r.W * r.H }

// Circle is a circle of radius R.
type Circle struct{ R float64 }

// Area returns pi times R squared.
func (c Circle) Area() float64 { return math.Pi * c.R * c.R }

// Total returns the sum of the areas of the given shapes. It returns 0 when
// the list is empty. For example:
//
//	total := shapes.Total(shapes.Rect{W: 2, H: 3}, shapes.Circle{R: 1})
func Total(list ...Shape) float64 {
	sum := 0.0
	for _, s := range list {
		sum += s.Area()
	}
	return sum
}

// Sum adds up areas.
//
// Deprecated: Use [Total], which accepts a variadic list.
func Sum(list []Shape) float64 { return Total(list...) }
Text
package shapes // import "example.test/lab"

Package shapes computes the area of simple figures.

A Rect and a Circle both satisfy Shape, so a mixed list can be summed without
knowing the concrete types.

# Units

Lengths are unitless. Areas are in the square of whatever unit the caller used,
so mixing meters and feet gives a meaningless total.

# Limits

  - Negative lengths are not rejected.
  - Results are float64, so very large totals lose precision.

FUNCTIONS

func Sum(list []Shape) float64
    Sum adds up areas.

    Deprecated: Use Total, which accepts a variadic list.

func Total(list ...Shape) float64
    Total returns the sum of the areas of the given shapes. It returns 0 when
    the list is empty. For example:

        total := shapes.Total(shapes.Rect{W: 2, H: 3}, shapes.Circle{R: 1})


TYPES

type Circle struct{ R float64 }
    Circle is a circle of radius R.

func (c Circle) Area() float64
    Area returns pi times R squared.

type Rect struct {
	W, H float64 // width and height
}
    Rect is an axis-aligned rectangle.

    The zero value is an empty rectangle with area 0.

func (r Rect) Area() float64
    Area returns the width times the height.

type Shape interface {
	// Area returns the area of the shape.
	Area() float64
}
    Shape is implemented by every figure in this package.

Compare each piece of source with what the terminal printed. The package comment is first, with the brackets around [Rect] and [Circle] removed, because a text terminal cannot link. The # Units and # Limits lines keep their hash signs, and the list is indented. Each declaration shows its comment below its signature, grouped under FUNCTIONS and TYPES. The indented line in Total's comment is a code block, and the deprecated Sum carries its notice in the same paragraph. On pkg.go.dev the same text becomes real headings, links to Rect, Circle and Shape, and a deprecation marker on Sum. The rendering differs by tool and the source is identical, so write for the plain text first and treat the markup as a bonus.

The rules, one at a time

Start with the name. The first sentence of a doc comment begins with the identifier it documents: Total returns ..., Rect is .... The tools that build summaries and search indexes take that first sentence, and a reader scanning a list of functions reads only the first lines. A comment like // This function adds things up says nothing in a list. Write full sentences and end the first with a period.

No blank line between the comment and the declaration. A blank line separates them, and the comment becomes a floating one that tools ignore. This is the commonest reason a comment fails to show up.

Package comment above the package clause. One file in the package carries it, and for anything longer than a paragraph the convention is a file called doc.go that contains only the comment and the package line. It begins Package name .... For package main, the comment describes the program and begins with the command name.

Document the contract, not the code. Say what the function promises: what it returns on error, whether it is safe for concurrent use, whether it keeps a reference to its arguments, what a zero value means. The reader of the documentation has not read your function body and should not need to. The last two matter most: a missing "safe for concurrent use" statement is read as "not safe".

What Go 1.19 added

Doc comments grew a small, deliberate markup, described in the Go Doc Comments page. It is designed so that a comment reads well as plain text and renders better with the tools.

WriteRenders as
# Title on its own line, with blank lines around itA heading
[Name], [pkg.Name], [Type.Method]A link to that identifier's docs
[text]: https://example.test/ on its own lineA link definition, then [text] is a link
Lines starting with - or 1. indented by two spacesA list
Lines indented by a tab or spaces beyond the paragraphA preformatted code block
A paragraph starting Deprecated:A deprecation notice that tools act on

gofmt rewrites doc comments into this canonical layout, reindenting lists and code blocks, so the formatting is not something to align by hand. If a list in your comment gets re-indented after a save, that is the formatter doing its job.

Deprecation notices

Start a paragraph with Deprecated: followed by what to use instead. Editors with Go support strike through the identifier at every call site, and staticcheck reports a use of a deprecated identifier. That is the whole mechanism, and it is enough to move a codebase off an old function in a release or two. Do not delete the function until the major version changes, since removal breaks importers, and say in the notice which function replaces it, as Sum did above.

Examples that are tested

An example function in a _test.go file, named ExampleTotal, shows up in the documentation next to Total, and go test runs it and compares its output with a comment starting // Output:. It is the one piece of documentation that cannot go stale:

example_test.goGo
package shapes_test

import (
	"fmt"

	shapes "example.test/lab"
)

func ExampleTotal() {
	total := shapes.Total(shapes.Rect{W: 2, H: 3}, shapes.Rect{W: 1, H: 4})
	fmt.Println(total)
	// Output: 10
}
Text
=== RUN   ExampleTotal
--- PASS: ExampleTotal (0.00s)
PASS
ok  	example.test/lab	1.235s

If the function changed to return something else, the test would fail. That is a stronger guarantee than a prose claim, and a good example answers the question a user would otherwise ask in an issue. Examples for a type are ExampleRect, and for a method ExampleRect_Area, and the suffix after an underscore names a variant.

When documentation fails

The silent failures are the blank line between comment and declaration, a comment that starts with a different word than the name, and a package comment that is not above the package clause. Check by running go doc -all on the package and reading it as a stranger. Another failure is documenting too much: a comment that paraphrases the code line by line goes stale the first time the code changes, and an out-of-date comment is worse than none. Reserve comments inside functions for the reason something is done, and let doc comments hold the contract. Unexported identifiers do not appear in the rendered documentation, but they get the same treatment in your editor, so a good comment on an internal function is worth the sentence. For the full grammar and every edge, the official Go Doc Comments reference is the source, and the go doc command lists its flags.

Also inTooling
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.