A notebook is a Markdown file whose ```go blocks are cells. The kernel compiles it into a real Go program, orders the cells by what they use โ worked out from the code itself, as marimo does for Python, not from the order you ran them in โ and runs only what changed. A cell whose code and inputs did not change keeps its result, and its values are handed over instead of being computed again, across rebuilds and across runs.
# Spectrum ```go import "math" n := 1_000_000 ``` The sum below runs once; editing the last cell does not run it again. ```go sum := 0.0 for i := 1; i <= n; i++ { sum += 1 / float64(i*i) } ``` ```go math.Sqrt(6 * sum) ```
notebook run, html, watch, serveThe first run compiles the notebook and runs every cell. Then the last cell is changed to math.Pi - math.Sqrt(6*sum) and the notebook is run again, as a new process: the first two cells are kept, and sum comes back from the kernel's store instead of being summed again. The output below is a real run, with nothing edited.
$ notebook run spectrum.md โโ cell 0 (line 4, ran in 12ยตs) โโ cell 1 (line 12, ran in 2.663ms) โโ cell 2 (line 19, ran in 36ยตs) 3.1415916986605086 # change the last cell to math.Pi - math.Sqrt(6*sum), then: $ notebook run spectrum.md โโ cell 0 (line 4, kept) โโ cell 1 (line 12, kept) โโ cell 2 (line 19, ran in 42ยตs) 9.54929284535666e-07
Jupyter runs Python in an interpreter, with hidden mutable state, in whatever order the cells were clicked. gophernotes interprets Go. GoNB compiles, but keeps only declarations: values are computed again on every run. marimo orders cells from the code, but interprets Python. None has native speed, kept values and a reproducible order together; go-notebook is built to have all three.
Prose is kept byte for byte (format.Parse(src).String() == src, fuzzed). GitHub shows a notebook as a document, a diff is a diff of text, and no output is ever written into the file.
The kernel compiles the notebook into a worker process. Go's build cache makes a rebuild after a one-cell edit cost about a tenth of a second.
Each cell's definitions and free identifiers come from the parse alone, so the graph works on code that does not compile yet. Cycles, double definitions and foreign assignments are errors.
A cell's key hashes its source, the packages it refers to and the keys of the cells it reads. An unchanged key keeps its result; after a rebuild its variables are handed to the new worker. Moving cells runs nothing.
Results and values are kept in the work directory, so a new kernel, or the next notebook run, starts where the last one stopped.
notebook html writes a standalone page: no scripts, no external resources, raw HTML in the prose left out. A page made from someone else's notebook runs nothing of theirs.
Gated in CI on Linux, macOS and Windows.
Every repository of the organization.
x := load(path) or fmt.Println(x) at the top level of a cell is most of what a notebook is. Top-level func, type and import are declarations of the notebook; the rest runs in order.
A name defined at the top level (:=, var, const, func, type) is shared with every cell, and exactly one cell defines it. Go already forbids two package-level declarations of a name.
The dependency graph would not see the change, so it is refused. Mutation through a method or an element cannot be seen statically and is not refused, as in marimo.
Names starting with _, and err, which nearly every cell declares and none means to share.
A value with a Display() map[string][]byte method shows itself as HTML, SVG or PNG; anything else shows its %v.
//line directives point every generated line back into the .md. A panic stops the cells that read the failing one, and they say why.
The kernel, the notebook command (run, html, watch, serve) and the HTML export: format parses the Markdown, analysis builds the dependency graph from the syntax, gen writes the Go program, kernel compiles and drives it through runtime and keeps results and values, export writes the page.
A notebook in the browser, with no server. The page shows the prose as text and each cell as an editor; Ctrl+Enter runs the whole notebook again in the tab. A Go interpreter (yaegi) compiled to WebAssembly runs the cells, while the libraries they import โ go-statistics, go-numerics, go-ndarray, go-plot โ run compiled: only the notebook's own code is interpreted. Each run posts the whole notebook to a Web Worker and gets every cell's results back, one call in and one string out, so a long run neither freezes the page nor costs a call per widget. Nothing reaches the network or a file system. Deployed as /playground/ on go-statistics and go-numerics.
The documentation (Hugo + Hextra): what a notebook is, the cell rules, the command, how the kernel decides what to run, and why it is built this way. Source โ
Logos and icons for the organization, in SVG / PNG / JPG / ICO / ICNS across colour, white and black variants.
go-notebook is part of an ecosystem with go-ndarray (NumPy-style arrays) and go-fft (numpy.fft). What is not done yet โ a Jupyter front end, concurrent branches, LaTeX in prose โ is listed in the documentation.