Go Command — Find the Bug¶
Practice finding and fixing bugs in Go code related to the
gocommand, build process, module management, and common toolchain issues.
How to Use¶
- Read the buggy code carefully
- Try to find the bug without looking at the hint
- Write the fix yourself before checking the solution
- Understand why the bug happens — not just how to fix it
Difficulty Levels¶
| Level | Description |
|---|---|
| 🟢 | Easy — Common beginner Go mistakes, wrong flags, basic module issues |
| 🟡 | Medium — Logic errors, subtle build behavior, module dependency problems |
| 🔴 | Hard — Race conditions, CGO issues, cross-compilation edge cases, linker problems |
Bug 1: Missing module initialization 🟢¶
What the code should do: Compile and print "Hello, World!"
// main.go — in a new directory with no go.mod
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
Expected output:
Actual output:
Hint
What file does every Go project need before you can build?Bug Explanation
**Bug:** No `go.mod` file exists in the directory. **Why it happens:** Since Go 1.16, module-aware mode is the default. Without `go.mod`, Go does not know the module path and refuses to compile. **Impact:** Build fails completely.Fixed Code
**What changed:** Added `go mod init` before building.Bug 2: Running a multi-file package incorrectly 🟢¶
What the code should do: Print "Sum: 15"
// main.go
package main
import "fmt"
func main() {
result := Sum(1, 2, 3, 4, 5)
fmt.Printf("Sum: %d\n", result)
}
// math.go
package main
func Sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
Expected output:
Actual output:
Hint
How many files did you pass to `go run`? Does it know about `math.go`?Bug Explanation
**Bug:** `go run main.go` only compiles `main.go`, not `math.go`. **Why it happens:** When you specify individual files, `go run` only compiles those files. The `Sum` function is in `math.go` which was not included. **Impact:** Compilation error — `undefined: Sum`.Fixed Code
**What changed:** Used `go run .` to compile all `.go` files in the package.Bug 3: Wrong go get usage for installing tools 🟢¶
What the code should do: Install golangci-lint as a CLI tool.
# Inside a project directory with go.mod
go get github.com/golangci/golangci-lint/cmd/golangci-lint@latest
Expected result: golangci-lint binary available in PATH.
Actual result: The dependency is added to go.mod but no binary is installed. Running golangci-lint gives "command not found."
Hint
Since Go 1.17, how should you install CLI tools?Bug Explanation
**Bug:** `go get` in module-aware mode only updates `go.mod`. It does not install binaries. **Why it happens:** Since Go 1.17, `go get` is for managing dependencies, not installing tools. Using `go get` pollutes your project's `go.mod` with a tool dependency. **Impact:** Tool not installed; `go.mod` unnecessarily modified.Fixed Code
**What changed:** Replaced `go get` with `go install` for tool installation.Bug 4: Version injection with const instead of var 🟡¶
What the code should do: Print the injected version when built with -ldflags.
package main
import "fmt"
const version = "dev"
func main() {
fmt.Printf("Version: %s\n", version)
}
Expected output:
Actual output:
Hint
What is the difference between `const` and `var` in Go? Can the linker modify constants?Bug Explanation
**Bug:** `version` is declared as `const`, but `-ldflags -X` can only modify `var` declarations. **Why it happens:** Constants in Go are resolved at compile time and baked into the binary. The linker's `-X` flag modifies package-level string *variables* at link time, after compilation. It cannot change constants. **Impact:** Version is always "dev" regardless of `-ldflags`.Fixed Code
**What changed:** Changed `const` to `var`.Bug 5: Test cache hiding a broken test 🟡¶
What the code should do: Test that always passes.
// main_test.go
package main
import (
"os"
"testing"
)
func TestFileExists(t *testing.T) {
_, err := os.Stat("/tmp/test-data.txt")
if err != nil {
t.Fatal("test data file not found")
}
}
# First, create the file
echo "data" > /tmp/test-data.txt
# Run test — PASS
go test -v ./...
# Delete the file
rm /tmp/test-data.txt
# Run test again — still PASS!
go test -v ./...
Expected output after file deletion:
Actual output after file deletion:
Hint
What does `(cached)` mean? Does Go re-run the test?Bug Explanation
**Bug:** Go caches test results. The second run uses the cached PASS result without re-executing the test, even though the external file was deleted. **Why it happens:** Go's test cache is based on the test binary, flags, and Go source files — NOT external files. Since nothing in the Go code changed, the cache hit returns the old result. **Impact:** Test appears to pass but the actual condition is broken. This is dangerous for tests that depend on external state.Fixed Code
# Option 1: Disable test cache
go test -count=1 -v ./...
# Option 2: The test itself should create its own data (better design)
func TestFileExists(t *testing.T) {
// Create test data as part of the test — don't rely on external state
tmpFile := t.TempDir() + "/test-data.txt"
if err := os.WriteFile(tmpFile, []byte("data"), 0644); err != nil {
t.Fatal(err)
}
_, err := os.Stat(tmpFile)
if err != nil {
t.Fatal("test data file not found")
}
}
Bug 6: Build tag syntax error (silent failure) 🟡¶
What the code should do: Only compile debug.go when the debug tag is provided.
// debug.go
// +build debug
package main
import "log"
func init() {
log.Println("DEBUG MODE ENABLED")
}
go build -tags debug -o server
./server
# Expected: "DEBUG MODE ENABLED" appears in output
# Actual: No debug message — file was silently excluded
Hint
Look at the build constraint syntax carefully. Is there a required blank line?Bug Explanation
**Bug:** The `// +build debug` comment must be followed by a blank line before the `package` declaration. Without the blank line, Go treats it as a regular comment, not a build constraint. **Why it happens:** The old-style `// +build` constraint requires a blank line separator. Without it, the directive is ignored silently — no error, no warning. **Impact:** The file is always excluded (or always included, depending on placement), defeating the purpose of the build tag.Fixed Code
**What changed:** Used the new `//go:build` syntax (Go 1.17+) which does NOT require a blank line and gives a compile error if malformed. This is the recommended syntax going forward.Bug 7: go mod tidy removing needed test dependency 🟡¶
What the code should do: Tests import a testing utility, but go mod tidy removes it.
// main_test.go
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSomething(t *testing.T) {
assert.Equal(t, 42, 42)
}
go mod tidy
go test ./...
# Error: cannot find module providing package github.com/stretchr/testify/assert
Hint
Did you add the dependency before running `go mod tidy`? Or did you run `go mod tidy` before writing the test file?Bug Explanation
**Bug:** The test file was created AFTER running `go mod tidy`, or `go mod tidy` was run when the test file had a syntax error or was not saved. **Why it happens:** `go mod tidy` scans all `.go` files including `_test.go` files to find imports. If the test file was not present or had errors when `go mod tidy` ran, the dependency is treated as unused and removed. **Impact:** Tests fail because the dependency is missing from `go.mod`.Fixed Code
**What changed:** Ensure all source files (including tests) exist before running `go mod tidy`.Bug 8: Data race not caught without -race flag 🔴¶
What the code should do: Safely increment a counter from multiple goroutines.
package main
import (
"fmt"
"sync"
)
var counter int
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++
}()
}
wg.Wait()
fmt.Println("Counter:", counter)
}
Expected output:
Actual output:
Hint
Run with `go run -race main.go`. What does it report?Bug Explanation
**Bug:** `counter++` is a data race — multiple goroutines read and write `counter` simultaneously without synchronization. **Why it happens:** `counter++` is not atomic. It is actually three operations: read, increment, write. When two goroutines execute simultaneously, one goroutine's increment can be lost. **Impact:** Incorrect counter value. Without `-race`, the program "appears" to work but gives wrong results. The bug is intermittent and hard to reproduce. **Go spec reference:** The Go memory model states that concurrent access to shared variables must be synchronized.Fixed Code
**What changed:** Replaced `int` with `atomic.Int64` and used `Add(1)` for atomic increment. **Alternative fix:** Use `sync.Mutex` to protect the counter, or use a channel-based approach.Bug 9: CGO binary fails in scratch container 🔴¶
What the code should do: Run a Go HTTP server in a Docker scratch container.
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN go build -o server ./cmd/server
FROM scratch
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]
Expected output:
Actual output:
Hint
Check if the binary is statically linked. What does `CGO_ENABLED` default to in the golang Docker image?Bug Explanation
**Bug:** The Go binary is dynamically linked against glibc because `CGO_ENABLED=1` is the default in the `golang` Docker image. The `scratch` container has no shared libraries. **Why it happens:** When CGO is enabled, the Go net package uses the system DNS resolver (`libc`), which requires `libnss_dns.so`, `libresolv.so`, etc. These libraries exist in the builder stage but not in `scratch`. **Impact:** Container crashes immediately with a confusing "no such file or directory" error (it is looking for the dynamic linker `ld-linux-x86-64.so.2`). **How to detect:** `file server` shows "dynamically linked" instead of "statically linked". Or `ldd server` shows shared library dependencies.Fixed Code
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o server ./cmd/server
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]
Bug 10: Cross-compilation silently uses wrong architecture 🔴¶
What the code should do: Build a Linux ARM64 binary on an AMD64 machine.
GOOS=linux GOARCH=arm64 go build -o server-arm64 ./cmd/server
file server-arm64
# Expected: ELF 64-bit LSB executable, ARM aarch64
// cmd/server/main.go
package main
/*
#include <stdio.h>
void greet() { printf("Hello from C!\n"); }
*/
import "C"
import "fmt"
func main() {
C.greet()
fmt.Println("Server starting...")
}
Expected output:
Actual output:
Hint
The code uses CGO (`import "C"`). What does cross-compilation with CGO require?Bug Explanation
**Bug:** The code uses CGO (`import "C"`), but cross-compiling with CGO requires a C cross-compiler for the target architecture. The system's default `gcc` targets AMD64, not ARM64. **Why it happens:** When `import "C"` is present, Go automatically enables CGO. Cross-compiling then tries to use the host's C compiler, which produces code for the wrong architecture. **Impact:** Build fails with linker errors, or worse — produces a binary that crashes on the target platform.Fixed Code
# Option 1: Remove CGO dependency (preferred)
# Remove the import "C" and C code, use pure Go alternatives
# Option 2: Provide a cross-compiler
CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc \
GOOS=linux GOARCH=arm64 \
go build -o server-arm64 ./cmd/server
# Option 3: Build inside a Docker container matching the target
docker buildx build --platform linux/arm64 -t myserver .
Bug 11: go:embed path not found 🔴¶
What the code should do: Embed all HTML templates.
package main
import (
"embed"
"fmt"
)
//go:embed ../templates/*.html
var templates embed.FS
func main() {
entries, _ := templates.ReadDir("templates")
for _, e := range entries {
fmt.Println(e.Name())
}
}
Hint
Can `go:embed` paths go outside the package directory?Bug Explanation
**Bug:** `go:embed` does not allow paths that go above the package directory (no `..` paths). **Why it happens:** For security reasons, `go:embed` only allows embedding files within the package's directory tree. Paths with `..` or absolute paths are rejected. **Impact:** Build fails with a pattern syntax error.Fixed Code
// Move the embed directive to a package at or above the templates directory
// Or restructure your project so templates are within the package:
//
// cmd/server/
// ├── main.go
// └── templates/
// ├── index.html
// └── about.html
//go:embed templates/*.html
var templates embed.FS
func main() {
entries, _ := templates.ReadDir("templates")
for _, e := range entries {
fmt.Println(e.Name())
}
}
Score Card¶
| Bug | Difficulty | Found without hint? | Understood why? | Fixed correctly? |
|---|---|---|---|---|
| 1 | 🟢 | ☐ | ☐ | ☐ |
| 2 | 🟢 | ☐ | ☐ | ☐ |
| 3 | 🟢 | ☐ | ☐ | ☐ |
| 4 | 🟡 | ☐ | ☐ | ☐ |
| 5 | 🟡 | ☐ | ☐ | ☐ |
| 6 | 🟡 | ☐ | ☐ | ☐ |
| 7 | 🟡 | ☐ | ☐ | ☐ |
| 8 | 🔴 | ☐ | ☐ | ☐ |
| 9 | 🔴 | ☐ | ☐ | ☐ |
| 10 | 🔴 | ☐ | ☐ | ☐ |
| 11 | 🔴 | ☐ | ☐ | ☐ |
Rating:¶
- 11/11 without hints → Senior-level Go toolchain expertise
- 8-10/11 → Solid Go middle-level understanding
- 5-7/11 → Good junior, keep practicing
- < 5/11 → Review the topic fundamentals first