A look at godiff's Go internals: bufio.Scanner buffer limits, single-pass patch parsing, and bounded word-level diffs.
· 6 min read

How godiff Parses Large Git Patches Fast in Go


One line in godiff’s patch parser explains much of its approach to large inputs:

sc.Buffer(make([]byte, 64*1024), 64*1024*1024)

That’s from internal/diff/diff.go. It raises bufio.Scanner’s default token limit so the parser can accept lines up to roughly 64MB. godiff is a native diff viewer written in Go, and the interesting parts are the places where it decides what not to compute: capped buffers, capped word diffs, capped prompt sizes.

Each of those caps is a technique you can lift into your own code.

bufio.Scanner silently stops at 64KB by default

bufio.Scanner is a convenient way to read line-by-line in Go, and it has a sharp edge. Per the bufio docs, the maximum token size defaults to MaxScanTokenSize, which is 64KB. Hit a longer line and Scan returns false; scanner.Err() then reports bufio.ErrTooLong. Skip that error check and the partial parse can look successful.

Git patches can contain lines that large: a minified JavaScript bundle, a base64 fixture, or machine-generated JSON can all cross 64KB.

package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

func main() {
	// One line, 1MB long.
	r := strings.NewReader(strings.Repeat("x", 1<<20) + "\n")

	sc := bufio.NewScanner(r)
	for sc.Scan() {
		fmt.Println("line length:", len(sc.Text()))
	}
	if err := sc.Err(); err != nil {
		fmt.Fprintln(os.Stderr, "scan failed:", err) // bufio.Scanner: token too long
	}
}

The fix is Buffer(buf, max). The first argument supplies the initial buffer; the second sets the ceiling it may grow to. godiff starts at 64KB and allows growth up to 64MB. That covers unusually long lines without allowing one token to grow without bound.

There is an important caveat in the reviewed source: Parse does not check sc.Err() after the loop. Raising the limit handles lines below the new ceiling, but a line beyond it still stops the scan and leaves the caller with a partial result. The safe pattern is to pair an explicit limit with an error check, or change the parser API so it can return an error.

That two-argument split is the part people get wrong. Passing make([]byte, 64*1024*1024) as the initial buffer allocates 64MB on every parse, including the ones where no line exceeds 80 characters.

A single pass with a finish closure

internal/diff/diff.go parses git’s unified diff output in one sweep. Parse keeps a handful of locals: the current *File, the current *Hunk, remaining line counts for each side (remOld, remNew), and a bytes.Buffer holding the current file’s raw patch text.

What makes it readable is a finish closure declared before the loop:

finish := func() {
	if cur == nil {
		return
	}
	// append the pending hunk, count lines, fingerprint, append the file
}

Every time the scanner sees a diff --git header, it calls finish() and starts a new file. After the loop, it calls finish() once more to flush the last one. No slice of half-built files waiting for a second pass to compute totals. No regex.

The raw bytes.Buffer is reused across files via raw.Reset(). Reset keeps the underlying storage, so once one large file has grown it, later files can reuse that capacity without involving a pool.

There’s a cost hiding in here. Parse calls sc.Text(), which returns a newly allocated string for each scanned line. sc.Bytes() avoids that allocation but returns memory the scanner may overwrite on the next Scan. godiff keeps line text after the loop, so it needs owned data somewhere; a parser that only inspects each line before advancing can use Bytes() and avoid those per-line string allocations.

Fingerprints with truncated SHA-1

Each parsed file gets a Fingerprint, computed in diff.go:

h := sha1.New()
for _, p := range parts {
	h.Write([]byte(p))
	h.Write([]byte{0})
}
return hex.EncodeToString(h.Sum(nil))[:16]

Two details worth noticing. The 0 byte written between parts prevents ambiguity: without it, ("ab", "c") and ("a", "bc") produce the same digest. Classic composite-hashing bug, fixed by one extra Write.

Then there’s [:16]. The full hex digest is 40 characters and godiff keeps 16, representing 64 bits. This is an identity key answering “has this file’s change content changed since I marked it viewed?”, not a security boundary. A 64-bit value is not collision-proof—birthday collisions become plausible around billions of distinct values—but it is a pragmatic compact key for the small number of file changes in a local review. It should not be reused where collisions are adversarial or costly.

Bounding the quadratic part of word-level diffs

internal/diff/words.go is where the main algorithmic risk lives. godiff uses a longest common subsequence table for word-level highlighting, which costs O(n×m) in both time and memory for the token counts n and m. Without bounds, that work grows quickly on large or heavily changed lines.

godiff bounds that work in several layers.

The first is a hard length cap:

const maxWordDiffLen = 1000

func WordDiff(a, b string) (ra, rb []Range) {
	if a == b || len(a) > maxWordDiffLen || len(b) > maxWordDiffLen {
		return nil, nil
	}
	// ...
}

Identical lines return immediately. Lines over 1000 bytes get no intra-line highlighting, putting an initial bound on the amount of text that reaches tokenization.

Next comes common prefix and suffix trimming. WordDiff tokenizes both lines, advances pre while the leading tokens match and suf while the trailing tokens match, then slices down to ta[pre:len(ta)-suf] and tb[pre:len(tb)-suf]. For a small edit in the middle of a line, this can shrink the LCS inputs to only the changed region.

Here’s the technique in isolation, generic over any comparable slice:

// trimCommon returns the slices with their shared prefix and suffix removed,
// plus the length of the removed prefix so callers can map indices back.
func trimCommon[T comparable](a, b []T) (ta, tb []T, offset int) {
	pre := 0
	for pre < len(a) && pre < len(b) && a[pre] == b[pre] {
		pre++
	}
	suf := 0
	for suf < len(a)-pre && suf < len(b)-pre && a[len(a)-1-suf] == b[len(b)-1-suf] {
		suf++
	}
	return a[pre : len(a)-suf], b[pre : len(b)-suf], pre
}

Look at the suf < len(a)-pre conditions. They stop the suffix scan from running back into the prefix you already consumed. Compare naively from both ends and you’ll ship that bug, usually discovering it on a line like aaaa versus aa.

After trimming, godiff allocates the LCS table only when len(ma)*len(mb) <= 250_000. That is the direct memory-and-time guard on the quadratic step. Finally, WordDiff returns nothing when less than 30% of the non-space text is shared. Highlighting most of a rewritten line would add noise, so in that case the cheapest useful answer is no answer.

Pairing changed lines in one sweep

Pairs, in the same file, matches deleted lines to the added lines that replace them, which is what a split view needs to align its two columns. It walks the hunk once, collects a run of Del lines, collects the run of Add lines immediately after, and zips them:

for k := range min(dels, adds) {
	pairs = append(pairs, Pair{Del: start + k, Add: addStart + k})
}

Three lines, two pieces of recent Go. min is the builtin added in Go 1.21, so no math.Min and no round trip through float64. for k := range someInt is range-over-int from Go 1.22. The enclosing function is one for loop whose index the inner loops advance, which keeps it O(n) over the hunk with a single append-grown slice.

The same bound, applied to prompts

The size-capping habit shows up outside the parser too. In internal/agent/run.go, godiff checks whether the combined diffs fit within inlineLimit:

inline := len(diffs(req.Files)) <= inlineLimit

If they don’t fit, godiff prefers a temp file inside the repository’s .git directory. When .git is not a directory, as in a linked worktree, it falls back to the operating system’s temp directory. defer os.Remove(patchPath) handles cleanup. The run is wrapped in context.WithTimeout(ctx, timeout) with a 20-minute ceiling. exec.CommandContext ties cancellation to the child command, and on Unix godiff replaces the default cancellation function so the whole process group receives SIGKILL.

It is the same pattern as maxWordDiffLen: price the expensive path up front, then take a bounded alternative when the input will not fit. Profiling still matters, but explicit limits protect the edge cases that average-input benchmarks can miss.

If you’re parsing untrusted or machine-generated text in Go, three ideas transfer well: give Scanner.Buffer an explicit maximum and check Err(), put a direct size guard in front of quadratic work, and trim common regions before running the expensive algorithm.