With cells, candidates, and placement in hand, you can write a correct solver. Today you assemble them into recursive backtracking search - the walking skeleton that already solves real puzzles.
Solve a puzzle by trying each candidate in the first blank and recursing, backtracking on failure.
This is the payoff of the chapter: a complete, correct Sudoku solver in a few lines. Find the first blank; if there is none the grid is solved. Otherwise try each of that cell’s candidates in ascending order, place it, and recurse. If a branch comes back successful you are done; if every candidate fails you return failure and let the caller try its next digit. That unwinding is the backtrack - the search abandons a doomed branch and retreats to the last open choice.
Trying candidates in a fixed order keeps the result deterministic, so a puzzle has one exact solution string you can pin. Two edges prove the recursion’s base cases: a grid with no blanks is returned unchanged (already solved), and a grid where some blank has no candidates at all - like a cell boxed in by the digits 1 through 9 - dead-ends immediately with no solution. This solver is correct on every valid puzzle; it is only slow on the hard ones, which is exactly what the next lesson fixes.
// pick the first blank; try its candidates in order; recurse; backtrackfunc SolveBasic(g [81]int) ([81]int, bool) {cell := FirstEmpty(g)if cell == -1 { return g, true } // no blanks: solvedfor _, d := range Candidates(g, cell).Members() { // ascendingif sol, ok := SolveBasic(Assign(g, cell, d)); ok { return sol, true }}return g, false // every candidate failed: dead end}