Lesson 18 / 18
Documentation Conventions
Six names — three exported, three unexported; in each triple, one has a comment adjacent to it, one has a comment separated by a blank line, one has no comment at all — are asked of the doc tool. The number of names that show up depends only on the case of the letter (3/6), while the number that carry a description depends only on the comment's position (1/6). The limiting measurement shows that an incorrect doc comment is displayed exactly the same way: the doc tool never checks the comment's correctness at all.
Published
In this lesson
- Measures how many of six names show up in the doc tool and shows that what determines this is the name's letter
- Tests that whether a comment counts as a description comes not from its content but from its adjacency to the definition
- Shows that the doc tool does not check a comment's correctness, printing a wrong description exactly the same way
Contents
The previous three lessons called three members of the language’s own command family: the compiler decided whether a source was valid, the formatter canonicalized its appearance, vet caught a pattern the compiler overlooked. All three were concerned with the source itself. This final lesson looks at the family’s fourth member: the doc tool that produces the text describing what a package offers to the outside. This tool neither compiles the source, nor formats it, nor tests it against a pattern — it only reads, and decides which name, with which comment, to show and when. The course’s final question: is it the comment itself that makes this decision, or the first letter of the name?
Adjacent Comment, Detached Comment
PT19. When a comment is written adjacent to a definition (with no blank line in between), that comment counts as that definition’s doc comment; when a blank line comes in between, the same comment text is no longer attached to any definition at all. PT20. The doc tool lists only exported names; no matter how carefully an unexported name’s doc comment is written, that name never enters the documentation at all.
// tools/tools.go — shared routines that call the doc tool from within the program
package tools
import (
"os"
"os/exec"
"path/filepath"
"strings"
)
func environment() []string {
return append(os.Environ(), "GOTOOLCHAIN=local")
}
// SetUpPackage writes the given package source to the "trial/pkg" subpackage and
// returns the module's root directory; the caller runs the doc tool in this directory.
func SetUpPackage(pkgSource string) string {
dir, _ := os.MkdirTemp("", "trial")
os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module trial\n\ngo 1.24\n"), 0o644)
os.MkdirAll(filepath.Join(dir, "pkg"), 0o755)
os.WriteFile(filepath.Join(dir, "pkg", "pkg.go"), []byte(pkgSource), 0o644)
return dir
}
// VisibleInDocs asks the doc tool about a name; visible is true if the name appears in
// the docs, and hasDescription says whether there is a comment text below the line.
func VisibleInDocs(dir, name string) (visible, hasDescription bool) {
cmd := exec.Command("go", "doc", "./pkg."+name)
cmd.Dir = dir
cmd.Env = environment()
output, err := cmd.Output()
if err != nil {
return false, false
}
lines := strings.Split(strings.TrimRight(string(output), "\n"), "\n")
nonEmpty := 0
for _, l := range lines {
if strings.TrimSpace(l) != "" {
nonEmpty++
}
}
// When the output is only the package heading and the signature line, it carries
// two non-empty lines; when a description is added, this count increases.
return true, nonEmpty > 2
}
// DocText returns the full output the doc tool gives for a name.
func DocText(dir, name string) string {
cmd := exec.Command("go", "doc", "./pkg."+name)
cmd.Dir = dir
cmd.Env = environment()
output, _ := cmd.Output()
return strings.TrimSpace(string(output))
}
VisibleInDocs calls the doc tool directly with a name: if the name is found, the output
carries at least the package heading and the signature line; if not found, the tool returns with
a non-zero exit code, and the error text is never read at all. Whether there is a description is
counted by looking at how many lines the output carries — when a description is added below the
signature line, the output gains a third non-empty line.
Six Names, Two Independent Questions
// main.go — the doc is produced from the source; visibility depends on the letter, description on position
package main
import (
"fmt"
"os"
"lesson/tools"
)
func main() {
pkgSource := "// Package pkg is a small library for doc comment experiments.\n" +
"package pkg\n\n" +
"// LargeCommented returns the sum of the two given numbers.\n" +
"func LargeCommented(a, b int) int { return a + b }\n\n" +
"// smallCommented returns the sum of the two given numbers.\n" +
"func smallCommented(a, b int) int { return a + b }\n\n" +
"func LargeUncommented(a, b int) int { return a - b }\n\n" +
"func smallUncommented(a, b int) int { return a - b }\n\n" +
"// LargeDetachedComment is a description, but it is separated from the definition by a blank line.\n\n" +
"func LargeDetachedComment(a, b int) int { return a * b }\n\n" +
"// smallDetachedComment is a description, but it is separated from the definition by a blank line.\n\n" +
"func smallDetachedComment(a, b int) int { return a * b }\n\n" +
"// Average returns the average of the given numbers.\n" +
"func Average(numbers ...int) int {\n" +
"\tsum := 0\n" +
"\tfor _, n := range numbers {\n" +
"\t\tsum += n\n" +
"\t}\n" +
"\treturn sum\n" +
"}\n"
dir := tools.SetUpPackage(pkgSource)
defer os.RemoveAll(dir)
names := []string{
"LargeCommented", "smallCommented",
"LargeUncommented", "smallUncommented",
"LargeDetachedComment", "smallDetachedComment",
}
fmt.Println("-- six names, visibility and description in the docs --")
visibleCount, describedCount := 0, 0
for _, name := range names {
visible, hasDescription := tools.VisibleInDocs(dir, name)
if visible {
visibleCount++
}
if hasDescription {
describedCount++
}
fmt.Printf("%-22s visible=%-5v description=%v\n", name, visible, hasDescription)
}
fmt.Printf("total: %d/6 visible, %d/6 described\n", visibleCount, describedCount)
fmt.Println()
fmt.Println("-- limiting measurement: the doc tool does not verify the comment's correctness --")
fmt.Println(tools.DocText(dir, "Average"))
fmt.Printf("Average(2, 4, 6) real result: %d\n", actualAverage(2, 4, 6))
}
// actualAverage independently shows what the produced Average function actually does:
// the comment says "average," the function returns the sum.
func actualAverage(numbers ...int) int {
sum := 0
for _, n := range numbers {
sum += n
}
return sum
}
-- six names, visibility and description in the docs --
LargeCommented visible=true description=true
smallCommented visible=false description=false
LargeUncommented visible=true description=false
smallUncommented visible=false description=false
LargeDetachedComment visible=true description=false
smallDetachedComment visible=false description=false
total: 3/6 visible, 1/6 described
-- limiting measurement: the doc tool does not verify the comment's correctness --
package pkg // import "trial/pkg"
func Average(numbers ...int) int
Average returns the average of the given numbers.
Average(2, 4, 6) real result: 12
PT21. Three of the six names show up in the docs, three do not show up at all; all three that
show up start with an uppercase letter, all three that do not show up start with a lowercase
letter. PT22. Of the three visible names, only one carries a description — LargeCommented,
whose comment is adjacent to the definition; LargeUncommented, which has no comment at all, and
LargeDetachedComment, whose comment is separated by a blank line, both show up by name in the
docs but without a description.
These two observations answer two completely independent questions. The answer to “does this
name exist in the docs?” looks only at the case of the letter — smallDetachedComment‘s comment
is detached in exactly the same way as LargeDetachedComment‘s, but the two names’ fates in the
docs are not the same at all, because the difference is not in the comment, it is in the name
itself. The answer to “does this name have a description?” looks only at the comment’s position —
LargeUncommented has no comment at all; LargeDetachedComment has a comment, but it is
disconnected from the definition; both show up in the docs without a description, because
neither has an adjacent comment for the doc tool to read.
The Limiting Measurement: The Doc Tool Does Not Check Correctness
PT23. The Average function’s doc comment says “returns the average of the given numbers”;
the function itself returns the sum. When Average(2, 4, 6) is called, the value returned is
12 — not the average, the sum. PT24. The doc tool prints this comment exactly as it is,
never testing its correctness at all; neither the compiler, nor vet, nor the doc tool catches this
mismatch.
This is the course’s final “left open” line, and it stands in a different place than the earlier
ones. The earlier “left open” examples (writing to an uninitialized map stopping with a panic, a
string being indexed by byte) were always a result that surfaced at run time — something
happened when you ran the program. What is left open here has nothing to do with run time at
all: the Average function runs without error, does not panic, does not return the wrong type —
only what its name says and what it does do not agree, and there is no tool at all to check
this mismatch. Just as the compiler does not check a string’s content, it does not check a
comment’s content either; vet matches format verbs against argument types but never compares
what a comment describes against what the code does; the doc tool does not even connect these two
files (the source and the comment) to each other, it only copies the comment according to its
position and displays it. What is unwritten here is a verification mechanism, and the
language puts nothing at all in its place.
Course Wrap-Up
Go Fundamentals opened with a single question: when writing a program, the thing that matters most is not what is written, it is what is unwritten. Eighteen lessons asked this question across three mechanisms: in the language itself (zero values, slice and map behavior), in the compiler (patterns rejected, from an unused variable all the way to the export rule), and outside the source (version selection, form, documentation).
The first claim was confirmed: in Go there is no such state as “uninitialized” — the place of an unwritten value is always filled by a defined counterpart, whether that is a zero value or a slice bound’s default. The second claim showed that every pattern the compiler rejects is a decision to “narrow the counterpart of the unwritten” — from an unused variable to an unexported field, the compiler does not accept some kinds of unwrittenness at all. The third claim established that definedness works in the reading direction and stops in the writing and interpreting direction: this final lesson showed the most extreme example of this — reading a comment is defined, but whether that comment is correct is checked nowhere at all.
The course closes without overgeneralizing: these three classes (defined, rejected, left open) do not cover all of Go’s behavior, they only classify the answers given to the single question these eighteen lessons asked — what happens when you do not write this?
| Lesson | Unwrittenness measured | Counterpart (defined · rejected · left open) | Limiting measurement |
|---|---|---|---|
| Go’s Design Goals | Four reasons for rejection: an unused variable/import, an unwritten return, an implicit conversion | rejected | Rejecting an implicit conversion does not shorten the source: the uncompiled line is 15, its compiled counterpart is 22 characters |
| Environment Setup and the First Program | Three parts mandatory to run: the package declaration, func main, the module file |
rejected | The module file sits outside the source; with the source completely unchanged, removing only the module file stops compilation |
| Variables and Constants | Of six constants, only two have their value written in the source, four are produced with iota |
defined (iota) · rejected (a short declaration at package level) |
A short declaration is valid only inside a function; the same line is rejected at package level |
| Basic Types | An untyped constant’s default type is int; a sum exceeding int8’s limit wraps to -128 |
defined (default type, overflow) · left open (floating-point comparison) | Rejecting an implicit conversion does not apply to constants; an untyped constant fits the target type on its own |
| Zero Values | The distribution of fifteen unwritten cases — the course’s binding count is produced here | 8 defined · 4 rejected · 3 left open | nil can be appended to a slice, nil cannot be written to a map; both are a zero value in the same way |
| Control Flow | The optionality of for’s three parts, an expressionless switch, an unwritten break |
defined (all five of the five forms) | Reversing the default is not free: wanting to fall through requires writing fallthrough |
| Functions | A named return never written in the body at all; a returned value never captured at all | defined (zero return) · rejected (an unwritten return) · left open (an ignored return) | Once a returned value is bound to a name, the unused-variable rejection comes into play |
| Variadic Functions | A parameter given no arguments at all being a nil slice |
defined | A nil slice and an empty slice are the same in terms of len, separate in operations that look at identity |
| Arrays and Slices | The array/slice difference in an assignment line: a copy for an array, sharing for a slice | defined | When append exceeds capacity, sharing is cut off; the copied header now points to a separate backing array |
| Slice Operations | The default for unwritten slicing bounds | defined | The same append call, with the same source text, gives two different results at two different capacities |
| Maps | The class of four access forms: an existing/non-existing key, reading from/writing to a nil map |
defined · left open (writing panics) | Without comma-ok access, an entry whose value is zero cannot be told apart from one that does not exist at all |
| Structs | The zero value of unwritten fields; embedding carrying unwritten names to the outer type | defined | Embedding is not inheritance: when an embedded type’s method is called, the receiver is the embedded value itself, not the outer struct |
| Pointers | An array and a struct passed without taking their address not changing on the caller’s side | defined (copy) · left open (reading a field from a nil pointer) |
A pointer is a value too, and it gets copied; reassigning it changes only the local copy |
| Strings and Runes | len counts bytes, indexing gives a byte, range gives a rune |
defined (a promise of bytes) · left open (no counterpart for “character”) | A rune count is not a character count either; combining code points break this |
| Package Layout and Visibility | Exporting depending on a single letter | rejected (from outside the package) · defined (inside the package) | The boundary is the package boundary, not the file boundary; two files of the same package share a lowercase name with no import at all |
| Modules and Dependencies | An unwritten dependency version | defined (minimal version selection) | The selected version is not the catalog’s newest, it is the greatest of the requests actually written (or derived) |
| Go Command Family | An unwritten source form; a pattern the compiler does not reject | defined (form) · rejected (vet) | The form changes (7 of 18 lines), the meaning does not: the source’s output is the same before and after formatting |
| Documentation Conventions | The name and description entering the docs | defined (visibility by letter) · left open (content is not checked) | The doc tool does not check a comment’s correctness; a wrong description is printed exactly the same way |
Summary
- The doc tool lists only exported names; whether a name shows up in the docs is determined only by whether its first letter is uppercase, not by the comment itself.
- Whether a comment counts as a description depends on its being written adjacent to a definition; a single blank line in between detaches the comment from that definition and keeps it out of the docs.
- Three of six names show up in the docs (only the uppercase ones), and only one of the visible ones carries a description (only the one with an adjacent comment) — the two questions are independent of each other.
- The limiting measurement: the doc tool never checks at all whether a comment is correct; a wrong description is printed exactly the same way as a correct one.
- The course closed with three classes (defined, rejected, left open); these classes do not cover all of Go’s behavior, only the single question these eighteen lessons asked.
To keep your progress and take notes, Log in
My notes
Log in to take notes.