GoogleFOSSSydney
Treating concurrency as an architectural structure rather than a parallelism tool resolves input-output mismatches, letting developers eliminate switch-statement dispatch loops with self-advancing recursive state functions.
AI-assisted summary of Lexical Scanning in Go - Rob Pike — original source. Check the original for full context.
Source published:
Key ideas
- Rob Pike introduces an idiomatic Go lexical scanner that replaces traditional switch-based state machines with recursive state functions (`type stateFn func(*lexer) stateFn`), collapsing dispatch loops into a single self-advancing line of code. (13:46)
- By running the lexer concurrently as a goroutine communicating over Go channels, the design cleanly resolves Michael Jackson's classic input-output structural mismatch without requiring complicated lookahead buffering. (0:02)
- Because Go disallows running goroutines to completion during package `init()` execution, the concurrent lexer can be transformed into a synchronous `nextItem()` API using a select loop and a channel buffer sized to two. (33:39)
- The scanner achieves zero-copy token generation by slicing directly into the original input string rather than allocating intermediate character buffers. (44:32)
Detailed notes
## Background Published by the GoogleFOSSSydney channel on YouTube in August 2011, this video documents a presentation delivered by Rob Pike at the Google Technology User Group in Sydney. According to the page description, the lecture details lexical scanning techniques in Go and references accompanying slides hosted on Google Code.
## TL;DR Rob Pike discusses the architectural redesign of the Go template package's lexer, framing it around resolving the classical structural mismatch between token producers and consumers. Rather than relying on heavyweight regular expressions, code-generation tools like Lex, or bloated switch-case dispatch loops, Pike introduces an elegant pattern built on recursive state functions that return the next state to execute. He demonstrates how Go channels and goroutines decouple the scanner from the parser with zero-copy string slicing, and shows how a small internal channel buffer and a select loop seamlessly convert the concurrent model into a synchronous nextItem API to accommodate Go's package initialization rules.
## Atmosphere & style The session feels like an intimate, conversational masterclass led by a seasoned systems pioneer who blends technical precision with dry humor and candid self-critique. Pike talks through live code patterns with obvious delight, treating software clarity and idiomatic style as genuine aesthetic pursuits. The room remains focused and participatory throughout technical digressions on concurrency and performance, culminating in a charmingly abrupt finish when Pike realizes he is double-booked and already thirty minutes late to his next engagement.
## Bulletin - [13:46](https://www.youtube.com/watch?v=HxaD_trXwRE&t=826s) Rob Pike introduces an idiomatic Go lexical scanner that replaces traditional switch-based state machines with recursive state functions (`type stateFn func(*lexer) stateFn`), collapsing dispatch loops into a single self-advancing line of code. - [00:02](https://www.youtube.com/watch?v=HxaD_trXwRE&t=2s) By running the lexer concurrently as a goroutine communicating over Go channels, the design cleanly resolves Michael Jackson's classic input-output structural mismatch without requiring complicated lookahead buffering. - [33:39](https://www.youtube.com/watch?v=HxaD_trXwRE&t=2019s) Because Go disallows running goroutines to completion during package `init()` execution, the concurrent lexer can be transformed into a synchronous `nextItem()` API using a select loop and a channel buffer sized to two. - [44:32](https://www.youtube.com/watch?v=HxaD_trXwRE&t=2672s) The scanner achieves zero-copy token generation by slicing directly into the original input string rather than allocating intermediate character buffers.
## Key takeaways - [00:02](https://www.youtube.com/watch?v=HxaD_trXwRE&t=2s) Coroutines and concurrent message passing resolve the structural mismatch problem between data producers and consumers described by Michael Jackson. - [09:03](https://www.youtube.com/watch?v=HxaD_trXwRE&t=543s) Traditional tools like Lex and regular expressions are often heavyweight, awkward for generating helpful error messages, and overly complex for scanning programming language tokens. - [12:20](https://www.youtube.com/watch?v=HxaD_trXwRE&t=740s) Switch-based state machines waste computational state by repeatedly exiting action handlers only to immediately re-dispatch on an integer variable. - [13:46](https://www.youtube.com/watch?v=HxaD_trXwRE&t=826s) Defining a state function recursively as a function that returns a state function allows the lexer execution loop to collapse into `for state != nil { state = state(l) }`. - [15:48](https://www.youtube.com/watch?v=HxaD_trXwRE&t=948s) Decoupling the lexer and parser via Go channels allows the token producer to push items independently without managing the consumer's execution state. - [19:06](https://www.youtube.com/watch?v=HxaD_trXwRE&t=1146s) Tokens are tracked using slice indices (`start` and `pos`), ensuring zero string copies are made when emitting tokens from the input buffer. - [25:14](https://www.youtube.com/watch?v=HxaD_trXwRE&t=1514s) Fine-grained cursor manipulation relies on primitive helpers `next`, `backup`, and `ignore` to step through runes and prune non-semantic whitespace. - [28:17](https://www.youtube.com/watch?v=HxaD_trXwRE&t=1697s) Chaining acceptor methods (`accept` and `acceptRun`) isolates complex numbers and identifiers more cleanly and maintainably than complex regular expressions. - [28:17](https://www.youtube.com/watch?v=HxaD_trXwRE&t=1697s) A lexer should isolate token boundaries rather than strictly validate semantic types, delegating nuanced parsing to downstream libraries. - [32:10](https://www.youtube.com/watch?v=HxaD_trXwRE&t=1930s) Formatting errors as first-class token items and returning `nil` terminates the state loop gracefully without complex exception mechanisms. - [33:39](https://www.youtube.com/watch?v=HxaD_trXwRE&t=2019s) Go runtime initialization rules prevent background goroutines from completing during `init()`, forcing template parsers that run at startup to avoid mandatory concurrency. - [35:25](https://www.youtube.com/watch?v=HxaD_trXwRE&t=2125s) A synchronous `nextItem()` API can be implemented cleanly by running a single state iteration in the `default` case of a Go `select` statement. - [43:18](https://www.youtube.com/watch?v=HxaD_trXwRE&t=2598s) A channel buffer capacity of two is sufficient to guarantee the synchronous stepping loop never deadlocks during single-state emissions. - [38:28](https://www.youtube.com/watch?v=HxaD_trXwRE&t=2308s) Concurrency serves primarily as an architectural structuring technique rather than solely a mechanism for parallel execution performance. - [48:39](https://www.youtube.com/watch?v=HxaD_trXwRE&t=2919s) While state-function machines scale naturally to lexers, parsers typically require hundreds of states, making handwritten state functions less practical without code generators.
## Detailed walkthrough ### [00:02](https://www.youtube.com/watch?v=HxaD_trXwRE&t=2s) Introduction and Jackson Structured Programming
Tim introduces the evening session at the August 2011 Google Technology User Group (GTUG) in Sydney, announcing a pair of talks on the Go programming language by Rob Pike and himself, with Pike taking the stage first to discuss lexical scanning in Go. Pike begins by polling the room on their familiarity with the work of Michael Jackson, noting that Jackson developed Jackson Structured Programming (JSP) to tackle what he rightly characterized as structural mismatch problems. In computing, many common tasks involve reconciling data arriving in one shape with data needed in another—such as converting blocks of input into discrete lines—leading to unwieldy control flows with partial blocks, partial lines, buffering, and lookahead on both sides.
Pike points out that these messy synchronization issues occur in blocking and unblocking, packet assembly, and especially in lexing and parsing. He notes an intriguing historical connection: coroutines were actually discovered or invented to solve this exact category of non-distributed data management problem before Jackson formally articulated it. With a proper coroutine model, one can write both sides—the block consumer and the line processor—completely independently, handing control back and forth cleanly. Pike states his intention to apply this exact coroutine-style approach to writing a lexical scanner.
> “When you do a coroutine model right, you get to write both pieces of the problem—the block reader and the line processor, or whatever—completely independently, and they switch control as necessary and the program can become quite a bit nicer as a result.” — Rob Pike
```mermaid flowchart LR accTitle: Structural Mismatch Resolution via Coroutines accDescr: Diagram illustrating how coroutines decouple an input block reader from a line or token processor. subgraph InputSide["Input Processing"] A["Block Reader / Raw Stream"] end subgraph Mismatch["Structural Boundary"] B["Independent Control Flow / Coroutine Hand-off"] end subgraph OutputSide["Consumer Processing"] C["Line Processor / Parser"] end A -->|"Yields data"| B B -->|"Resumes consumer"| C ```
[02:30](https://www.youtube.com/watch?v=HxaD_trXwRE&t=150s) Motivation: Rebuilding the Template Lexer
Pike frames the talk around the context of a Go template system. Early in the development of the Go standard libraries, he authored an initial text template package (conceptually akin to HTML templating, though more general). With frank self-criticism, Pike admits it was not very good: although the team thought it was cool at the time, it proved inflexible and inexpressive, prompting other contributors to invent alternative solutions. More critically, the underlying implementation was fragile; as demands arose for features like numerical constants and quoted strings, the code became clunky and resistant to extension.
Earlier in 2011, Pike decided to rewrite the package properly from scratch with a clean, decoupled lexing and parsing architecture. While a full review of the template engine is outside the scope of the talk, he outlines the grammar the lexer must digest: raw text interspersed with actions delimited by double curly braces (`{{` and `}}`). Within these delimiters, the lexer encounters standard language constructs: - Simple field evaluators like `.title`, executed against an implicit contextual data structure - Functions and constants, such as calling `printf` with a hex format directive and complex number literals (e.g., complex constants inside templates) - Control structures for iteration (`range`), conditionals (`else`, `end`), and evaluation
Outside of Perl, Pike jokes, this mirrors the basic lexical structure of almost any modern programming language: arbitrary raw text surrounding actionable tokens that need extraction.
> “And what I want to show you today is how the lexer works in that, which is about as nerdy a thing as you can talk about, but I think you'll find that it's actually quite pretty how it turned out.” — Rob Pike
[05:07](https://www.youtube.com/watch?v=HxaD_trXwRE&t=307s) Lex Items and Custom Go Types
Delivering tokens from the input stream to the parser requires an explicit item representation. Rather than converting values immediately into runtime types (such as parsing `23.2` directly into a floating-point number), standard lexer design couples an integer token type identifier with the original literal slice of text:
```mermaid flowchart TD accTitle: Anatomy of a Lex Item accDescr: Representation of a lexical item comprising an integer ItemType and a string literal value. subgraph LexItem["item struct"] T["typ: itemType (int)"] V["val: string (e.g., '23.2')"] end ```
Pike defines a distinct Go type for token categorization: `type itemType int`. He clarifies that unlike a C or C++ `typedef`, this creates a genuine, strongly typed new type rather than a mere alias, opening up capabilities like attaching custom methods. Token constants are declared in an enumerated `const` block using Go's `iota` keyword:
```go type itemType int
const ( itemError itemType = iota // starts at 0 itemDot // 1 itemEOF // 2 //... ) ```
By assigning `iota` to `itemError`, each successive identifier in the block receives a sequentially incremented `itemType` integer (`itemError` is 0, `itemDot` is 1, `itemEOF` is 2, followed by keywords like `else`, `end`, `range`, delimiters for left/right action meta-strings, and data tokens like `itemNumber`). These integer identifiers are strictly typed as `itemType`, preventing accidental mixing with standard integers.
> “That `equals iota` is a nice little piece of Go code that says start counting as though I'm going to create an enumerated constant, and then after that all of the other items in the const block are going to get sequential values.” — Rob Pike
[07:38](https://www.youtube.com/watch?v=HxaD_trXwRE&t=458s) Pretty-Printing and the Stringer Interface
Pike demonstrates how Go's formatted printing handles custom types. In Go's `fmt` library, `Printf` detects any type that implements the method `String() string` and automatically uses it as the default output representation when formatting values. Pike shows the `String` method defined on the `item` struct for debugging purposes:
```go func (i item) String() string { switch i.typ { case itemEOF: return "EOF" case itemError: return i.val } if len(i.val) > 10 { return fmt.Sprintf("%.10q...", i.val) } return fmt.Sprintf("%q", i.val) } ```
The method switches over the item's `typ` field: - `itemEOF` returns the plain text string `"EOF"`. - `itemError` returns the error message itself, taking advantage of a clever architectural trick: the lexer stores synthetic error message strings directly inside the item's `val` field, even though the error text was never part of the original source input. - For all general tokens, the value is quoted. To prevent multi-thousand-character blocks of raw text from overwhelming terminal outputs or log streams during debugging, any value longer than ten characters is cleanly truncated with an added trailing ellipsis (`...`).
> “The formatted library for Go has a package called `fmt`, which is very much like C's `printf` that you're probably at least a little familiar with, but it has one nice property that's Go-specific, which is that any type that has a method called `String` that returns `string` as a basic type is known to `printf` and can be used by `printf` so you get a default pretty print for that type value.” — Rob Pike
[09:03](https://www.youtube.com/watch?v=HxaD_trXwRE&t=543s) Why Avoid Traditional Lexing Tools and Regular Expressions
Once the custom `String()` method on `item` is in place, printing formatted tokens with `fmt.Printf` becomes trivial. Rob Pike turns to the central question of the talk: how to tokenize an arbitrary input stream and turn raw characters into distinct `item` values. There are standard, well-trodden approaches available in computing. One path is using dedicated lexical generation tools like Lex—originally created by Mike Lesk and later rewritten by Eric Schmidt while interning at Bell Labs—or modern alternatives like Ragel. Another common approach is regular expressions, and a third is writing a bespoke state machine driven by a `switch` statement. Pike makes it clear that he will not use any of these, and takes time to explain his design reasoning.
Regarding dedicated tools, Pike stresses he has nothing fundamentally against them and thinks some are quite neat, but they carry drawbacks that engineers routinely neglect:
“First of all tools, there's nothing wrong with using a tool to do this—don't don't take away from this that I think these tools are a bad idea, some of them are pretty cool—but I do think they have problems that are often neglected.”
Generating clear, helpful error messages from tool-driven parsers and lexers is notoriously frustrating. Tools also require developers to learn a separate specification language on top of the host programming language: “I think we already have a language in hand, why do we need to learn another one?” Generated code is often heavyweight and slow; Lex in particular is infamous for building massive state tables even for simple tasks. Tools can also be a poor conceptual fit for the actual problem.
Regular expressions suffer from being too dynamic and powerful for the static needs of lexical scanning. Pike recalls a code review he conducted the prior week, where an engineer tried to use regexes to lex text. Regular expression engines are large and complicated, evaluating expansive state spaces. Pike argues:
“The key point is that it's kind of too big a tool for such a small job.”
When tokenizing, one rarely needs to match against 55 arbitrary keywords simultaneously when simply checking the immediate next character is sufficient. Because programming language lexers are largely similar to one another, writing one clean implementation by hand yields a pattern that can be repurposed for almost any language.
- Lex was authored by Mike Lesk and rewritten during an internship by Eric Schmidt. - Modern lexing tools like Ragel are better than classical Lex, but still impose tool-specific overhead. - Lexers for modern programming languages share nearly identical structure, making handwritten lexers highly reusable across projects.
[12:20](https://www.youtube.com/watch?v=HxaD_trXwRE&t=740s) The Problem with Switch-Based State Machines
Pike examines the idiomatic way programmers write state machines by hand. Typically, an engineer defines integer or enum states (such as inside a comment, inside text, or inside an action block) and a collection of action routines. A central loop feeds the current state into a `switch` statement, runs the matching action to consume characters and emit tokens, and receives the next state back.
```mermaid flowchart TD accTitle: Traditional Switch-Based State Loop accDescr: Diagram showing the redundant round-trip between caller loop and action functions through an explicit switch statement.
subgraph Dispatcher["Control Loop"] Switch["switch state"] end
subgraph Actions["Action Handlers"] A1["Action 1: inside text"] A2["Action 2: inside comment"] A3["Action 3: inside action"] end
Switch -->|"state == 1"| A1 Switch -->|"state == 2"| A2 Switch -->|"state == 3"| A3
A1 -->|"returns new state"| Switch A2 -->|"returns new state"| Switch A3 -->|"returns new state"| Switch ```
Pike points out an architectural flaw in this pattern: it discards knowledge. When an action finishes running, the code already knows exactly what state it transitioned into. Yet, because of the dispatch loop structure, it exits the action, returns the state value to the caller, loops around, and immediately evaluates the `switch` statement to figure out where to go all over again. The program counter loses its place and has to reconstruct it via variable dispatch.
“When you came back from action two, you knew that you were say in state one, so why do you need to leave and come back and then figure out where you were again and get back to where you were? Why not just go right to state one?”
Pike insists that a state machine should not throw away its current state only to immediately recompute it; execution should flow directly from one state to the next without an artificial intermediate dispatch step.
[13:46](https://www.youtube.com/watch?v=HxaD_trXwRE&t=826s) State Functions and the Recursive Function Loop
To eliminate the `switch` statement, Pike formalizes the concepts of state and action. A *state* represents where the lexer currently is in the input stream and what it expects to see next. An *action* represents what the lexer does with that input. Pike unifies both concepts into a single construct: a function.
This produces a recursive type definition in Go:
“A state function is a function that returns a state function.”
In Go code, this is declared as:
```go type stateFn func(*lexer) stateFn ```
Pike acknowledges that passing `*lexer` explicitly makes it a function rather than a method, but jokes that calling them "state methods" simply does not sound right. With this recursive definition, the entire outer loop of the lexer collapses from a bloated `switch` block into three lines of code:
```go for state:= startState; state!= nil; { state = state(l) } ```
The lexer initializes `state` to a starting function pointer (such as `lexText`). As long as `state` is not `nil`, it invokes the current function passing the lexer pointer `l`. The executed function does its work and returns the pointer to the next `stateFn` to execute, which is assigned right back to `state`. When scanning is complete or an error terminates the process, the function returns `nil`, terminating the loop.
“The switch statement we had back here has now been folded into a single line of code that just runs itself constantly: get a state, call it; get a state, call it; get a state, call it.”
By doing this, the CPU's program counter always stays directly aligned with where the scanner needs to go next, creating an elegant, self-advancing state machine.
[15:48](https://www.youtube.com/watch?v=HxaD_trXwRE&t=948s) Concurrency and Go Channels for Token Emission
One crucial piece is missing from the minimal loop: nowhere in `state = state(l)` are tokens returned to the consumer. In a traditional single-threaded lexer, the lexer must pause, return a token to the parser, and wait to be called again on the next iteration.
Pike solves this using Go's native concurrency model. Instead of intertwining lexing logic with parser calls, the lexer runs concurrently as a goroutine (or co-routine) alongside the parser client. The two components function as completely independent machines: the parser builds parse trees, while the lexer processes runes. Whenever the lexer finishes scanning an item, it sends it across a Go channel.
```mermaid flowchart LR accTitle: Concurrent Lexer and Parser Architecture accDescr: The lexer runs in its own goroutine and streams scanned items over a Go channel to the parser.
subgraph LexerRoutine["Lexer (Goroutine)"] L["lexText / lexAction\nState Machine"] end
Ch["items chan item\n(Go Channel)"]
subgraph ParserRoutine["Parser (Client)"] P["Client Parser\nAST Generator"] end
L -->|"lobs item over wall"| Ch Ch -->|"consumes item"| P ```
Pike describes the channel as working conceptually like a Unix pipe with stricter type semantics, allowing the lexer to simply "lob it over the wall" to the parser without needing to manage the client's execution state.
To support this design, Pike introduces the initial structure of the `lexer` type:
```go type lexer struct { name string // used only for error reports input string // the string being scanned start int // start position of this item pos int // current position in the input width int // width of last rune read items chan item // channel of scanned items } ```
While several fields track index positions inside the input string, the key operational field is `items chan item`. A constructor function initializes the lexer struct and its channel, launching the scanning loop in the background. Pike notes with a grin that the early constructor signature returns both `(*lexer, chan item)`, admitting it is a slightly unusual API from an early version of the code that gets refined as the implementation progresses.
[17:35](https://www.youtube.com/watch?v=HxaD_trXwRE&t=1055s) Concurrency in the Lexer: `go l.run()` and State Initialization
Rob Pike explains how the lexer is actually kicked off. An initialization function, `lex`, takes a template name and the input string, allocates and initializes the `lexer` data structure, and sets the stage for scanning. In Go, one only needs to set the struct fields that matter; all other fields are automatically initialized to their zero values. The critical line of code where the real power of Go comes in is `go l.run()`. Pike emphasizes that this single statement starts the lexer’s state machine running in its own goroutine, completely concurrent with the caller. The state machine is immediately active, waiting for input and producing tokens, while `lex` returns control to the caller (typically the parser). The parser can then proceed to parse by pulling tokens off the Go channel that the runner delivers onto.
```mermaid flowchart LR accTitle: Concurrent Lexer and Parser Architecture accDescr: Diagram showing the parser spawning the lexer goroutine and consuming tokens via an items channel. subgraph Caller ["Parser Goroutine"] P["Parser"] end subgraph Worker ["Lexer Goroutine"] R["l.run() Loop"] S["State Functions"] end P -->|"spawns via go l.run()"| R R <-->|"state = state(l)"| S R -->|"l.items <- item"| P ```
Pike then presents the concrete implementation of the `run` method on `lexer`. Unlike earlier simplified schematics, Pike stresses that “this is honest code there's no cheats here.” The method establishes the initial state as plain text (`lexText`), because a template file almost invariably starts with raw text prior to encountering any template action. The engine then executes the familiar loop: `for state!= nil { state = state(l) }`. Once that loop terminates because a state function has returned `nil`, the lexer executes `close(l.items)`. Pike explains that closing the channel is a idiomatic Go mechanism to signal to the parser that no further tokens will be delivered.
[19:06](https://www.youtube.com/watch?v=HxaD_trXwRE&t=1146s) Tracking Offsets and Emitting Tokens: `emit`, `start`, and `pos`
To understand how text is turned into tokens, Pike details the internal position tracking of the `lexer` struct. The struct retains the original `input` string alongside two integer indices: `start` and `pos`. While Pike jokes that these “aren't particularly good names but that's what they are,” their roles are straightforward: `start` marks the beginning index of the token currently being scanned, whereas `pos` represents the scanner's current position as it steps through characters. At any given moment, the token being examined is simply the slice of the input string spanning from `start` up to `pos`.
The mechanism to deliver that token to the consumer is an `emit` method on `lexer`. Taking the token type as its argument, `emit` constructs an `item` containing that type and the substring slice `l.input[l.start:l.pos]`. It then sends this item directly across the channel using Go’s channel send operator (`<-`):
```go l.items <- item{t, l.input[l.start:l.pos]} l.start = l.pos ```
Pike describes this process with a physical metaphor: “so that lobs the the item over the wall to the parser and then to move ahead in the input all we do is move start up to pause so you go scan scan scan scan aha here's an item throw it over the fence then do the next one throw it that's the model.” Advancing `start` to meet `pos` effectively consumes the emitted token and prepares the lexer to scan the next lexeme from a clean slate.
[20:24](https://www.youtube.com/watch?v=HxaD_trXwRE&t=1224s) Scanning Text and Left Delimiters: `lexText` and `lexLeftMeta`
With offsets and emitting established, Pike dives into the first real state function: `lexText`. Its sole responsibility is to scan through raw template text until it encounters the opening delimiter of an action, represented by the constant `leftMeta` (the double curly brace `{{`). The function runs a loop over the input string. If `pos` is sitting at the start of a `leftMeta` marker, it checks whether there is any text accumulated between `start` and `pos`. If `pos > start`, it immediately calls `l.emit(itemText)` to flush out the preceding raw text. Then, it transitions to the next state by returning the function `lexLeftMeta`:
```go func lexText(l *lexer) stateFn { for { if strings.HasPrefix(l.input[l.pos:], leftMeta) { if l.pos > l.start { l.emit(itemText) } return lexLeftMeta } if l.next() == eof { break } } // Correctly handle EOF and emit remaining text... return nil } ```
Pike then demonstrates `lexLeftMeta`, which he describes as a trivial state function that highlights how simple transitions can be. Because `lexText` only transitions to `lexLeftMeta` when the input is guaranteed to be pointing directly at the left delimiter, `lexLeftMeta` does not need to re-verify the match. It advances `pos` by the length of `leftMeta` (`l.pos += len(leftMeta)`), calls `l.emit(itemLeftMeta)` to push the delimiter item to the parser, and transitions straight into the action block by returning `lexInsideAction`.
- The constant `leftMeta` represents the opening marker of an action (`{{`). - `lexText` continuously steps forward until it spots `leftMeta` or hits EOF. - `lexLeftMeta` advances `pos` by `len(leftMeta)`, emits `itemLeftMeta`, and hands execution over to `lexInsideAction`.
[23:09](https://www.youtube.com/watch?v=HxaD_trXwRE&t=1389s) Lexing Inside an Action: Dispatching Tokens
Once inside an action block, the lexer faces a much more dynamic syntactic environment. As Rob Pike notes, “when we're inside an action we don't know what we're on is is a number coming up is a identifier coming up a string constant we don't know so we got to ask what are we sitting on right now.” The function `lexInsideAction` operates like a dispatcher, asking a sequence of questions about the input at the current position:
1. **Right delimiter check:** If the input begins with `rightMeta` (`}}`), the action is over; it immediately returns `lexRightMeta`. 2. **Next character inspection:** Otherwise, it pulls the next rune using `l.next()` and evaluates it: - **EOF or newline (`\n`):** Actions cannot span across newlines in this template grammar, so encountering either signals an unclosed action delimiter; it returns an error state via `l.errorf(...)`. - **Whitespace:** Spaces between tokens are skipped by calling `l.ignore()`. - **Pipe (`|`):** Emits a pipe token, which is used for pipeline operations in templates. - **Quotes (`"` or `` ` ``):** Transitions to `lexQuote` or `lexRawQuote` to handle interpreted and raw Go string literals. - **Plus, minus, or digits (`+`, `-`, `0`-`9`):** Indicates the start of a numeric literal. The lexer calls `l.backup()` to step back before the consumed character, then returns `lexNumber`. - **Alphanumerics:** Since digits are already handled, an alphanumeric character signifies an identifier. The lexer backs up and returns `lexIdentifier`.
Pike contrasts this dispatching logic with traditional regular-expression-based tokenizers, arguing: “And compared to what a regular expression does to work this out it's actually in some senses simpler of course there's more code but it's really nice how you just sort of pick a one character what do I do now I move on it's kind of nice.”
[25:14](https://www.youtube.com/watch?v=HxaD_trXwRE&t=1514s) Stepping and Pruning: `next`, `ignore`, and `backup`
To support fine-grained movement over the input, the lexer relies on a handful of compact helper methods. The fundamental primitive is `next`, which decodes and returns the next Unicode code point—termed a `rune` in Go—from the input. If `pos` has reached the end of the input string, `next` returns a sentinel `eof` value. Otherwise, it decodes the next UTF-8 character, advances `pos` by the byte width of that rune, and returns the rune itself.
Pike then covers two complementary helper functions: - `ignore`: When the lexer encounters characters that carry no semantic value (such as whitespace separating identifiers), `ignore` simply sets `l.start = l.pos`. This silently discards everything scanned since the previous token without emitting anything to the channel. - `backup`: When the lexer peeks ahead by calling `next` and discovers it has stepped one character into a distinct lexeme (such as stepping onto a digit or identifier start), it cannot keep that character in the current token slice. `backup` reverses `pos` by the width of the last rune read, leaving the input cursor poised right at the boundary for the subsequent state function to consume cleanly.
[26:29](https://www.youtube.com/watch?v=HxaD_trXwRE&t=1589s) Navigating the Input: Backup, Peek, and Acceptor Helpers
Rob Pike reviews the cursor mechanics on the `lexer` struct. Because the lexer's `next` method advances `pos` while tracking the character width in `width`, rewinding the input when you have stepped one character too far—such as inadvertently stumbling into a number or past an expected boundary—is straightforward: simply subtract `width` from `pos` in a helper called `backup`. Building on this, Pike introduces `peek`, a standard utility across scanning software when you must inspect the upcoming character without consuming it. Implementing `peek` in Go is completely trivial: call `next`, note the rune, and immediately call `backup`.
```mermaid flowchart LR accTitle: Peek implementation flow accDescr: Demonstrates how peek is implemented by calling next and immediately backing up. subgraph PeekHelper ["peek() Helper"] A["Call next()"] -->|"advances pos, gets rune"| B["Read Rune"] B -->|"rewinds pos by width"| C["Call backup()"] C --> D["Return Rune"] end ```
With those basic movements in place, Pike introduces the core tools for consuming structured patterns: the acceptor methods. The `accept` method accepts a string of allowable characters (such as digits or letters) and uses `strings.IndexRune` to check whether the next rune belongs to that valid set. If it does, `accept` consumes the rune and returns `true`; if not, it steps back with `backup` and returns `false`. Its companion, `acceptRun`, loops continuously over `accept` to swallow an unbroken run of matching runes:
- `accept(valid string) bool`: Consumes the next rune if present in `valid`; otherwise rewinds and returns `false`. - `acceptRun(valid string)`: Absorbs characters in a loop as long as incoming runes match the `valid` set. Pike notes that while returning whether anything matched can be useful in general, this template scanner does not require a return value from `acceptRun`.
---
[28:17](https://www.youtube.com/watch?v=HxaD_trXwRE&t=1697s) Scanning Complex Numbers and Delegating to the Parser
Pike turns to scanning numeric literals, pointing out that numbers are deceptively tricky to lex once you consider all their representations: decimal, octal, hexadecimal, floating point (with or without decimals), signs, exponents, and imaginary constants. Writing a single regular expression to match every number format produces a terrifyingly long, unwieldy pattern: “For fun try to write the regular expression that matches a number; you will be surprised—it's about this long.”
Instead of wrestling with regular expressions, the lexer chains acceptor methods together. The lexer does not even need to accumulate a buffer, because the moving `pos` cursor within the source string tracks slice boundaries automatically:
```mermaid flowchart TD accTitle: Lexing a number accDescr: Step-by-step pipeline for scanning integers, hex, floats, exponents, and imaginary constants. subgraph SignAndBase ["1. Sign & Base Detection"] S1["accept('+-')"] --> S2["Default digits: '0-9'"] S2 --> S3{"accept('0') and accept('xX')?"} S3 -->|"Yes"| S4["digits = '0-9a-fA-F' (Hex)"] S3 -->|"No"| S5["digits = '0-9' (Decimal)"] end
subgraph MantissaAndExp ["2. Digits, Fraction & Exponent"] S4 --> M1["acceptRun(digits)"] S5 --> M1 M1 --> M2{"accept('.')?"} M2 -->|"Yes"| M3["acceptRun(digits)"] M2 -->|"No"| M4{"accept('eE')?"} M3 --> M4 M4 -->|"Yes"| M5["accept('+-'); acceptRun('0-9')"] M4 -->|"No"| M6{"accept('i')?"} M5 --> M6 end
subgraph ValidationAndEmit ["3. Verification & Token Emission"] M6 -->|"Check boundary"| V1{"Next rune alphanumeric?"} V1 -->|"Yes"| V2["l.errorf('bad number syntax')"] V1 -->|"No"| V3["emit(itemNumber)"] V3 --> V4["Return lexInsideAction"] end ```
The scan proceeds through signs, checks for `0x` to widen the character set to hexadecimal digits, slurps the mantissa with `acceptRun`, accepts an optional decimal point and fractional digits, scans optional `e` or `E` exponents with potential signs, and finally absorbs an optional trailing `i` for Go's imaginary number constants. At the boundary, Pike checks that the next rune is not alphanumeric; it must be punctuation or whitespace, or the input syntax is malformed.
Pike admits that this loose scan will accept constructs that are technically invalid Go numbers, such as `0x3E4` (hexadecimal floating point). He explains that the scanner's responsibility is merely to isolate the maximal substring that represents a token, leaving semantic numeric validation to downstream standard libraries: “It's fine for a lexer to say, 'I think I know what this is,' and still let the parser validate it.” Once the token boundaries are found, the lexer calls `emit(itemNumber)` and returns `lexInsideAction` as the next state function to resume parsing inside the action braces.
---
[32:10](https://www.youtube.com/watch?v=HxaD_trXwRE&t=1930s) Elegant Error Handling via State Functions
Pike walks through the implementation of `errorf`, emphasizing that error handling integrates seamlessly into the state machine. In Go, `errorf` is written as a variadic method using an empty interface slice (`...interface{}`), behaving like `fmt.Printf`:
> “I believe in really good error messages; this is a really easy way to make them.”
The method formats the error message, emits an `item` of type `itemError` containing that formatted text onto the `items` channel, and returns `nil`. Because the lexer's core execution loop terminates whenever the current state function evaluates to `nil`, emitting an error automatically halts scanning:
- Formatted text delivered to the parser includes the invalid source fragment. - Returning `nil` shuts down the state machine cleanly without requiring dedicated exception handling or abort flags. - For template parsing, halting immediately on bad input is preferable to attempting error recovery.
---
[33:39](https://www.youtube.com/watch?v=HxaD_trXwRE&t=2019s) The Initialization Concurrency Trap
Pike summarizes the design up to this point: concurrent goroutines, clean state functions, string slicing, and communication over Go channels. He mentions that after discarding several earlier, unsatisfactory implementations, treating lexing as a concurrent producer-consumer pipeline resolved the structural mismatch between parser and lexer in an evening of work.
However, this design hits a fatal barrier within Go's runtime rules. In web servers, developers typically compile HTML templates into package-level global variables during program startup so they are ready before serving requests:
> “We decided you just simply cannot run a goroutine to completion during initialization: you can create one, but it doesn't run until initialization is done.”
Because Go enforces strict, deterministic package initialization semantics, allowing goroutines to run concurrently during `init()` introduced ambiguous execution ordering. Consequently, goroutines created during variable initialization remain paused until all initialization concludes. Because the concurrent lexer depends on a background goroutine actively pushing items to a channel that the parser reads, attempting to parse a template at initialization time deadlocks. Pike reassures the audience that this problem has a clean solution, setting up the need to adapt the lexer design to operate without mandatory concurrency.
[35:25](https://www.youtube.com/watch?v=HxaD_trXwRE&t=2125s) Converting the Lexer to a Synchronous `nextItem` API
Rob Pike explains that while stripping concurrency out might sound counterproductive after building such a clean model, it turns out to be remarkably simple to adapt the concurrent lexer into a traditional, synchronous token-fetching API. A conventional lexer interface does not expose channels to the caller; instead, it provides a function like `nextItem()` that simply returns the next token on demand. To achieve this, the lexer constructor `lex` is modified so that it returns a handle to the lexer itself (`*lexer`) rather than returning an output channel.
Under the hood, the lexer still allocates a channel for tokens, but it is kept private and given a small buffer. Rob describes this buffering trick as effectively turning the channel into a ring buffer: the lexer can emit a couple of tokens into the channel and then pick them up later without synchronous binding. To power `nextItem()`, the execution loop is restructured around an infinite `for` loop containing a Go `select` statement. The `select` checks whether an item is currently sitting in the buffered channel ready to be received:
```mermaid flowchart TD accTitle: nextItem polling loop accDescr: Flowchart showing the select loop inside nextItem evaluating channel receive or executing a state iteration.
subgraph Loop ["Loop: nextItem()"] Check{"select on l.items"} Emit["Receive item from l.items"] Default["default: Run state machine one step"] end
Check -->|"item available"| Emit Emit --> Ret["return item to caller"] Check -->|"no item ready"| Default Default --> Check ```
If a token is available on the channel, the `select` receives it and immediately returns it to the caller. If no token is ready, execution falls through to the `default` case, which advances the state machine by exactly one step (`state = state(l)`). If that single step does not produce a token, the loop cycles again, hitting `default` and executing another step until a state function finally emits a token onto the channel. Rob calls this slide the most “magical” part of the talk because it completely eliminates explicit goroutines from the public interface while preserving the internal elegance of the state functions:
> “All I want to show you is that that's all that was required to remove the concurrency from the picture.”
- The public API becomes two straightforward operations: instantiate a lexer, then call `nextItem()` repeatedly until receiving an end-of-file (`itemEOF`) token. - The channel is hidden entirely inside the lexer struct, turning a concurrent channel into a private ring buffer. - The state machine avoids messy state restoration logic because each iteration simply runs the stored `stateFn` pointer.
---
[38:28](https://www.youtube.com/watch?v=HxaD_trXwRE&t=2308s) Concurrency as a Design Technique and Talk Conclusion
Rob stresses that concurrency is fundamentally an architectural design tool rather than just a technique for parallel performance. When people attempt to write a synchronous `nextItem()` API directly from scratch, they typically end up with tangled switch statements that constantly try to unpack where execution left off, restore internal state, and restart parsing. By structuring the problem concurrently from the beginning, the code retains clear, self-contained state functions that are easy to reason about. Rob points out that this structural separation directly solves the classic input-output structural mismatch identified by computer scientist Michael Jackson:
> “Concurrency is this design concept it's not about the way a lot of people think about it sort of parallelism it's actually a design idea and we use it here to deal with this structural mismatch from from Michael Jackson who by the way was not the musician in case you think it was the same guy.”
Rob elaborates that “concurrency enables parallelism, doesn't require it,” highlighting how thinking about problems as independent, communicating entities brings grace to software architecture. He notes that the lexer design blends object-oriented method syntax with Go’s concurrent primitives. Furthermore, the design proved remarkably flexible in practice. The code presented in the slides was from the very first day of development; as the template language evolved, new lexical tokens and features were incorporated simply by adding one or two lines or introducing a brief two-line state function, avoiding the rigid fragility that plagued the older template package. Rob wraps up his presentation with an enthusiastic summary:
> “So in conclusion lexers are fun concurrency is fun and Go is a lot of fun so I encourage you to play around with it.”
He directs the audience to `golang.org` for documentation and notes that Andrew Gerrand will later demonstrate parts of the new template engine, which is scheduled to replace the deprecated template library in the upcoming Go release.
---
[41:12](https://www.youtube.com/watch?v=HxaD_trXwRE&t=2472s) Q&A: Build Artifacts, Native Binaries, and Performance
The presentation transitions to audience questions, with Rob requesting that questioners speak into the handheld microphone for the video recording. An audience member asks about Go's compilation output—whether it compiles into a standalone binary or acts like a.NET library that can be linked and referenced externally. Rob clarifies that Go compiles directly to statically linked native binaries. The lexer itself is just a package within Go’s standard library that gets compiled and linked into the final binary. While an experimental.NET implementation of Go is in progress, Go is designed and treated as a compiled, statically typed language with native performance rather than a runtime-interpreted or bytecode-driven system.
A second audience member asks if Rob has measured performance statistics for this lexer. Rob candidly confesses that he anticipated the question right before the talk and has not yet run formal benchmarks:
> “The answer is it's probably okay but not great because I'm using the language a little too having a little too much fun with the language and doing the channel communication and and that kind of stuff actually puts more overhead into the inner loop than that needs to be there.”
Rob notes that channel communication incurs inner-loop overhead that could be tuned if extreme speed were needed. However, the state-function dispatch itself—jumping through function pointers—operates close to theoretical peak performance for handwritten lexers, performing comparably to an indexed table switch.
---
[43:18](https://www.youtube.com/watch?v=HxaD_trXwRE&t=2598s) Q&A: Buffer Sizing and Pipeline Staging
A participant asks whether the lexer could retain its concurrent goroutine across multiple stages and run outside of initialization. Rob explains that in this specific use case, parsing must run to completion during initialization because the program needs to construct the full parse tree for the template before rendering can begin; delaying it would only block subsequent execution.
Rob then explains the deliberate choice of channel buffer capacity in the synchronous implementation, calling two “a very magic number.” Because no single execution of a state function ever emits more than two items into the channel before yielding or looping, a buffer size of two guarantees that the channel never deadlocks or blocks unexpectedly during a single state step. While the buffer could theoretically be made arbitrarily large, sizing it to two matches the precise operational requirements of the state machine. Staging the lexer directly into a parser works naturally under this scheme, and `nextItem()` simply steps the machine forward whenever the caller requires the next token.
[44:32](https://www.youtube.com/watch?v=HxaD_trXwRE&t=2672s) Concurrency, Buffering, and Zero-Copy Efficiency
Rob Pike concludes his architectural overview by emphasizing that despite using goroutines and channels, the execution remains strictly synchronous. Because only one goroutine runs while the other waits on channel communication, there are no thread-scheduling overheads or race conditions. Even though it is concurrent, it is not running in parallel, resulting in a completely direct, linear execution path.
```mermaid flowchart LR accTitle: Single-threaded Goroutine Handoff accDescr: Lexer and parser run concurrently via a channel without thread contention. subgraph Execution["Linear Control Flow"] L["Lexer Goroutine"] C["Item Channel"] P["Parser Client"] L -->|"send item"| C C -->|"receive item"| P end ```
Pike then takes an audience question forwarded from the IRC chatroom asking whether channel-based concurrency could be replaced by a simple list or vector where all tokens are pushed upfront. Pike confirms that Nigel Tao had submitted a change list (CL) doing precisely that. Although Pike acknowledged Nigel's approach was probably more performant and would eliminate potential channel overhead, Pike pushed back and kept the channel design because the list implementation simply was not as aesthetically pleasing. If profiling ever proves it to be performance-critical, he notes that converting it to an explicit ring buffer would take merely ten lines of code. Crucially, Pike notes that the underlying problem has nothing inherently concurrent about it; rather, concurrency serves as an elegant mechanism to structure the program.
An audience member follows up with an efficiency question, asking how many times the input text is copied during the scanning process. Pike explains that the lexer copies the input string zero times. All items emitted to the parser are string slices pointing directly into the original input buffer. Even scanning a multi-megabyte string with zero template actions allocates no copies, making token generation exceptionally lightweight.
- “The point about this is there's nothing inherently concurrent about the problem only the solution; the solution gets pretty because you can use concurrency to solve the problem.” — Rob Pike
[46:39](https://www.youtube.com/watch?v=HxaD_trXwRE&t=2799s) Adapting to `io.Reader` and Idiomatic Go Design
An attendee asks about the effort needed to make the lexer accept an `io.Reader` interface instead of a pre-read string. Pike explains that doing so is straightforward: one would update the lexer struct to hold the reader and adjust the `next` method to pull bytes from it. While backtracking (`backup`) becomes slightly trickier, Pike has implemented that pattern in other contexts. One could even wrap existing strings in readers to make the lexer universally accept any input source. However, for the specific needs of Go's `template` package, requiring an in-memory string was a deliberate choice, whereas a general-purpose regular expression engine or network stream processor would benefit from taking an `io.Reader`.
When asked whether this pattern inspired him to rewrite other components like XML or HTTP parsers, Pike explains that he hasn't pursued those, though he mentions a recent blog post where an author parsed HTML with regular expressions: “I gave him a Smackdown,” Pike quips, adding that the author graciously accepted the feedback and revised his implementation. For Pike, this lexer serves as a prime showcase for idiomatic Go. It brings together concurrency, light object-oriented design, methods, first-class functions, and efficient string slicing into an elegant solution for a classic compiler problem.
- “The reason I chose this program to present it is I think it's in a lot of ways it's idiomatic Go: there's a little bit of concurrency, a little bit of object-oriented stuff, there's some nice use of strings, and you see methods and functions—there's sort of a lot of things going on.” — Rob Pike - “It's not clear that it's the solution to world hunger, but it's a solution to Lexing my templates.” — Rob Pike
[48:39](https://www.youtube.com/watch?v=HxaD_trXwRE&t=2919s) Parser Generators, Progress Tracking, and Farewell
An audience member asks whether this state-function technique could be adapted into a parser generator. Pike confirms that it is theoretically possible because both lexers and parsers are formal state machines. However, parsers entail significantly larger state spaces. While this template lexer requires only 10 to 15 state functions, a typical parser requires several hundred, which would introduce considerable boilerplate without automated code-generation tooling. He notes that Ragel, a state machine compiler, generates internal code that is essentially isomorphic to this design, though far less readable by hand.
Another attendee asks via microphone about receiving progress updates during long parsing runs or writing out intermediate results. Pike points out that Go’s `select` statement enables multi-way concurrent communication. By adding roughly four lines of code inside the event loop, a caller could query the lexer's progress or configure the lexer to emit periodic heartbeat notifications over an auxiliary channel based on byte counters. Furthermore, because Go is a general-purpose language, writing diagnostic files mid-stream is trivially supported (“Files are cool... underrated”).
Pike brings the session to an abrupt close, apologizing to the audience for having to cut questions short. Due to an unfortunate double-booking conflict, he was scheduled to be at another engagement thirty minutes earlier—precisely when the lecture began.
- “Parsing has many, many states, right? There's only about maybe 10 or 15 state functions in this lexer; there'd probably be several hundred in a typical parser, so there'd be a lot of boilerplate—you'd probably want a machine to help you write it.” — Rob Pike - “I apologize: I have a horrible scheduling conflict and I'm supposed to be somewhere else half an hour ago, which is when my talk started, so I have to run and I'm really sorry about that.” — Rob Pike
## Notable quotes > [28:17](https://www.youtube.com/watch?v=HxaD_trXwRE&t=1697s) It's fine for a lexer to say, 'I think I know what this is,' and still let the parser validate it. — Rob Pike
> [44:32](https://www.youtube.com/watch?v=HxaD_trXwRE&t=2672s) The point about this is there's nothing inherently concurrent about the problem only the solution; the solution gets pretty because you can use concurrency to solve the problem. — Rob Pike
> [00:02](https://www.youtube.com/watch?v=HxaD_trXwRE&t=2s) When you do a coroutine model right, you get to write both pieces of the problem—the block reader and the line processor, or whatever—completely independently, and they switch control as necessary and the program can become quite a bit nicer as a result. — Rob Pike
## Mentioned Rob Pike, Michael Jackson, Jackson Structured Programming, Go, Lex, Mike Lesk, Eric Schmidt, Bell Labs, Ragel, .NET, Nigel Tao, Andrew Gerrand, Google Technology User Group
## Video description Lexical Scanning in Go, a talk by Rob Pike at Google Technology User Group given on Tuesday, 30 August 2011.
The slides: http://rspace.googlecode.com/hg/slide/lex.html
[Source](https://www.youtube.com/watch?v=HxaD_trXwRE)