From 7d7e650261f5a5192a07ed04b89eb162a6ea30b9 Mon Sep 17 00:00:00 2001 From: dipto0321 Date: Thu, 2 Jul 2026 03:21:52 +0600 Subject: [PATCH] docs: add zero-knowledge Go learning guide and AI-content-correction issue template LEARN_GO_AND_NODEUP.md teaches Go from scratch using real lines from this codebase (structs, pointers, the (value, error) return pattern, interfaces, context.Context) rather than generic examples, for anyone approaching the project with no Go background. Since it (and REVIEW_FINDINGS.md) were AI-generated, also add an issue template so readers can flag inaccurate or hallucinated content for correction. Co-Authored-By: Claude Sonnet 5 --- .../ISSUE_TEMPLATE/ai_content_correction.md | 27 ++ LEARN_GO_AND_NODEUP.md | 397 ++++++++++++++++++ 2 files changed, 424 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/ai_content_correction.md create mode 100644 LEARN_GO_AND_NODEUP.md diff --git a/.github/ISSUE_TEMPLATE/ai_content_correction.md b/.github/ISSUE_TEMPLATE/ai_content_correction.md new file mode 100644 index 0000000..983fedb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ai_content_correction.md @@ -0,0 +1,27 @@ +--- +name: AI content correction +about: Report inaccurate, outdated, or hallucinated info in AI-generated docs (e.g. LEARN_GO_AND_NODEUP.md) or AI-authored PR/issue content +title: 'docs(ai): ' +labels: ['documentation'] +assignees: [] +--- + +## Where is the problem? + +File and section/line (e.g. `LEARN_GO_AND_NODEUP.md`, section 4 +"Functions, multiple return values...", or a specific PR/issue/comment +authored by an AI assistant). + +## What does it currently say? + +Quote or paraphrase the claim. + +## What's wrong with it? + +Is it factually incorrect, outdated (the code changed since it was +written), misleading, or just confusing? A code snippet or line number +that contradicts the claim is the most useful thing you can include. + +## Suggested correction (optional) + +If you already know what it should say instead, note it here. diff --git a/LEARN_GO_AND_NODEUP.md b/LEARN_GO_AND_NODEUP.md new file mode 100644 index 0000000..8472b65 --- /dev/null +++ b/LEARN_GO_AND_NODEUP.md @@ -0,0 +1,397 @@ +# Learning Go Through `nodeup` — A Guide From Zero + +This document teaches Go to someone who has never written a line of it, +using a real, working project (`nodeup`) instead of toy examples. Every +concept below is paired with an actual line from this codebase. It's meant +for anyone approaching `nodeup` with zero Go background — a new +contributor, a reviewer, or the maintainer — read it top to bottom once, +then keep it as a reference while you read the actual source. + +> **A note on how this document was made:** this guide, and the companion +> `REVIEW_FINDINGS.md` in this repo, were generated by an AI assistant +> (Claude) reading the codebase. That means two things worth keeping in +> mind: +> +> 1. It's a genuinely useful starting map, cross-checked against the real +> source at the time it was written — not a generic Go tutorial with +> the project's name pasted in. +> 2. It can still be wrong. AI-generated explanations can misstate how +> something works, go stale as the code changes, or occasionally just +> hallucinate a detail that sounds plausible but isn't true. Treat every +> claim here as "probably right, cross-check against the actual file +> and line referenced" rather than gospel. +> +> If you spot something here that's inaccurate, outdated, or just doesn't +> match what the code actually does, please file it — use the **"AI +> content correction"** issue template +> (`.github/ISSUE_TEMPLATE/ai_content_correction.md`) so mistakes get +> tracked and fixed rather than quietly misleading the next reader. + +--- + +## 1. The big picture: what even is Go, and why does this project use it + +Go (a.k.a. Golang) is a compiled, statically-typed language made by Google. +"Compiled" means `.go` source files get turned into a single standalone +executable file (a "binary") — unlike Node.js or Python, there's no +interpreter needed on the end user's machine. That's *why* `nodeup` is +written in Go: it's a tool that manages Node.js installs, so it can't +require Node.js to already be working correctly just to run itself (the +"bootstrap paradox"). + +"Statically typed" means every variable's type (string, number, etc.) is +known at compile time, not figured out while the program runs. This is +different from JavaScript, where a variable can hold a string one moment +and a number the next. In Go, once something is declared a `string`, it's a +`string` forever. + +## 2. Reading a real file, line by line + +Open `internal/node/dist.go` (or just read the excerpt below — it's real +code from this project) and let's go through it. + +```go +// Package node provides access to the nodejs.org/dist/index.json API. +// It fetches, caches, and resolves Node.js versions (LTS, Current, etc.). +package node + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/Masterminds/semver/v3" +) +``` + +- **`package node`** — every Go file belongs to a "package," which is + Go's word for a module/namespace. All files in the same folder + (`internal/node/`) must declare the same package name. This is + roughly like a Python module or a JS file that exports things — except + in Go, *the folder* is the unit, not the file. +- **`import (...)`** — like `require()` in Node.js or `import` in + JavaScript. Each line is a package path. `"encoding/json"`, `"fmt"`, + `"io"`, `"net/http"`, `"os"`, `"path/filepath"`, `"time"` are all part + of Go's **standard library** — they ship with Go itself, no npm-style + install needed. `"github.com/Masterminds/semver/v3"` is a **third-party + dependency** — the equivalent of something in `package.json`, except + Go's version is `go.mod` (more on that in section 6). + +### Comments as documentation + +Notice the `// Package node provides...` comment directly above `package +node`. In Go, a comment immediately above a declaration (a package, a +function, a type) *is* that thing's official documentation — tools like +`go doc` extract it automatically. This is why this project's CLAUDE.md +says "no exported names without a godoc comment" — every public +function/type is expected to have one of these directly above it. + +## 3. Types: structs, and the concept of a "shape" + +```go +type ManifestVersion struct { + Version string `json:"version"` + Date string `json:"date"` + LTSCodename *string `json:"lts"` +} +``` + +- **`type ... struct { ... }`** defines a new named "shape" — think of it + like a TypeScript `interface` or a Python `dataclass`, except Go doesn't + have classes at all (see section 5). +- Each line inside is a **field**: a name, then its type. `Version string` + means "a field called Version, which holds text." No `:` like + TypeScript, no `=` — just name then type, separated by whitespace. +- **`` `json:"version"` ``** is a *struct tag* — metadata attached to a + field, read by the `encoding/json` package at runtime. It tells Go's + JSON decoder "when you see a JSON key called `version`, put its value + into this field." This is how `dist.go` turns nodejs.org's raw JSON + response into a usable Go value. +- **`*string`** — the `*` means "pointer to a string," which in Go's + idiom here really means "either nil (absent), or a string." Go doesn't + have `null`/`undefined` the way JS does; a plain `string` can never be + "missing," it defaults to `""` (empty string) if unset. To represent + "this field might genuinely be absent," Go code uses a pointer, because + a pointer's zero-value is `nil` (Go's version of "nothing here"). + This exact trick is why `LTSCodename` is `*string`: nodejs.org's `lts` + JSON field is either `false` (not LTS) or a string codename like + `"Iron"` (is LTS) — `nil` means "not LTS," a non-nil pointer means "LTS, + and here's its codename." + +## 4. Functions, multiple return values, and Go's approach to errors + +This is the single biggest mental shift coming from JS/Python. Look at this +real function: + +```go +func (m Manifest) LatestLTS() (*ManifestVersion, error) { + var latest *ManifestVersion + var latestSem *semver.Version + + for i := range m { + if m[i].LTSCodename == nil { + continue + } + v, err := parseVersion(m[i].Version) + if err != nil { + continue + } + if latestSem == nil || v.GreaterThan(latestSem) { + latestSem = v + latest = &m[i] + } + } + + if latest == nil { + return nil, fmt.Errorf("no LTS version found in manifest") + } + return latest, nil +} +``` + +- **`func (m Manifest) LatestLTS() (*ManifestVersion, error)`** — this + declares a function called `LatestLTS` that is attached to the + `Manifest` type (the `(m Manifest)` part is called a *receiver* — it's + Go's substitute for "methods on a class"; more in section 5). It + returns **two values**: a `*ManifestVersion` (the result, or nil if + none found) and an `error` (nil if everything went fine, non-nil if + something went wrong). +- **This two-value return is Go's entire error-handling philosophy.** + There are no `try`/`catch` blocks for normal error flow in Go (there IS + a `panic`/`recover` mechanism, but it's reserved for truly exceptional, + unrecoverable situations — not everyday error handling). Instead, every + function that can fail returns an `error` as its *last* return value, + and the caller is expected to immediately check it: + + ```go + v, err := parseVersion(m[i].Version) + if err != nil { + continue + } + ``` + + Read this as: "try to parse the version; if that produced an error, + skip this entry and move to the next one." This `if err != nil { ... + }` pattern appears *everywhere* in Go code — you'll see it dozens of + times per file. It's not boilerplate to be annoyed by; it's the + language's core discipline: **you cannot use a value without + acknowledging whether it succeeded.** +- This is exactly why this project's CLAUDE.md says *"errcheck is enabled + and treated as a bug. Every error return must be handled."* The linter + `errcheck` scans the code for any place a function returns an error that + then never gets looked at — which in Go is very easy to do by accident + (unlike JS, an unhandled error doesn't throw; it just sits there in a + variable nobody checked). +- **`for i := range m`** — a loop. `range` over a slice (Go's dynamic + array, see section 5) gives you the index `i` each time through. This is + like `for (let i = 0; i < m.length; i++)` in JS, just shorter. +- **`:=`** is Go's "declare and assign" operator. `v, err := parseVersion(...)` + means "create two new variables, `v` and `err`, and assign them the two + return values from this call." Compare to `var latest *ManifestVersion` + earlier, which declares a variable *without* assigning it yet (it + starts at its zero value — `nil` for a pointer). +- **`&m[i]`** — the `&` operator means "give me a pointer to this value" + (the address-of operator). `latest = &m[i]` stores a pointer to the + i-th element of the manifest, not a copy of it. + +### Error wrapping + +Elsewhere in the same file: + +```go +return nil, fmt.Errorf("fetch manifest from %s: %w", url, err) +``` + +`fmt.Errorf` is like a `sprintf` for building error messages. The special +verb **`%w`** ("wrap") embeds the *original* error inside the new one, +preserving the full chain so you can later ask "was this ultimately caused +by a network timeout?" without losing the outer context ("fetch manifest +from https://...: dial tcp: i/o timeout"). This wrap-with-context pattern +is idiomatic Go and shows up throughout `internal/`. + +## 5. No classes — structs + methods + interfaces instead + +Go has no `class` keyword. Instead, it composes three things to get +class-like behavior: + +**A struct** (the data): +```go +type Config struct { + Manager string + Track TrackConfig +} +``` + +**A method** (a function "attached" to that struct via a receiver): +```go +func (c *Config) IsValid() bool { + ... +} +``` +Read `(c *Config)` as "this function can be called as `myConfig.IsValid()`, +and inside the function, `c` refers to that specific config." The `*` means +it receives a *pointer* to the struct (so it can see/modify the real one), +not a copy. + +**An interface** (a contract — "any type that has these methods counts"): +```go +// internal/detector/detector.go (conceptually) +type Manager interface { + Name() string + Detect() bool + Version() (string, error) + ListInstalled() ([]semver.Version, error) +} +``` +This is the most important interface in the whole codebase. It says: +"anything that has a `Name()` method returning a string, a `Detect()` +method returning bool, etc., **counts as** a `Manager` — it doesn't matter +what struct it actually is." That's how `fnm.go`, `nvm.go`, `volta.go`, and +five other files can all be treated identically by `detector.go`'s +`DetectAll()` function, even though they're completely separate types with +totally different internals (`nvm` shells out to bash, `fnm` calls a real +binary directly). This is Go's version of polymorphism — instead of "class +Fnm extends Manager," Go says "anything shaped like a Manager IS a +Manager," checked automatically by the compiler. This is called +**structural typing** or "duck typing, but checked at compile time." + +## 6. Packages, modules, and this project's folder structure + +- **`go.mod`** (at the repo root) is the Go equivalent of `package.json`. + Open it — you'll see: + ``` + module github.com/dipto0321/nodeup + go 1.24.0 + require ( + github.com/Masterminds/semver/v3 v3.5.0 + github.com/spf13/cobra v1.10.2 + gopkg.in/yaml.v3 v3.0.1 + ) + ``` + `module github.com/dipto0321/nodeup` names the whole project (Go uses + the eventual GitHub URL as the name — that's just Go's convention for + making import paths globally unique). The `require` block lists the + actual dependencies — Cobra (CLI framework), Masterminds/semver (version + comparison), yaml.v3 (config file parsing). Three total. That's it — the + project's "no new dependencies without a rationale line" rule is easy to + honor when the list is this short. +- **`go.sum`** is like `package-lock.json` — exact cryptographic hashes of + every dependency version, so builds are reproducible and tamper-evident. + Never hand-edit it. +- **`internal/`** is a folder name Go treats specially: any package inside + a folder called `internal/` can **only** be imported by code that lives + in the same repo, at or above that `internal/`'s parent directory. This + is enforced by the Go compiler itself, not a convention — try to `import + "github.com/dipto0321/nodeup/internal/config"` from a different project + and it simply won't compile. That's why CLAUDE.md notes "`internal/` + packages cannot be imported by external projects (Go enforces this)" — + it's a real language feature, used here to say "everything under + `internal/` is nodeup's own business logic, not a public library." +- **`cmd/nodeup/main.go`** is the entrypoint. By Go convention, the + `cmd//` folder pattern is where you put the tiny file that + just wires everything else together and calls `main()` — Go requires + exactly one function named `main()` in a package named `main` as the + program's starting point, the same way C requires `int main()`. + +## 7. Concurrency vocabulary you'll see (even if unused yet) + +Go is famous for **goroutines** (`go someFunc()` — runs a function +concurrently) and **channels** (`chan int` — a pipe for passing values +between goroutines safely). Skimming this codebase, `nodeup` doesn't +currently use either — every operation is sequential (detect, then fetch, +then snapshot, then install...). You may see this vocabulary in library +code the project depends on (Cobra, the HTTP client) even without ever +writing a `go func(){}()` yourself. If a future feature parallelizes, say, +fetching data for multiple managers at once, this is the mechanism that +would be used — for now, it's safe to ignore. + +## 8. Testing in Go + +Go's test framework is built into the standard library — no Jest/Mocha +needed. A file named `*_test.go` (e.g. `internal/node/dist_test.go`) sits +next to the code it tests, and each test is just a function starting with +`Test`: + +```go +func TestLatestLTS(t *testing.T) { + ... + if got.Version != "v20.15.0" { + t.Errorf("expected v20.15.0, got %s", got.Version) + } +} +``` + +`t *testing.T` is a handle the test uses to report failures — `t.Errorf` +marks the test failed but keeps running (compare to `t.Fatalf`, which +stops immediately). Run all tests with `go test ./...` (the `...` means +"this package and every package below it") — this is exactly what `make +test` runs, plus `-race` (detects concurrency bugs) and +`-coverprofile=coverage.out` (measures what % of lines the tests exercise). + +## 9. The specific tools enforcing this project's rules + +You don't need to memorize these, but it helps to know what they're for +when CI fails: + +| Tool | What it does | Why nodeup uses it | +|---|---|---| +| `go build` | Compiles the code into a binary | Catches type errors, missing imports | +| `go vet` | Catches suspicious-but-technically-legal code | e.g. a format string that doesn't match its arguments | +| `golangci-lint` | Runs ~15 linters at once (a "linter of linters") | Enforces the errcheck/contextcheck/gocritic rules CLAUDE.md mentions | +| `go test -race` | Runs the tests, watching for data races | Catches two goroutines touching the same memory unsafely | +| `gofmt` (used by golangci-lint) | Auto-formats code to one canonical style | So diffs are never about whitespace | + +`errcheck` (mentioned throughout CLAUDE.md) is one linter *inside* +golangci-lint specifically dedicated to "was an error return value ever +looked at." `contextcheck` is another, dedicated to "is +`context.Background()` being used where an existing context should be" — +this came up repeatedly in the code review that produced `REVIEW_FINDINGS.md`, +so it's worth understanding *why* it matters: + +### What is a `context.Context`, really? + +A `Context` is Go's way of carrying a "this operation might get cancelled" +signal through a chain of function calls. When a user presses Ctrl-C on a +running `nodeup upgrade`, or when a future timeout fires, that signal needs +to reach all the way down into whatever subprocess (`npm install -g ...`) +is currently running, so it can be killed instead of running to +completion. Cobra (the CLI framework used here) gives every command a +`cmd.Context()` that's wired to exactly this kind of cancellation. When +code instead calls `context.Background()` (a context that can *never* be +cancelled — it's a placeholder for "no context available"), it silently +breaks that chain: Ctrl-C stops responding for that operation. This is +why several review findings in `REVIEW_FINDINGS.md` are about spotting +`context.Background()` where `cmd.Context()` should have been threaded +through instead. + +## 10. How to actually read the rest of the codebase now + +A practical reading order, using what you now know: + +1. Start at `cmd/nodeup/main.go` — the entrypoint, tiny. +2. Go to `internal/cli/root.go` — see how subcommands register themselves. +3. Pick one subcommand file, e.g. `internal/cli/version.go` — it's the + simplest, good for confirming you can follow a `RunE` function + end-to-end. +4. Then `internal/detector/detector.go` — the `Manager` interface, the + core abstraction everything else builds on. +5. Then whichever `internal/detector/.go` file matches a version + manager you personally use (e.g. `fnm.go` if you use fnm) — seeing + real, concrete code that talks to a tool you already understand + operationally will cement the patterns fastest. + +Every time you hit an unfamiliar piece of syntax, search this document +first — most of Go's "new-looking" syntax is one of: a struct tag, a +pointer, a multiple-return-with-error, a receiver method, or an interface. +There genuinely isn't much more to the language than that plus the +standard library. + +--- + +*Found something in this document that's wrong, stale, or doesn't match +the actual code? Please file it using the "AI content correction" issue +template — see `.github/ISSUE_TEMPLATE/ai_content_correction.md`.*