Go Commands & Documentation — Find the Bug¶
Each section presents a scenario with a hidden issue. Try to find it before opening the hint/solution.
Bug 1 🟢 — Wrong Doc Comment Format¶
package calculator
// This function adds two numbers together and returns their sum.
// It can handle both positive and negative integers.
func add(a, b int) int {
return a + b
}
// The exported version of add.
func Add(a, b int) int {
return a + b
}
Running go doc calculator.Add shows nothing useful.
Hint
What is the rule for doc comments? Should they start with any specific text? Is the `add` function exported?Solution
**Bugs:** 1. `add` (lowercase) is unexported — it won't appear in `go doc` at all, but the doc comment format is still wrong 2. `Add` (uppercase) has a doc comment that doesn't follow the convention: it should start with the function name: `// Add returns the sum of a and b.` **Fix:** **Rule:** Doc comments for exported symbols must start with the symbol's name. `go doc` uses this convention to identify what is being described. IDEs and pkg.go.dev display this as the first sentence.Bug 2 🟢 — Build Tag Syntax Error¶
// +build linux
//go:build linux
package main
import "fmt"
func main() {
fmt.Println("Linux only!")
}
This file never compiles on Linux.
Hint
What is the required order of build constraint comments relative to the package statement? Is there something between the build tag and `package`?Solution
**Bug:** The build constraints must be followed by a blank line before the `package` statement. Without the blank line, Go does not recognize them as build constraints. Additionally, in modern Go (1.17+), the `//go:build` line should come BEFORE the `// +build` line (for backward compatibility). **Fix:** The blank line between the build tag block and `package` is mandatory. Without it, the tag is treated as a regular comment.Bug 3 🟢 — go generate Directive in Wrong Place¶
package main
import "fmt"
// Direction represents a compass direction.
type Direction int
// go:generate stringer -type=Direction
const (
North Direction = iota
South
East
West
)
func main() {
fmt.Println(North)
}
Running go generate ./... does nothing.
Hint
The `//go:generate` directive has a specific format. Look carefully at the comment.Solution
**Bug:** The generate directive is `// go:generate` (with a space between `//` and `go`). It should be `//go:generate` (no space). Go only recognizes `//go:generate` (no space) as a generate directive. `// go:generate` is a regular comment. **Fix:** Note: The `//go:generate` directive should be placed directly before the type declaration it generates for, not after it.Bug 4 🟢 — Ignoring go vet Warnings¶
package main
import "fmt"
func printUserInfo(name string, age int) {
fmt.Printf("User: %s, Age: %d", name) // forgot to pass age
}
func main() {
printUserInfo("Alice", 30)
}
The code compiles but the output is missing age.
Hint
Run `go vet ./...` on this code. What does it report?Solution
**Bug:** `fmt.Printf` format string has `%d` but the corresponding argument `age` is not passed. The output would be: `User: Alice, Age: %!d(MISSING)`. `go vet` would catch this: **Fix:** **Lesson:** Always run `go vet ./...` and take its warnings seriously. This is exactly the class of bug it's designed to catch.Bug 5 🟡 — GOPROXY Causing Silent Failures¶
Scenario: A developer sets up a new machine and clones the company's private repo. When running go build, they get:
go: github.com/company/private-module@v1.2.3:
reading https://proxy.golang.org/github.com/company/private-module/@v/v1.2.3.info:
410 Gone
Hint
What does `go env GOPROXY` show? Why would the public proxy return 410 for a private module?Solution
**Bug:** The `GOPROXY` is set to `https://proxy.golang.org,direct` (default). The public proxy doesn't have access to private company modules and returns 410 (Gone) instead of trying `direct` in some cases. **Fix:**# Option 1: Use GONOSUMDB to bypass the public proxy for private modules
go env -w GONOSUMDB="github.com/company/*"
go env -w GONOSUMCHECK="github.com/company/*"
# Option 2: Use GOPRIVATE (sets both GONOSUMDB and GONOSUMCHECK)
go env -w GOPRIVATE="github.com/company/*"
# Option 3: Use a corporate proxy that has access
go env -w GOPROXY="https://corp-proxy.company.com,direct"
# Option 4: Bypass proxy entirely for this domain
go env -w GOPROXY="direct" # not recommended for all packages
Bug 6 🟡 — go generate Not Committed¶
# .gitignore
*_string.go # generated by stringer
*.pb.go # generated by protoc
mock_*.go # generated by mockgen
The CI pipeline keeps failing with errors about missing types.
Hint
What is the Go philosophy about generated files? Should they be committed or regenerated on each build?Solution
**Bug:** Generated files should NOT be in `.gitignore`. In Go's workflow: 1. Developers run `go generate ./...` locally 2. Generated files are committed to version control 3. CI builds use the committed generated files WITHOUT running `go generate` This ensures: - Reproducible builds (no need for generators in CI) - Version-controlled generated code (visible in diffs) - Faster CI (no generator installation needed) **Fix:** **Alternative:** If generated files are large and change frequently, some teams DO use the "regenerate in CI" approach. But then CI must have all generators installed, which adds complexity.Bug 7 🟡 — Missing Error Check After go build¶
#!/bin/bash
# deploy.sh
echo "Building..."
go build -o /tmp/myapp ./cmd/server
echo "Deploying..."
scp /tmp/myapp server:/usr/local/bin/myapp
ssh server "systemctl restart myapp"
echo "Done!"
The deployment script sometimes deploys the OLD binary even when the build fails.
Hint
What does `go build` do with the output file when compilation fails? Does it overwrite the existing file?Solution
**Bug:** When `go build` fails: 1. It exits with a non-zero code 2. It does NOT create/update the output file `-o /tmp/myapp` However, the bash script doesn't check the exit code! If `/tmp/myapp` already exists from a previous successful build, it proceeds with the old binary. **Fix:** `set -e` at the top makes the script exit on any non-zero exit code. The explicit `if !` check provides a better error message.Bug 8 🟡 — Wrong go doc Package Path¶
# Developer is in ~/projects/myapp
# They want to see docs for their internal package
go doc internal/auth
# Error: no such package: internal/auth
Hint
What is the difference between a file path and an import path? How should you reference your own module's packages?Solution
**Bug:** `go doc` accepts import paths, not filesystem paths. The correct way to reference internal packages uses either: 1. The full import path: `github.com/myorg/myapp/internal/auth` 2. The relative package path: `./internal/auth`# Option 1: Full import path
go doc github.com/myorg/myapp/internal/auth
# Option 2: Relative path (must be in the module root)
go doc ./internal/auth
# Option 3: List packages first to find the right path
go list ./...
# github.com/myorg/myapp/internal/auth
# Then:
go doc github.com/myorg/myapp/internal/auth
Bug 9 🔴 — Vendoring Causes Build Inconsistency¶
project/
├── vendor/
│ └── github.com/dep/package/
│ └── (files from 3 months ago)
├── go.mod (specifies dep v2.0.0)
├── go.sum
└── main.go
The CI builds pass with the vendored version, but a developer's local build uses a different version.
Hint
How does Go decide whether to use the `vendor/` directory or the module cache? What command would help you understand this?Solution
**Bug:** The vendor directory is out of sync with `go.mod`. This happens when: 1. `go.mod` was updated (e.g., `go get dep@v2.0.0`) without running `go mod vendor` 2. Or `vendor/` was manually modified 3. Developers without the vendor flag use the module cache (different version!) **Investigation:**# Check if vendor is in sync
go mod verify
# See what module versions are actually vendored
cat vendor/modules.txt # shows what versions are vendored
# Check for discrepancy
go list -m dep/package # go.mod version
cat vendor/modules.txt | grep dep/package # vendored version
Bug 10 🔴 — go:generate Breaks Cross-Platform CI¶
This works on the developer's Mac but fails on the Linux CI runner.
Hint
Is `bash` guaranteed to be available? Is the `-c` flag's behavior with newlines consistent across shells? What's the recommended approach?Solution
**Bug:** The `bash` command and specific `echo` behavior (`\n` as newline) are platform-dependent: - On macOS: `bash -c "echo 'x\ny'"` may or may not interpret `\n` depending on the shell - On Alpine Linux CI: `bash` might not be installed, or `echo` may not support `-e` - Windows: `bash` definitely isn't available by default **Fix:**// Option 1: Use a Go program as the generator
//go:generate go run ./cmd/generate
// Option 2: Write a standalone Go generator
//go:generate go run gen.go
// gen.go (with //go:build ignore to exclude from normal build)
//go:build ignore
package main
import "os"
func main() {
content := []byte("package main\nvar Generated = true\n")
os.WriteFile("generated.go", content, 0644)
}
Bug 11 🔴 — Documentation Example Has Wrong Output Comment¶
// ExampleFormatBytes shows how to format byte sizes as human-readable strings.
func ExampleFormatBytes() {
sizes := []int64{1024, 1048576, 1073741824}
for _, s := range sizes {
fmt.Println(FormatBytes(s))
}
// Output:
// 1KB
// 1MB
// 1GB
}
go test ./... passes locally but fails in CI.
Hint
What environment-specific factors could cause `FormatBytes` to produce different output? Think about the output comment — is it exactly right?Solution
**Bug:** The `// Output:` comment must match the actual output EXACTLY, including: - Whitespace - Case - Line endings If `FormatBytes` produces `1 KB` (with space) instead of `1KB` (no space), the test fails. Alternatively, if CI runs on a different OS where float formatting differs, it can fail. **Investigation:** **Fix — Option 1:** Update the output comment to match actual output: **Fix — Option 2:** Make the function deterministic: **Lesson:** Example functions are tests. Run `go test ./...` every time you write or modify an example.Bug 12 🔴 — Build Cache Causes Stale Binary in Production¶
#!/bin/bash
# build-release.sh
export GOFLAGS="-ldflags=-X main.Version=$1"
go build -o release/myapp ./cmd/server
echo "Version $1 built"
The release binary keeps showing an old version even though $1 changes.
Hint
How does the build cache determine whether to recompile? Does changing a build flag invalidate the cache?Solution
**Bug:** Build flags ARE included in the cache key. However, `-ldflags` only affects the LINKER step, not the compiler. The object files may be cached and reused even when the ldflags change. But there's another issue: `GOFLAGS` in the environment may not always be picked up correctly, or the version variable might not be linked. The real issue is often this: the `-X main.Version` syntax requires the variable to exist and be a `var`, not a `const`:// WRONG — constants can't be set by ldflags
const Version = "dev"
// CORRECT — var can be set by ldflags
var Version = "dev"
# CORRECT syntax:
go build -ldflags="-X 'main.Version=$VERSION'" ./cmd/server
# If the package is not main but another package:
go build -ldflags="-X 'github.com/org/app/pkg/version.Version=$VERSION'" .
# Check the version is embedded correctly
./release/myapp --version
# or:
go version -m ./release/myapp | grep main.Version
#!/bin/bash
set -e
VERSION="${1:-dev}"
COMMIT=$(git rev-parse --short HEAD)
BUILD_TIME=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
go build \
-ldflags="-X 'main.Version=${VERSION}' -X 'main.Commit=${COMMIT}' -X 'main.BuildTime=${BUILD_TIME}'" \
-o release/myapp \
./cmd/server
echo "Built version ${VERSION} (${COMMIT})"
Bug 13 🔴 — go mod tidy Removes Needed Indirect Dependencies¶
// tools.go
//go:build tools
package tools
import (
_ "github.com/golang/mock/mockgen" // used by go generate
)
After running go mod tidy, mockgen's dependencies are removed from go.mod, breaking go generate.
Hint
Why would `go mod tidy` remove a dependency? What does it consider "used"? Is `tools.go` included in the normal build?Solution
**Bug:** `go mod tidy` removes dependencies that aren't imported by the code that will actually be compiled. The `//go:build tools` constraint means `tools.go` is NEVER compiled, so `go mod tidy` considers its imports unused and removes them. **Fix:**# Run go mod tidy, then immediately re-add the tools
go mod tidy
go get github.com/golang/mock/mockgen@v1.6.0
// tools.go
//go:build tools
package tools
// Import tools so go.mod tracks their versions.
// These imports keep the tool dependencies in go.sum
// even though they're not imported in production code.
import (
_ "github.com/golang/mock/mockgen"
_ "honnef.co/go/tools/cmd/staticcheck"
_ "golang.org/x/tools/cmd/stringer"
)
Summary Table¶
| # | Difficulty | Bug Type |
|---|---|---|
| 1 | 🟢 | Wrong doc comment format (doesn't start with symbol name) |
| 2 | 🟢 | Missing blank line after build tag |
| 3 | 🟢 | Space in // go:generate (should be //go:generate) |
| 4 | 🟢 | Ignored go vet warning for Printf format mismatch |
| 5 | 🟡 | GOPROXY blocking private module access |
| 6 | 🟡 | Generated files in .gitignore (should be committed) |
| 7 | 🟡 | Missing error check after go build in deploy script |
| 8 | 🟡 | Wrong package path format for go doc |
| 9 | 🔴 | Vendor directory out of sync with go.mod |
| 10 | 🔴 | Shell-dependent go:generate breaks on different platforms |
| 11 | 🔴 | Example // Output: comment doesn't match actual output |
| 12 | 🔴 | ldflags not embedded in variable (const instead of var) |
| 13 | 🔴 | go mod tidy removes tool dependencies |