What Goose is
Goose is a small programming language built on top of Go by a single developer (calico32/goose, 3 stars on GitHub). It reads like a mix of Python and Ruby: blocks close with end, and strings use $name for interpolation.
I went in expecting a five-minute install. Instead, the documentation was wrong three separate times, and I learned more from that than from the language itself.
My setup: Debian, Go 1.27.0, a conda (base) environment active (harmless for Go).
The install: when the README is out of date
The README said to run go run cli/goose.go. After cloning, that file did not exist:
$ go run cli/goose.go --help
stat cli/goose.go: no such file or directory
The author had restructured the project since that README was published (May 2024). My next guess, go run ., failed too:
$ go run . --help
package github.com/calico32/goose is not a main package
In Go, only a file declaring package main is a runnable program. The top-level main.go declares package goose, so it is a library, not the program.
The fix was to stop trusting the README and read the Makefile, the recipe the author actually uses to build:
$ grep -F goose.go Makefile
go build $(LDFLAGS) -o build/goose ./bin/cmd/goose.go
A side lesson: plain grep goose.go also matched lines like goose goose-docs. Grep treats . as a regex wildcard meaning any character. Adding -F searches for the literal text.
The working install:
git clone https://github.com/calico32/goose.git
cd goose
make goose
mkdir -p ~/.local/bin
cp build/goose ~/.local/bin/
which goose # /home/lvydvy/.local/bin/goose
Using ~/.local/bin keeps it in my user account, with no sudo needed.
Testing it: predict, run, compare
I wrote small test files with heredocs and predicted each result before running it. Quoting the delimiter (<< 'EOF') mattered: without the quotes, bash would have replaced $name with its own empty variable before Goose ever saw it.
cat > ~/test1.goose << 'EOF'
fn add(a, b = 2)
return a + b
end
println(add(1))
let name = "Dennis"
println("hello $name")
println(10 ** 30)
EOF
| Feature | Result | Evidence |
|---|---|---|
memo caching | Works | Fibonacci example: only 101 calls for 100 numbers |
| Big integers | Works | 100th Fibonacci and 10 ** 30 exact, past the 64-bit limit |
Default parameters, repeat N times, $name interpolation | Works | All outputs matched predictions |
to / step ranges | Works, though listed as “future” | 0 to 10 step 2 printed 0 2 4 6 8 |
int.parse | Works, though listed as “future” | Printed 123 |
int.max | Not built | Printed null |
| Missing properties | Silently return null | A deliberate typo, int.mxa, printed null with no error |
One prediction I got wrong: the docs said integers were 64-bit, so I expected the 93rd Fibonacci number to overflow and go negative. It didn’t. I cross-checked the 100th number with a Python one-liner, and both gave 354224848179261915075.
One small bug: goose -version reports the current time as the “built at” time, not the actual build time.
Security lessons hiding in the docs
The project’s syntax.goose file is half a feature tour and half a wishlist. Reading it closely turned up four things worth flagging:
- A mislabeled hash. It claims
hash.sha256("foo")returns0beec7b5…a33. That is 40 hex characters, 160 bits, and it is actually the SHA-1 hash of “foo”. A real SHA-256 hash is 64 hex characters. Recognizing a hash type by its length is a basic forensics and password-cracking skill. - Importing code from a URL. A planned
import "https://…"would fetch and run remote code on every run. If that host were compromised, every program using it would run the attacker’s code. That is a textbook supply-chain risk. - Unescaped macros. The docs note that
foo!(2 - 3, 5 + 4)gives -7, not -9, because the arguments are pasted in as raw text. That is the same root cause as SQL injection, which thesql!macro example then tries to guard against. - Silent nulls. In the working language, a typo like
int.mxareturnsnullinstead of raising an error. In a check like “is this user an admin?”, a silentnullcan become a logic flaw. Python’sAttributeErroris the safer default.
Verdict
Goose is a neat way to see how a language is built. Its folders map directly onto the pipeline: scanner breaks code into tokens, parser builds a tree, interpreter runs it. That same pattern shows up when parsing packets in Black Hat Python.
As a practical tool, it is not there yet: most of the advertised features are still planned. My study time stays with Python for Black Hat Python and the AWS DVA-C02 exam.
The bigger takeaway is the method. The README was wrong about the file path, the comments were wrong about 64-bit integers, and the “future” label was wrong about what already worked. The Makefile and small, predicted tests were what I could trust. That habit of verifying behavior instead of believing documentation is the core of security work.