# d5/tengo: a Go-embeddable script language that compiles to bytecode

> Tengo is a small dynamic language written in native Go, embedded as a library or run as a standalone REPL. It fits rules engines and data pipelines, and it is the wrong tool when you need a large standard library or a stable v2 import path.

**d5/tengo** — A fast script language for Go

- Repository: https://github.com/d5/tengo
- Website: https://tengolang.com
- Stars: 3,842 · Forks: 339
- Language: Go
- License: MIT
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/d5-tengo

## The gap Tengo fills between Go and a full scripting runtime

Go has no built-in way to evaluate a string of user-supplied logic at runtime. The usual answers are to write a small expression evaluator yourself, or to embed a general-purpose interpreter such as Lua or JavaScript. Tengo takes the second path but keeps the implementation in native Go, with no external dependencies and no cgo. The README states the language is "fast and secure because it's compiled/executed as bytecode on stack-based VM that's written in native Go." That description matters more than the benchmark table: a pure-Go VM means you can cross-compile your binary as usual and ship a single artifact that carries the interpreter inside it.

The intended audience is Go developers who need to hand a slice of control to someone else. The README lists rules engine, state machine, data pipeline and transpiler as use cases. Those share a shape: a host program owns the data and the side effects, and the script decides which branch to take. If your problem looks like that, Tengo is aimed at you. If your problem is "let users write whole applications," it is not.

## Compile, then run on a stack VM: the execution model

The repository layout shows the pipeline directly. There is a parser/ directory, a compiler.go, a symbol_table.go, a bytecode.go, an instructions.go and a vm.go, plus a token/ package. Source text becomes tokens, tokens become an AST, the compiler walks the AST and emits bytecode against a symbol table, and the VM executes that bytecode. Values are represented as Tengo objects (objects.go) with runtime types and operators documented separately.

The public entry point is a Script value. According to the README example, you construct one with tengo.NewScript([]byte(...)), inject host variables with script.Add, then call script.RunContext. The result is a compiled object whose Get method retrieves named variables back out. That direction of travel is the important design decision: inputs go in by name, outputs come out by name, and the script cannot reach into your Go memory unless you hand it a reference. For a rules engine this is exactly the boundary you want.

For one-off expressions the package offers tengo.Eval, which the README demonstrates with a ternary expression and a map of inputs. That is a shorter path when you do not need reusable compiled state. The README does not describe caching or reuse semantics for Eval, so if you call it in a hot loop you are relying on behaviour the documentation does not spell out.

## Installing Tengo and running your first embedded script

The README gives a single install command for the library. It fetches the v2 module path, which is what the go.mod in the repository declares as module github.com/d5/tengo/v2.

```bash
go get github.com/d5/tengo/v2
```

The module targets go 1.13 in its go.mod, so any reasonably current toolchain will build it. After that, the README's quick start example is the shortest real use: define a script that sums and multiplies injected values, add four numbers, run it, and read two results back.

```golang
script := tengo.NewScript([]byte(
`each := func(seq, fn) {
    for x in seq { fn(x) }
}

sum := 0
mul := 1
each([a, b, c, d], func(x) {
    sum += x
    mul *= x
})`))

_ = script.Add("a", 1)
_ = script.Add("b", 9)
_ = script.Add("c", 8)
_ = script.Add("d", 4)
compiled, err := script.RunContext(context.Background())
```

With those inputs the README says the printed results are "22 288", retrieved through compiled.Get("sum") and compiled.Get("mul"). Note that the example ignores the errors returned by Add; in production code you would check them, since a failed Add means the script will not see that name.

There is also a standalone CLI. The Makefile runs it as go run ./cmd/tengo -resolve ./testdata/cli/test.tengo, which tells you the binary lives under cmd/tengo and accepts a -resolve flag. The README links a separate CLI document rather than inlining the full flag list, so treat that document as the source for anything beyond -resolve. Syntax highlighting exists for VSCode, Atom, Vim and Emacs, which is a small but real signal that people edit Tengo files rather than generating them blind.

## Where Tengo stops being the right choice

The benchmark table in the README is the project's own, run from a separate repository, and it compares fib(35) across interpreters. Tengo is listed at 2,315ms against 47ms for native Go. That is roughly a fifty-fold gap, and it is the honest number to plan around. If your script is on a per-request path at high volume, the interpreter cost is real and you should measure it against your own workload rather than against Fibonacci.

The tail-call row is the more interesting one. fibt(35) shows 3ms for Tengo, the same as go-lua and GopherLua, which suggests the tail-call version is optimised into a loop rather than exercising the call stack. Do not read that as evidence that deep non-tail recursion is cheap.

The bigger limitation is the surface area. Tengo ships a standard library under stdlib/, but it is not Python's or JavaScript's, and the README does not claim otherwise. There is no package manager for Tengo code, no ecosystem of third-party modules to import beyond what the host registers through the interoperability API. If your users expect to pull in a library to parse a date format or sign a JWT, you will be writing that bridge in Go yourself. The docs/ directory covers objects, runtime types, operators, builtins, interoperability and the stdlib, and that list is effectively the whole language surface.

Version churn is the other practical risk. v3.0.0 landed in May 2025 while the README still points at the v2 module path and the quick start still imports github.com/d5/tengo/v2. A major version bump in Go means a new module path, so an upgrade is a code change, not a version pin.

## Tengo against goja and Starlark for embedded scripting

The benchmark table names the alternatives directly, so the comparison is not hypothetical. goja runs JavaScript and sits at 5,194ms on the same fib(35) test; starlark-go, Google's Python-like configuration language, is listed at 6,954ms as an interpreter rather than a VM. Both are in the same weight class as Tengo in terms of being embeddable Go libraries.

The difference is what you get for the extra milliseconds. goja gives your users JavaScript, which means a familiar syntax and a mental model they already have, at the cost of a larger and more complex runtime. Starlark is deliberately restricted, with no while loops or recursion in its design, which makes it a strong fit for configuration that must terminate. Tengo sits between them: more permissive than Starlark, smaller and simpler than a JavaScript engine, and faster than both on the project's own measurement.

The choice is really about who writes the scripts. If the answer is "our own engineers, occasionally," Tengo's small surface is an advantage because there is less to learn and less to get wrong. If the answer is "our customers, who already know JavaScript," picking goja removes a training problem at the cost of a heavier binary. Tengo's README does not argue for that trade-off, but the benchmark table makes the cost side visible.

## Maintenance, licensing and what an upgrade actually costs

The repository is not archived, and the last push was on 2026-04-29, roughly five months before this writing. Releases are spaced out: v2.16.1 in June 2023, v2.17.0 in February 2024, then v3.0.0 in May 2025. That cadence suggests a project that moves in deliberate steps rather than continuous churn, and the gap since the last push is worth noting before you assume a steady stream of fixes.

The licence is MIT, which permits commercial use and modification with the copyright notice retained. That is a permissive position, and it is the same licence family as most Go libraries you already depend on. Nothing in the repository suggests dual licensing or a contributor agreement that would complicate vendoring.

The upgrade cost is the concrete thing to plan for. The module path in go.mod is github.com/d5/tengo/v2, the README's install command fetches v2, and the quick start imports v2, while v3.0.0 exists as a release. Go's major-version rule means v3 lives at a different import path, so adopting Tengo today means deciding which line you are on before you write the first script. The Makefile's test target runs go test -race -cover ./... after generate and lint, and lint depends on golint, which is deprecated upstream; expect to adjust that step if you want to run the project's own checks.

## Conclusion

Adopt Tengo if you need a sandboxed expression or rules layer inside a Go service and you are willing to track the v2 to v3 import path change. Do not adopt it if you need a broad standard library, a large third-party package ecosystem, or a language whose semantics you can freeze for years. Before you commit, verify the import path your code will use, whether the standard library modules you need are present in the stdlib directory, and how the project's test target behaves on your Go version.

## FAQ

### What is d5/tengo?

It is a small dynamic script language for Go, compiled to bytecode and executed on a stack-based VM written in native Go with no external dependencies or cgo. The README lists rules engines, state machines, data pipelines and transpilers as use cases.

### How do I install d5/tengo?

The README gives one command for the library: go get github.com/d5/tengo/v2. That matches the module path declared in the repository's go.mod.

### Can I run d5/tengo scripts without embedding it in a Go program?

Yes. The repository has a cmd/tengo directory, and the Makefile invokes it as go run ./cmd/tengo -resolve ./testdata/cli/test.tengo. The README links a separate CLI document for the rest of the flags.

### Does d5/tengo need cgo or external dependencies?

The README states the compiler and runtime are written in native Go with no external deps or cgo, which is why the binary can be cross-compiled like any other Go program.

### How fast is d5/tengo compared with other embedded languages?

The README's own benchmark table lists Tengo at 2,315ms for fib(35) against 5,194ms for goja and 6,954ms for starlark-go, with native Go at 47ms. Those figures come from the project's separate benchmark repository, not from an independent run.

## Sources

- [d5/tengo on GitHub](https://github.com/d5/tengo)
- [License: MIT](https://github.com/d5/tengo/blob/master/LICENSE)
- [Project website](https://tengolang.com)
- [README](https://github.com/d5/tengo/blob/master/README.md)
- [Releases](https://github.com/d5/tengo/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/d5-tengo
