Comments #

Comments are the part of code the compiler ignores but humans read. In Go, comments play a bigger role than mere explanation: the godoc system uses comments to generate API documentation automatically, and some special comments (called directives) even influence the behavior of the compiler and toolchain. Understanding how to write good comments in Go means understanding the conventions that let the entire Go ecosystem stay consistently documented.

Functionally, Go comment types are divided by their writing format and their purpose, from explanations for developers to special instructions for the compiler. This categorization can be seen in the following diagram:

flowchart TD
    Comment["Comments in Go"] --> Dev["Developer-Facing Explanations"]
    Comment --> Comp["Compiler Directives"]

    Dev --> Single["Single-Line: //"]
    Dev --> Multi["Multi-Line: /* ... */"]
    Dev --> Doc["Doc Comment (Godoc)"]

    Comp --> Build["Build Constraints: //go:build"]
    Comp --> Gen["Code Generation: //go:generate"]

The Two Comment Types #

Go supports two comment syntaxes identical to C:

// This is a single-line comment.
// It starts with two slashes and applies until the end of the line.

/*
   This is a multi-line comment.
   It can span many lines at once.
   Often used to "disable" blocks of code temporarily.
*/

In practice, single-line // comments are far more common in Go — including for comments spanning several consecutive lines. /* */ comments are more often used to temporarily disable blocks of code while debugging.

package main

import "fmt"

func main() {
    // Calculate the total price after discount
    price := 100000
    discount := 0.1
    total := price - int(float64(price)*discount)  // subtract 10%

    fmt.Println("Total:", total)

    /*
    // This code is temporarily disabled for debugging
    if total < 0 {
        fmt.Println("Error: negative total")
    }
    */
}

Doc Comments — Documentation Read by godoc #

Go has a built-in documentation system: godoc reads comments written directly above a declaration and uses them as official documentation. This convention matters a lot because the entire Go standard library — and almost every good Go library — follows it.

Doc comment rules:

  • Written with // (not /* */)
  • Placed directly above the declaration with no blank line in between
  • Start with the name of the documented identifier
  • End with a period
// Package mathutil provides additional mathematical functions
// not available in the standard library.
// This package has no external dependencies.
package mathutil

// MaxInt is the largest integer value representable
// on this platform. Its value depends on whether the system is 32-bit or 64-bit.
const MaxInt = int(^uint(0) >> 1)

// ErrDivisionByZero is returned when a division operation
// attempts to divide by zero.
var ErrDivisionByZero = errors.New("division by zero")

// Add returns the sum of a and b.
// This function is safe for all int values, including negatives.
func Add(a, b int) int {
    return a + b
}

// Divide returns the result of dividing a by b.
// If b is zero, Divide returns 0 and ErrDivisionByZero.
func Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, ErrDivisionByZero
    }
    return a / b, nil
}

// Calculator stores calculation state and provides basic arithmetic operations.
// Use NewCalculator to create an initialized instance.
type Calculator struct {
    // Memory stores the value kept in the calculator's memory.
    Memory float64

    // Precision determines the number of decimal digits in the output.
    // Default: 2.
    Precision int

    result float64  // the current calculation result (not exposed)
}

// NewCalculator creates a new Calculator with a default precision of 2.
func NewCalculator() *Calculator {
    return &Calculator{Precision: 2}
}

After writing the comments above, run go doc to see the result:

go doc mathutil.Add
# Output:
# func Add(a, b int) int
#     Add returns the sum of a and b. This function is safe for all
#     int values, including negatives.

go doc mathutil.Calculator
# Output:
# type Calculator struct {
#     Memory    float64
#     Precision int
# }
#     Calculator stores calculation state...

Richer Doc Comment Conventions (Go 1.19+) #

Since Go 1.19, godoc supports lightweight markup in doc comments:

Paragraphs #

Separate them with a blank line (using an empty //):

// Connect establishes a connection to the database using the given parameters.
//
// This function attempts the connection up to maxRetry times with exponential
// backoff before returning an error. Each attempt is logged to the given logger.
//
// Example usage:
//
//	db, err := Connect("postgres://localhost/myapp", 3, logger)
//	if err != nil {
//	    log.Fatal(err)
//	}
//	defer db.Close()
func Connect(dsn string, maxRetry int, logger *zap.Logger) (*DB, error) {
    // ...
}

Lists in Doc Comments #

// Status represents the condition of an order.
// There are five valid statuses:
//
//   - Pending: order created, awaiting payment
//   - Paid: payment received, awaiting processing
//   - Processing: currently being processed
//   - Shipped: already sent out
//   - Cancelled: cancelled, cannot be changed
type Status int

Code in Doc Comments #

Code blocks in doc comments are indented with a single tab:

// ParseConfig reads configuration from a JSON file.
// Expected JSON file format:
//
//	{
//	  "host": "localhost",
//	  "port": 8080,
//	  "debug": false
//	}
//
// If a field is missing, its default value is used.
func ParseConfig(path string) (*Config, error) {
    // ...
}

Package Comments #

A package comment documents the package as a whole. It must sit above the package declaration in one of the package’s files (usually the main file or a file named doc.go):

// Package validator provides input validation for web applications.
//
// This package supports struct-tag-based validation and custom
// validation through the Validator interface. All validation functions are
// thread-safe and can be used concurrently from many goroutines.
//
// Basic usage example:
//
//	type User struct {
//	    Name  string `validate:"required,min=3,max=50"`
//	    Email string `validate:"required,email"`
//	    Age   int    `validate:"min=0,max=150"`
//	}
//
//	v := validator.New()
//	user := User{Name: "Budi", Email: "[email protected]", Age: 28}
//	if err := v.Validate(user); err != nil {
//	    fmt.Println("Validation failed:", err)
//	}
package validator

For packages with lengthy documentation, a separate doc.go file is usually created:

mypackage/
  ├── doc.go         ← only contains the package comment
  ├── validator.go
  ├── rules.go
  └── errors.go

Good Comments: “Why”, Not “What” #

This is the most important principle in writing comments. Good code already explains what it does — variable, function, and type names should be descriptive enough. Valuable comments explain why something is done a certain way:

// ANTI-PATTERN: comment that repeats the code
// Add 1 to the counter
counter++

// Create an empty slice
items := make([]Item, 0)

// Check whether err is not nil
if err != nil {
    return err
}

// CORRECT: comments that explain WHY
// The rate limiter bucket is refilled every second, not every request,
// to allow traffic bursts of up to maxBurst requests at once
// while still keeping the average within the limit.
token := time.Now().Unix() / int64(refillInterval.Seconds())

// Use int64 instead of int for IDs to stay safe on 32-bit platforms
// where int is only 32 bits and can't hold large database IDs.
var userID int64

// Skip validation for admins — they can access any resource
// without ownership checks. This decision was discussed
// in ADR-042 and approved by the security team.
if user.IsAdmin() {
    return resource, nil
}

// Remove from the end to avoid index shifting when deleting.
// If you iterate from the front, each delete shifts all elements after it,
// so you can skip elements.
for i := len(items) - 1; i >= 0; i-- {
    if items[i].Expired() {
        items = append(items[:i], items[i+1:]...)
    }
}

TODO, FIXME, and HACK Comments #

Tagged comments are a common convention for marking pending work:

// TODO: add email validation before saving to the database
// TODO(budi): implement retry logic after discussing with the team

// FIXME: this function is not safe for concurrent access
// A mutex needs to be added before deploying to production
func updateCache(key string, val interface{}) {
    cache[key] = val  // ← race condition!
}

// HACK: bypass validation due to a bug in the third-party library (issue #234)
// Remove this once the library is updated to v2.1.0
value := strings.TrimRight(input, "\x00")

// NOTE: the order below is very important — don't change it without understanding why.
// Step 1 must finish before Step 2 because Step 2 depends on state
// initialized by Step 1.

These tags are useful because they’re easy to grep:

grep -rn "TODO\|FIXME\|HACK" ./...

Compiler Directives — Special Comments #

Some comments in Go aren’t just documentation — they’re instructions to the compiler or toolchain. Their format is //go:directive with no space after //.

//go:generate — Automating Code Generation #

// Run go generate ./... to execute the commands below

//go:generate mockgen -source=./repository.go -destination=./mock/repository_mock.go
//go:generate stringer -type=Status
//go:generate protoc --go_out=. api.proto

Then run:

go generate ./...  # execute all go:generate directives in the package

//go:build — Build Constraints #

Determines the conditions under which this file gets compiled:

//go:build linux || darwin
// +build linux darwin  ← old format (pre-Go 1.17), still supported

package main

// This code only compiles on Linux and macOS
//go:build !windows

package fileutil

// Unix-style file permissions implementation
//go:build integration

package db_test

// This test only runs with: go test -tags=integration ./...

//go:noinline, //go:nosplit, etc. — Compiler Optimizations #

// Directive to prevent the function from being inlined by the compiler.
// Useful for accurate benchmarking.
//
//go:noinline
func computeHash(data []byte) uint64 {
    // ...
}

_ "embed" and //go:embed #

import _ "embed"

//go:embed templates/email.html
var emailTemplate string

//go:embed static/*
var staticFiles embed.FS

Disabling Code with Comments #

While debugging or developing, you’ll often need to “disable” blocks of code temporarily:

// Style 1: use /* */ for code blocks
/*
func old() {
    // old implementation not yet removed
    fmt.Println("this will not execute")
}
*/

// Style 2: use // for each line (more common in modern Go)
// func old() {
//     fmt.Println("this will not execute")
// }

// Style 3: use a boolean constant (for conditions toggled often)
const debug = false

if debug {
    fmt.Println("debug info")  // the compiler removes this block if debug=false
}
Modern editors (VS Code with the Go extension, GoLand) have a toggle-comment shortcut: Ctrl+/ (Windows/Linux) or Cmd+/ (macOS). It automatically adds or removes // on every selected line.

godoc and pkgsite — Reading Documentation #

Go provides two ways to read documentation from comments:

go doc — Command Line #

# Package documentation
go doc fmt

# Specific function documentation
go doc fmt.Println
go doc fmt.Sprintf

# Type documentation
go doc net/http.Request

# Everything exported from a package
go doc -all strings

# Documentation with source code
go doc -src strings.Builder

godoc — Local Web Server #

# Install godoc
go install golang.org/x/tools/cmd/godoc@latest

# Run the server on port 6060
godoc -http=:6060

# Open in the browser: http://localhost:6060/pkg/

pkgsite — The Modern pkgsite.go.dev Version #

# Install pkgsite
go install golang.org/x/pkgsite/cmd/pkgsite@latest

# Run it in your project
pkgsite

# Open in the browser: http://localhost:8080

Comment Anti-Patterns to Avoid #

// ANTI-PATTERN 1: Stale comments — more dangerous than no comment at all
// This function returns a string value from an integer
func formatPrice(price float64) string {  // ← the type changed, the comment wasn't updated
    return fmt.Sprintf("Rp%.2f", price)
}

// ANTI-PATTERN 2: Comments explaining the obvious
// Assign the value 10 to x
x := 10

// Return true if the name is empty
return name == ""

// ANTI-PATTERN 3: Commented-out code left around for a long time
// func oldImplementation() {  ← old code that was never removed
//     ...
// }
// Remove dead code — use version control for history

// ANTI-PATTERN 4: Comments as a substitute for good names
// p is the HTTP request processor
func p(w http.ResponseWriter, r *http.Request) {
// Better: give it a descriptive name
func handleUserProfile(w http.ResponseWriter, r *http.Request) {

// ANTI-PATTERN 5: TODO comments that never get resolved
// TODO: fix this (written 3 years ago, never touched)
// If a TODO won't be done soon, file an issue in the tracker

Complete Example Program #

The following program demonstrates all comment conventions in the context of a full package:

// Package currency provides types and functions for handling
// currency values with precise accuracy.
//
// This package avoids using float64 for financial calculations
// by storing all values in the smallest unit (sen).
//
// Example usage:
//
//	price := currency.NewIDR(150000)   // Rp 150.000
//	tax := price.Percent(11)           // 11% VAT
//	total := price.Add(tax)
//	fmt.Println(total.Format())        // Rp 166.500,00
package currency

import (
    "fmt"
    "strings"
)

// IDR represents a value in Indonesian Rupiah.
// The value is stored in sen (the smallest unit) as an integer
// to avoid floating-point rounding errors.
//
// IDR is a value type — operations like Add and Sub
// return new values without modifying the receiver.
type IDR struct {
    // sen stores the value in sen units (1/100 Rupiah).
    // Negative values represent debt or credit.
    sen int64
}

// NewIDR creates an IDR value from an amount of Rupiah.
// Example: NewIDR(150000) creates the value Rp 150.000,00.
func NewIDR(rupiah int64) IDR {
    return IDR{sen: rupiah * 100}
}

// NewIDRFromSen creates an IDR value from an amount of sen.
// Useful for full precision, e.g. NewIDRFromSen(15050)
// creates Rp 150,50.
func NewIDRFromSen(sen int64) IDR {
    return IDR{sen: sen}
}

// Add returns the sum of two IDR values.
// This operation does not modify the receiver.
func (a IDR) Add(b IDR) IDR {
    return IDR{sen: a.sen + b.sen}
}

// Sub returns the difference of two IDR values.
// The result can be negative if b is greater than a.
func (a IDR) Sub(b IDR) IDR {
    return IDR{sen: a.sen - b.sen}
}

// Percent calculates a percentage of the IDR value.
// The result is rounded to the nearest sen using normal rounding.
//
// Example: IDR{15000000}.Percent(11) = Rp 1.650.000 (11% of Rp 15.000.000)
func (a IDR) Percent(pct int) IDR {
    // Multiply by integers before dividing to avoid
    // losing precision. Add 50 before dividing by 100 for rounding.
    result := (a.sen*int64(pct) + 50) / 100
    return IDR{sen: result}
}

// IsZero reports whether the IDR value is zero.
func (a IDR) IsZero() bool {
    return a.sen == 0
}

// IsNegative reports whether the IDR value is negative (debt/credit).
func (a IDR) IsNegative() bool {
    return a.sen < 0
}

// Rupiah returns the value in Rupiah units (not sen).
// The decimal value is rounded down.
func (a IDR) Rupiah() int64 {
    return a.sen / 100
}

// Format returns the string representation of the IDR value
// in the standard Indonesian format: "Rp 1.500.000,00".
func (a IDR) Format() string {
    // Handle negative values
    prefix := "Rp "
    sen := a.sen
    if sen < 0 {
        prefix = "-Rp "
        sen = -sen
    }

    rupiah := sen / 100
    remainder := sen % 100

    // Format the rupiah amount with thousands separators
    rupiahStr := formatWithThousands(rupiah)

    return fmt.Sprintf("%s%s,%02d", prefix, rupiahStr, remainder)
}

// String implements the fmt.Stringer interface.
// Output is the same as Format().
func (a IDR) String() string {
    return a.Format()
}

// formatWithThousands formats an integer with thousands separators (dots).
// Example: 1500000 → "1.500.000"
func formatWithThousands(n int64) string {
    s := fmt.Sprintf("%d", n)
    if len(s) <= 3 {
        return s
    }

    var result strings.Builder
    start := len(s) % 3
    if start > 0 {
        result.WriteString(s[:start])
    }
    for i := start; i < len(s); i += 3 {
        if i > 0 || start > 0 {
            result.WriteByte('.')
        }
        result.WriteString(s[i : i+3])
    }
    return result.String()
}

func main() {
    // Demonstration of using the currency package
    price := NewIDR(15000000)   // Rp 15.000.000
    discount := price.Percent(10) // 10% discount
    afterDiscount := price.Sub(discount)
    tax := afterDiscount.Percent(11) // 11% VAT
    total := afterDiscount.Add(tax)

    fmt.Printf("Original price: %s\n", price)
    fmt.Printf("10%% discount  : %s\n", discount)
    fmt.Printf("After discount : %s\n", afterDiscount)
    fmt.Printf("11%% VAT        : %s\n", tax)
    fmt.Printf("Total          : %s\n", total)

    // TODO: add support for other currencies (USD, EUR, etc.)
    // Discuss the API design in issue #15 before implementing
}

Output:

Original price: Rp 15.000.000,00
10% discount  : Rp 1.500.000,00
After discount: Rp 13.500.000,00
11% VAT       : Rp 1.485.000,00
Total         : Rp 14.985.000,00

Summary #

  • Two comment types: // for single lines (more common) and /* */ for multi-line or temporarily disabling code blocks.
  • Doc comments are written with // directly above a declaration with no blank line, starting with the identifier name and ending with a period.
  • Doc comment order: package → const/var → type → function/method.
  • Package comments usually live in a doc.go file for large packages, or in the main file.
  • Explain “why”, not “what” — good code already explains what it does; comments explain context, design decisions, and trade-offs.
  • Convention tags: TODO, FIXME, HACK, NOTE — easy to grep and communicate code status.
  • //go:generate directives for code generation; //go:build for build constraints.
  • go doc reads documentation from the terminal; godoc runs a local web server.
  • Don’t let comments go stale — an inaccurate comment is more dangerous than no comment at all.
  • Remove commented-out code from production code — use version control for history.

← Previous: Core Syntax   Next: Variables →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact