1. Why GoDoc if everything is already obvious
When you write code, it feels like it speaks for itself. That feeling usually lasts until the first Monday when you open your own project and can’t remember why a function is called Normalize but does “almost normalize, but with a surprise.” GoDoc isn’t for aesthetics: it records the meaning, constraints, and guarantees of public elements so the code can be used without telepathy.
GoDoc is a set of conventions and tools that turn special comments into documentation. And not documentation “in a separate file that nobody updates,” but right in the source. Starting with Go 1.19, doc comments got a richer syntax (links, lists, clearer headings), and gofmt can format such comments into a standard shape.
Think of GoDoc like a sign on a door: “library here,” “badge required,” “don’t press the red button.” If there’s no sign, people will still walk in, but then they’ll complain.
A small diagram of who “consumes” GoDoc:
flowchart TD
A[.go files with doc comments] --> B[IDE / hints]
A --> C[go doc / godoc]
A --> D[pkg.go.dev / documentation generators]
2. What a doc comment is and how it attaches to code
The most frustrating GoDoc mistake is to write a great text in a way that the tooling can’t tell what it refers to. GoDoc has a simple rule: a doc comment must be placed immediately before the declaration, and there must be no blank line between the comment and the declaration. Otherwise it’s just a comment “floating in the air,” not element documentation.
The simplest example is a function from our training package todo. Let’s imagine we already use it in a CLI for managing tasks, and we have string IDs that we need to parse.
package todo
import "strconv"
// ParseID parses s as a positive integer task ID.
func ParseID(s string) (int, error) {
return strconv.Atoi(s)
}
Here the comment “attached” to ParseID, and the IDE will show it on hover, while the package documentation can render it. If you insert a blank line between // ParseID... and func ParseID..., the comment will lose its connection to the declaration.
An important nuance: a doc comment isn’t only about functions. You can document types, methods, variables (including sentinel errors), and constants. But it’s especially important to document exported items: those that start with a capital letter, because that’s your external contract.
A mini table (a “cheat sheet” for what is usually documented):
| Element | What to capture |
|---|---|
|
purpose, input constraints, results, error behavior |
|
type role, invariants, what “valid state” means |
|
when it is returned, how to compare/handle it |
|
meaning of the value, range of acceptable values |
3. The comment starts with the name: why this matters
In Go there is a convention that can feel “military” at first: the first sentence of a doc comment starts with the name of the documented identifier and reads like a complete sentence. It might sound trivial, but in practice it makes documentation predictable: it’s easy to scan, and tools can generate good synopsis lines.
Compare two variants. The first seems fine, but it’s “detached”:
// Parses a task ID from string.
func ParseID(s string) (int, error) { ... }
The second is the Go way:
// ParseID parses a task ID from string.
func ParseID(s string) (int, error) { ... }
If you document a type, you usually write Task represents …; for an interface, Store provides …; for a method, (*Task).MarkDone marks …. It looks uniform, and that’s the point: documentation across the project starts reading like a single text, not a pile of random remarks from different people (and different moods).
4. Documenting functions: contract and errors
Functions are the most common documentation target because they’re used directly. And this is where beginners usually write comments like “does X.” That’s not bad, but it’s not enough. Users of the function care more about constraints: which inputs are allowed, what is returned, and what is guaranteed when error != nil.
Let’s take validation of a task title. We want an empty title to be considered an error, and we want the calling code to be able to recognize it.
package todo
import "errors"
var ErrEmptyTitle = errors.New("empty title")
// ValidateTitle checks that title is not empty.
//
// It returns ErrEmptyTitle when title == "".
func ValidateTitle(title string) error {
if title == "" {
return ErrEmptyTitle
}
return nil
}
Here the comment records the condition under which a specific error is produced. That’s a crucial part of the contract: calling code can compare the error to ErrEmptyTitle and branch its behavior. This pattern (sentinel error + comparison) is very common: Go errors are values, and comparing against a known “marker” error is one of the standard techniques.
Now for the subtlest point: “what happens to the result on error.” Suppose we parse an ID:
package todo
import (
"fmt"
"strconv"
)
// ParseID parses s as a task ID.
//
// It returns a non-nil error if s is not a valid base-10 integer.
// On error, the returned ID value is 0.
func ParseID(s string) (int, error) {
id, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("parse id: %v", err)
}
return id, nil
}
The phrase On error, the returned ID value is 0 may feel obvious, but it saves people time. As the author, you set expectations: on error the value is the zero value, and it cannot be relied on as a “partial result.” Sometimes it’s different: a function can return a partial result and an error at the same time. Then you must say that too, otherwise people will guess and use the result incorrectly.
5. Documenting types: role and invariants
Type documentation is where many people create a “field registry”: “Task has Title, Done, CreatedAt…”. But that’s almost always useless because the fields are already visible in code. What’s far more important is to describe the meaning of the type and its invariants: what is required, what is allowed, and what is considered a correct state.
For example, we have a task model:
package todo
// Task represents a single to-do item.
//
// A Task is considered valid when Title is not empty.
type Task struct {
ID int
Title string
Done bool
}
Here we’re not repeating the fields; we say what a task is and what “valid” means. If later in the project you add a function NewTask(title string) (Task, error), you already know it must validate Title.
If a type has methods, document the behavior of the methods, not “what’s inside the switch.” Example with a status:
package todo
import "fmt"
// Status describes the lifecycle state of a task.
type Status int
const (
StatusOpen Status = iota
StatusDone
)
// String returns a human-readable status name.
func (s Status) String() string {
switch s {
case StatusOpen:
return "open"
case StatusDone:
return "done"
default:
return fmt.Sprintf("Status(%d)", int(s))
}
}
Notice: the comment for String captures observable behavior (human-readable status name), not the implementation. The implementation may change, but the contract should remain.
6. Errors, variables, and GoDoc markers
Documenting errors and exported variables
Errors in Go are API elements just like functions and types. If you have an exported error ErrEmptyTitle, it’s very useful to document it, because its meaning is not always clear from just the message "empty title". The message is for the user (or logs), while the documentation is for the developer writing code around the error.
package todo
import "errors"
// ErrEmptyTitle is returned by ValidateTitle when the task title is empty.
var ErrEmptyTitle = errors.New("empty title")
This comment is short, but it solves an important task: it tells where the error can come from and how it relates to the API. This is especially helpful when a package has a dozen errors and, without documentation, they turn into “a collection of strings.”
A good style is to describe “who returns it” and “when it’s returned.” Then a person won’t have to search through the code to figure out where the error comes from (they’ll be searching for other reasons—because they like adventures).
Markers Deprecated: and BUG(who):
Sometimes you need to be honest in an API: “we no longer use this.” And in that case it’s better not to write something vague like “old, don’t use,” but to use the standard marker Deprecated:. It must be a separate paragraph inside the doc comment and must start exactly with Deprecated: so documentation tools and IDEs can recognize it.
package todo
// LegacyParseID parses a task ID from string.
//
// Deprecated: use ParseID.
func LegacyParseID(s string) (int, error) {
return ParseID(s)
}
And BUG(who): is a top-level comment (meaning it does not have to be right before a function declaration). It ends up in the Bugs section of the documentation and lets you leave a note about an issue that is known and important for package users.
package todo
// BUG(dev): ParseID accepts leading plus sign, which is not intended.
In real code, of course, it’s better to fix the bug than to document it. But sometimes you can’t change behavior without risking breaking backward compatibility, and then honest documentation is the lesser evil.
7. GoDoc formatting: paragraphs and examples
It’s easy to go to extremes: either write dry “Do X,” or start documenting everything, including the author’s mood and the weather outside. You need the middle ground: concise, structured, and readable when viewed as documentation.
The rule is simple: separate paragraphs with a blank line. If you want to insert a “preformatted” chunk (for example, a mini-example or an API list), GoDoc traditionally uses indentation. This works in documentation tools and when reading in an IDE.
For example, you can describe a function’s contract and give a small usage example right in the comment (not huge—actually mini).
package todo
// ValidateTitle checks that title is not empty.
//
// Typical usage:
//
// if err := todo.ValidateTitle(title); err != nil {
// return err
// }
func ValidateTitle(title string) error {
if title == "" {
return ErrEmptyTitle
}
return nil
}
This Typical usage is useful because it shows an error-handling style as part of a normal use case. And yes, it’s the exact style Go encourages: errors are not exceptions, but a normal outcome.
Another important point: don’t duplicate the obvious. A comment for Add(a, b int) int like Add adds a and b may be acceptable in teaching examples, but in a real package it’s useless. Documentation should add meaning: constraints, guarantees, scenarios, nuances.
8. Common mistakes when writing GoDoc comments
Mistake #1: the comment is “floating in the air” because there is a blank line between it and the declaration.
This happens all the time: you nicely formatted the text, inserted a blank line “for aesthetics,” and GoDoc stopped associating the comment with the declaration. As a result, the IDE shows no tooltip, and the documentation looks as if you didn’t write anything at all.
Mistake #2: the first sentence does not start with the element name.
It may seem unimportant, but then the documentation reads like a set of fragments: Parses…, Validates…, Does…. When the first sentence starts with the name (ParseID parses...), the text becomes searchable and predictable: you can quickly scan a documentation page with your eyes.
Mistake #3: the comment restates the code but does not capture the contract.
The phrase ValidateTitle checks that title is not empty is a start, but if the function returns a specific sentinel error, be sure to state “exactly when” and “exactly which one.” Otherwise the calling code doesn’t know whether it can compare the error directly, log it, show it to a user, etc.
Mistake #4: for functions returning error, the behavior of the result on error is not described.
If a function returns (T, error), the calling code critically needs to know: is the result on error the zero value, a partial result, or “undefined”? If you don’t specify it, people will either use the value incorrectly or over-defend and write extra code.
Mistake #5: Deprecated is written as regular text rather than as the marker Deprecated:.
If you write This function is deprecated inside a paragraph, a person will understand. But tools may not highlight it as deprecation, and the IDE may not warn the user. In GoDoc, it’s better to use the exact marker Deprecated: as a separate paragraph.
GO TO FULL VERSION