Skip to content
beginner Phase 1 · Environment Setup and First Programs

Comments and Doc Comments

Annotate source with line and block comments and write doc comments that surface in godoc.

25m
0 problems
Topic Progress 0%

Two Comment Syntaxes, One of Which Lies to You

Go has exactly two comment forms, and the second one's nesting behavior surprises almost everyone on first contact.

// line comment — to the end of the line

/* block comment
   spans lines */

The line comment starts at // and runs to the end of the line. It is the form you will write 95% of the time: it needs no closer, it cannot accidentally swallow the next declaration, and linters can attach tool annotations to it. The block form starts at /* and stops at the first subsequent */ — the spec is explicit about this, and it means block comments do not nest:

/* outer /* inner */ still comment? no — comment ended here */

The comment closes at the first */, and the trailing */ becomes source tokens, which the compiler reports as a syntax error somewhere you did not intend. Inside a block comment you get no help from indentation and no second chance. The practical consequences: use /* */ for banner comments, ASCII diagrams, or temporarily disabling a dense block of code, and use // for everything else.

Two tokenizer details explain style rules you will otherwise fight. A block comment containing no newline acts like a space — a/*x*/b is the identifier ab. Any other comment acts like a newline, which is why semicolon insertion interacts with them: you cannot put a comment between a return keyword and its values on the same line and expect the line to hold together. And because Go inserts semicolons at newlines after certain tokens, brace placement rules (opening brace on the declaration line) apply through comments as they do through whitespace.

But the more important distinction in this topic is not line versus block. It is ordinary comment versus doc comment. An ordinary comment is a note to humans that the compiler ignores completely. A doc comment is a comment attached to a declaration — placed immediately above it, with no blank line in between — and the toolchain preserves it as part of that declaration's documentation. Attach it correctly and go doc, your editor's hover, and pkg.go.dev all show it; separate it with a blank line and it becomes invisible prose that no tool will ever surface. Position is the whole mechanism. Before relying on that rendering, make sure you have run the command yourself — Formatting and Inspection Tools covers go doc and what it does and does not display.

Writing the Comment the Reader Searches For

A doc comment is read in two directions: someone lands on your function from a search, and someone already inside your package skims the signature. Both readers need the same first sentence — one that starts with the name they just typed or hovered.

// Retry runs fn up to max attempts, doubling the delay after each failure.
// It returns nil as soon as fn succeeds, or the last error once attempts
// are exhausted. A max below 1 is treated as 1.
func Retry(fn func() error, max int) error {
	// ...
}

Compare the alternative: // Helper function for retrying. The strong version satisfies three conventions at once. The first word is Retry, matching the declaration, so generated docs read as sentences (Retry runs fn...) and a reader scanning a list of symbols can match names to behavior without backtracking. It states behavior, including the boundary case a caller will otherwise discover in production (max < 1). And it stays silent about implementation — why the delay doubles, or what the code does line by line, is not this comment's job; that belongs where the decision is made.

Placement rules are mechanical:

// Package cache provides a bounded in-memory cache with TTL expiry.
package cache          // package comment: directly above the package clause

// Config controls cache capacity and expiry policy.
type Config struct {   // type comment: directly above the type, no blank line
	MaxEntries int
}

// Get returns the value stored under key, or false if it has expired.
func (c *Cache) Get(key string) (any, bool) {  // method comment likewise
	// ...
}

A blank line between the comment and the declaration breaks attachment — the comment still renders as floating text in the file, but go doc no longer associates it with the symbol. The package comment is rarer and stricter: exactly one file should carry it, conventionally doc.go for libraries or the main file for programs. When two files both document the package, readers get whichever the tool stitched together, and the duplication rots the moment one copy is updated.

Where do these comments surface? go doc prints them in the terminal; pkg.go.dev renders them for the world; your editor's language server shows them on hover — all from the same source text, which is the economy of the design: write documentation once, in the only place that cannot drift from the code, and every surface inherits it. The identifier rules are the same ones Packages and Exported Identifiers enforces in code: exported names deserve doc comments, and the convention extends mechanically to every declaration. Reviewers enforce the name-matching rule because it is cheap to check and expensive to retrofit.

Prose, Dead Code, and Directives: What Kind of Comment Is This?

Not every comment is documentation, and three categories with different failure modes share the same syntax.

Prose that explains why. The valuable comment sits next to the surprising decision and answers the question the next reader will ask:

// Floor to whole cents: floating-point money drifts after enough additions.
price := math.Round(price*100) / 100

Delete the code and the comment becomes wrong; delete the comment and the code looks arbitrary. That coupling is the test: a comment that survives rewriting its code was attached to the wrong thing. Comments that restate the line below them (// increment i above i++) add noise that trains readers to skip all comments — the real cost is desensitization, not bytes.

Commented-out code. This is the second category and it is almost always a mistake. It was disabled for a reason the comment rarely records, it still looks like code to tools and to humans, and it silently diverges from the live version above it until no one can tell which fragment is true. Version control already stores history with blame and timestamps; a dead block in the working tree stores only ambiguity. Delete it.

Directives — comments the machine reads. The third category is not prose at all:

//go:generate stringer -type=Status
//go:embed templates/*.html
var templateFS embed.FS

//nolint:unused // kept for API compatibility
func legacySetup() {}

//go:generate hands its remainder to go generate; //go:embed tells the compiler to pack files into the binary (the mechanics are in Embedding Assets with go:embed); //nolint and friends are instructions to linters. Note the shape: no space between // and the name — //go:generate is an instruction, // go:generate would be prose about one, and go generate ignores the latter. These comments form a real interface with tools, so treat a directive as API: changing it changes behavior, and removing it removes a feature.

The sorting rule for everything in a file:

Comment Audience Failure mode
Doc comment on a declaration readers of go doc / hover blank line detaches it; vague first sentence hides it
Why-comment beside a decision the next maintainer states what the code says, so it goes stale
Dead code comment nobody lies with authority
//go: directive a specific tool mistyped form silently does nothing

Write the first two, delete the third, and learn the syntax of the fourth before using it. Then check the rendered result with the tools built for exactly that purpose — gofmt, go doc, and go vet are covered next in Formatting and Inspection Tools.

Quiz

1. A doc comment sits three lines above a function with two blank lines between them. What does `go doc` show for that function?

Question 1 options

2. What happens with the comment `/* outer /* inner */ tail */`?

Question 2 options

3. Where should a package-level doc comment go so every documentation surface picks it up?

Question 3 options

4. You write `// go:generate stringer -type=Status` with a space after //. What is the effect?

Question 4 options

Flashcards

Question

What makes a comment a doc comment rather than an ordinary comment?

Answer

Position. A doc comment sits immediately above a declaration with no blank line, so the toolchain treats it as that symbol's documentation and prints it in go doc, editor hover, and pkg.go.dev. The same text separated by a blank line is an invisible ordinary comment — attachment, not syntax, is the difference.

Question

What first-sentence convention do Go doc comments follow, and why does it exist?

Answer

The sentence begins with the name being declared: '// Retry runs fn up to max attempts...'. Generated docs then read as natural sentences, and a reader scanning a symbol list can match each name to its behavior without re-reading. It is the convention golint-family tools check mechanically.

Question

How do line comments and block comments differ in tokenization?

Answer

// runs to end of line and needs no terminator. /* */ stops at the first */ — block comments do not nest, so an inner /* does nothing and a stray */ inside closes the comment early, leaving text the compiler parses as code. A newline-free block comment acts like a space; a multi-line one acts like a newline.

Question

Why should commented-out code be deleted instead of left in place?

Answer

It carries no record of why it was disabled, it still looks like live code to reviewers and tools, and it drifts from the working version until no one can tell which fragment is true. Version control already stores disabled history with timestamps and blame — keeping a dead block in the tree only adds ambiguity.

Question

What distinguishes a //go: directive from a comment written about that directive?

Answer

The form: directives are //go:generate, //go:embed and friends with no space after //, and a specific tool parses the remainder as instructions. Adding a space turns it into prose that every tool ignores — still valid Go, silently inert. Treat directives as API because editing one changes build or generation behavior.

Revision Notes

Key Takeaways

  • 1. Block comments close at the first */ — nesting is impossible, which is why // is the default form
  • 2. Documentation attaches by position: comment flush above declaration, no blank line, and every surface (go doc, hover, pkg.go.dev) inherits it
  • 3. Doc comment first sentences begin with the declared name so generated docs read as sentences and lists stay scannable
  • 4. Commented-out code is deleted, not kept — history belongs in version control, ambiguity does not
  • 5. //go: directives are API: no space after //, and the spaced prose form compiles while silently doing nothing

Interview Tips

  • • "What's the difference between a comment and a doc comment?" — attachment, not syntax: flush above the declaration with no blank line, and it becomes part of the symbol's rendered documentation
  • • "Where does the package doc comment go?" — directly above the package clause in exactly one file (doc.go by convention); adding a second copies and drifts the documentation
  • • "How do you write a good doc comment?" — open with the name, state observable behavior and boundary cases, skip implementation; quote a one-line example like '// Retry runs fn up to max attempts...'
  • • "Name a comment that isn't documentation" — a //go: directive: machine-read, exact syntax required, and a missing space turns it into inert prose while the build still passes

Cheat Sheet

Comments and Doc Comments

Comment forms:

Form Ends at Nesting Typical use
// ... end of line n/a everything; tool annotations
/* ... */ first */ does NOT nest banners, ASCII art, disabling a block

Attachment rule: comment directly above the declaration, no blank line — otherwise it is not documentation.

Placement:

// Package cache provides a bounded cache with TTL expiry.
package cache

// Retry runs fn up to max attempts, doubling the delay after each failure.
// A max below 1 is treated as 1.
func Retry(fn func() error, max int) error { /* ... */ }
  • Package comment: exactly one file, conventionally doc.go
  • First sentence starts with the declared name
  • State behavior plus boundary cases; leave implementation to the code next to it

Directives (machine-read, no space after //):

Directive Reader
//go:generate ... go generate
//go:embed pattern compiler
//nolint:rule / //lint:ignore linters

Decision rules: why-not-what → prose comment; disabled code → delete; instruction to a tool → directive with exact syntax.

Continue Learning

Before You Start

  • No prerequisites listed.

Related Reading