How do ast.Inspect and ast.Walk differ, and what stops a traversal descending into children?
answer
- one is a wrapper over the other
- the return value is the steering wheel
- a visitor for the children, or none
- you are called once more with nothing
basics
~20 sast.Walk drives an ast.Visitor whose Visit returns a visitor for the children, or nil to skip them. ast.Inspect wraps that in a func(ast.Node) bool where false skips children. Both call you again with a nil node when a subtree ends.
solid answer
~50 sBoth do a depth-first, pre-order traversal of a `go/ast` tree. `ast.Walk(v, node)` takes an `ast.Visitor` — an interface with `Visit(ast.Node) ast.Visitor`. Returning a visitor descends into that node's children with it; returning nil prunes the subtree. `ast.Inspect(node, f)` is the closure convenience: `f` returns a bool, and false means "do not descend". After a node's children have been visited, both call you once more with a **nil** node, which is your post-order hook — the place to pop a stack. Walk is the one to reach for when different subtrees need different handling or per-subtree state, since you can return a *different* visitor; Inspect is better when a single closure over some accumulator will do. Neither gives you parent pointers, so if you need a node's parent you push and pop a stack around those nil calls yourself.
code
go · 13 linestype funcNames struct{ names []string }
func (v *funcNames) Visit(n ast.Node) ast.Visitor {
fn, ok := n.(*ast.FuncDecl)
if !ok {
return v // keep descending with the same visitor
}
v.names = append(v.names, fn.Name.Name)
return nil // prune: the body is not walked
}
v := &funcNames{}
ast.Walk(v, file)go deeper
Know that ast.Inspect walks the whole tree depth-first and calls your function for every node, and that a type assertion or type switch inside it is how you pick out the node kind you care about.
Explain the contract precisely: pre-order visits, the return value controls descent, the extra nil callback marks the end of a subtree, and ast.Inspect is a thin wrapper over ast.Walk's visitor interface.
Demonstrate the habits a real pass needs — a parent stack kept correct against the nil call, aggressive pruning for speed over hundreds of files, and collecting edits rather than mutating slices mid-walk.
Own the design question of how much traversal machinery a team should hand-roll: a stack-and-closure pass is cheap and legible, but a rewrite that needs parents, positions and comment fidelity everywhere is a signal to reach for a proper rewriting layer instead.
## The two entry points `go/ast` ships exactly two traversal helpers, and one is written in terms of the other. ```go type Visitor interface { Visit(node ast.Node) (w ast.Visitor) } func Walk(v Visitor, node Node) func Inspect(node Node, f func(Node) bool) ``` `Walk` visits `node`, then — if `v.Visit(node)` returned a non-nil visitor `w` — walks each child with `w`, and finally calls `w.Visit(nil)`. `Inspect` is a thin adapter: it wraps your `func(ast.Node) bool` in a visitor that returns itself when the function returns true and nil when it returns false. So the control knobs are the same in both, spelled differently: | | descend | prune | end-of-subtree signal | |---|---|---|---| | `ast.Walk` | return a non-nil `ast.Visitor` | return `nil` | `Visit(nil)` on the returned visitor | | `ast.Inspect` | return `true` | return `false` | `f(nil)` | ## The nil call is not an accident The callback receiving a nil node is the only post-order hook you get, and it is what makes stack-keeping correct. A common pass looks like this: ```go var stack []ast.Node ast.Inspect(file, func(n ast.Node) bool { if n == nil { stack = stack[:len(stack)-1] // leaving a node return false } stack = append(stack, n) // entering a node; parent is stack[len(stack)-2] return true }) ``` Note the asymmetry: you are called with nil only for nodes you descended into. If you return false, no nil call follows for that node, so a naive push/pop that ignores the return value gets out of sync. Getting this wrong is the classic first bug in an AST pass. ## Choosing between them Use `ast.Inspect` when one closure over a shared accumulator answers the question: collect every `*ast.CallExpr`, find every function whose name matches a pattern, count something. It reads well and is the idiomatic default. Use `ast.Walk` when the *handling itself* changes by context. Because `Visit` returns the visitor used for the children, you can hand a different visitor down one subtree — for instance, one visitor for the top level that switches to a body-scoped visitor with its own state when it enters a `*ast.FuncDecl`. That is stateful traversal expressed in types instead of in a manually maintained stack, and it is the reason `Walk` still exists after `Inspect` was added. ## What traversal does not give you - **No parent pointers.** `go/ast` nodes link downward only. If your rewrite needs "the enclosing function" or "the statement this expression sits in", you track it yourself with the stack shown above. - **No early exit.** Neither helper has a stop mechanism. Returning false only prunes the current subtree; the walk continues elsewhere. To abort you set a flag and return false everywhere afterwards, or panic with a sentinel value and recover it at the call site — an accepted, if blunt, idiom. - **No editing API.** Traversal hands you nodes, not slots. Because `Decls`, `Stmts`, `Args` and friends are slices of interface values reached through pointers, you *can* assign through the node you are holding — replacing `call.Fun`, renaming `ident.Name` in place — but you cannot delete or insert a sibling without holding the parent. Structural edits made mid-walk while the walker is iterating that same slice are a good way to confuse yourself; the safer shape is to collect a list of edits during the walk and apply them afterwards. - **No positions for what you synthesise.** Nodes you construct have `token.NoPos`, which matters as soon as you print the tree back out. ## Node kinds A traversal callback is nearly always a type switch or a single type assertion: ```go switch n := n.(type) { case *ast.FuncDecl: case *ast.CallExpr: case *ast.SelectorExpr: // n.X is the qualifier, n.Sel the field or method name case *ast.Ident: } ``` When you are unsure what shape the parser produced for some construct, `ast.Print(fset, node)` dumps the tree with positions resolved; `ast.Fprint` with `ast.NotNilFilter` gives a less noisy version. Reading that dump once for the construct you are rewriting saves more time than guessing at field names. ## Practical checklist for a repo-wide pass 1. Prune aggressively — returning false on subtrees you cannot possibly care about (whole declarations, imported-nothing files) is the cheapest speedup available. 2. Keep the stack if you need parents, and pop only on the nil call for nodes you descended into. 3. Collect edits, then apply them; do not mutate slices you are still walking. 4. Remember the callback runs pre-order, so an outer node is seen before anything inside it — useful when the outer node tells you whether the inner ones matter.
- Your callback needs the enclosing function declaration for the node it is looking at. How do you get it?You maintain it yourself — `go/ast` nodes have no parent links. Keep a stack in the closure: push the node when the callback is entered with a non-nil node and you return true, pop it on the nil call that marks the end of that subtree. The enclosing declaration is then the nearest `*ast.FuncDecl` down the stack.
- How do you abort an ast.Inspect traversal as soon as you have found what you need?There is no built-in stop. Returning false only prunes the current subtree. Either set a found flag and return false immediately on every subsequent call — cheap, but the walk still runs — or panic with a private sentinel value and recover it at the call site, which genuinely unwinds. The sentinel must be a type only your package can produce so you never swallow a real panic.
- Why does ast.Walk let Visit return a visitor at all, instead of just a bool?So the traversal can change behaviour by context. Returning a *different* visitor for a node's children gives you scoped state — one visitor for file level that hands off a body-scoped visitor on entering a function — without a manual stack. `ast.Inspect` collapses that to true/false because a single closure covers the common case.
saying these in an interview costs you the question
- Thinks returning false from ast.Inspect stops the whole traversal
- Pushes and pops a stack without handling the nil callback
- Expects ast.Node values to carry a parent pointer
- Believes ast.Walk visits children before the node itself
- Inserts or deletes siblings in a slice being walked