That said, a trivial “Hello World!” isn’t a meaningful benchmark. If you’re going to play that game, you might as well swap `fmt.Printf` for `fmt.Println`, or even `println` to avoid the import statement entirely. At that point, you’re no longer comparing anything interesting, the binaries end up roughly the same size anyway.
I find it quite interesting that import of "fmt" package alone leads to a 2+ MiB binary :). But, to be fair, TinyGo doesn't seem to treat "fmt.Printf" function any differently from others, so it does compile the same source code as the regular Go compiler and just has better escape analysis, dead code elimination, etc.
TinyGo was instrumental in getting https://github.com/rcarmo/go-rdp to work. It generates very tight, pretty high performing WASM, and that allowed me to push all the RDP decoding to the browser side while making sure I had a sane test suite. Heartily recommended.
Definitely don't recommend that since it works when it does and doesn't otherwise. Most users will not end up happy trying to make it work since the alternative is more common.
This isn't a fault of TinyGo itself, it is just targeting a space that doesn't really prioritize embedded but got picked up for that just because. But without fixing this Wasm ecosystem issue, compiling Go to Wasm will never be a real thing.
Interestingly enough for the C and C++ folks, compiler specific dialects for embedded without standard library, are still argued for as if being C and C++.
Thats the host, but the guess is missing, as stated in the issue last year "I guess we could add sockets to wasip2". To be honest, even BigGo failed me on websocket conns on both wasmtime and wazero (connects and gets into an inf loop on reply). WebWorkers and GOOS=js work like a charm tho on BigGo in FF/chrome.
Lot of stdlib, especially net, crypto, in tinygo doesn't compile, or if compiles has stubs as implementation that panics with not implemented. Few years ago, I tried compiling small terminal http client app and failed at compile stage.
Writing embedded code with an async-aware programming language is wonderful (see Rust's embassy), but wonder how competitive this is when you need to push large quantities of data through a micro controller, I presume this is not suitable for real-time stuff?
You can disable GC in tinygo, so if you allocate all the necessary buffers beforehand it can have good performance with real-time characteristics. If you _need_ dynamic memory allocation then no, because you need GC it can't provide realtime guarantees.
Doesn't seem like those should be mutually exclusive, though the habits involved are quite opposing and I can definitely believe they're uncommon.
E.g. GC doesn't need to be precise. You could reserve CPU budget for GC, and only use that much at a time before yielding control. As long as you still free enough to not OOM, you're fine.
I've written a fair amount of code for EmbeddedGo. Garbage Collector is not an issue if you avoid heap allocations in your main loop. But if you're CPU bound a goroutine might block others from running for quite some time. If your platform supports async preemption, you might be able to patch the goroutine scheduler with realtime capabilities.
It's really not so different! In embassy, DMA transfers and interrupts become things that you can .await on, the process is basically:
* The software starts a transaction, or triggers some event (like putting data in the fifo)
* The software task yields
* When the "fifo empty" interrupt or "dma transfer done interrupt" occurs, it wakes the task to resume
* the software task checks if it is done, and either reloads/restarts if there's more to do, or returns "done"
It's really not different than event driven state machines you would have written before, it's just "in-band" of the language now, and async/await gives you syntax to do it.
Even if you don't know Rust, I'd suggest poking around at some of the examples here:
Precisely, I would say embassy is a satisfying middle-point between "baremetal" firmware and running something like FreeRTOS / NuttX that hides the event loop from you.
Its a fantastic project, but seems almost inactive now. I have a tiny PR pending for weeks, even reviewed, but not merged. I have another patch I have not submitted as I want to first navigate the earlier PR to completion. Both were bugs that bit me, and I ended up wasting quite a lot of time trying to find it:
1. go:embed supports "all:<pattern" while tinygo silently ignore it. I ripped my hair out trying to figure out why my files were not showing up in embedfs. PR pending.
2. go allows setting some global vars at the build cli (think build version/tag etc). In the code, one can define a default, and then the value provided (if any) on the cli can override it at build time. Tinygo fails to override the value at build-time, silently, if a default value is provided for the var in code. This PR I have not submitted yet, as its more intrusive.
I hope it picks up steam again soon. I love using go for embedded and CF worker use and tinygo makes both of these use cases much more viable than regular go. Honestly, I hope tinygo can be rolled over into the main go toolchain as "target arch".
Tinygo made a lot of progress over the years -- e.g. they've recently introduced macOS support!
It does indeed produce much smaller binaries, including for macOS.
What does it look like if you pass -ldflags=“-s -w”?
With Go v1.26.1
Binary sizes:• 2581616B (2.5MB) → 1714560B (1.6MB) (with -ldflags="-s -w")
• 1531920B (1.5MB) → 753680B (0.7MB) (with upx --force-macos)
That said, a trivial “Hello World!” isn’t a meaningful benchmark. If you’re going to play that game, you might as well swap `fmt.Printf` for `fmt.Println`, or even `println` to avoid the import statement entirely. At that point, you’re no longer comparing anything interesting, the binaries end up roughly the same size anyway.
I find it quite interesting that import of "fmt" package alone leads to a 2+ MiB binary :). But, to be fair, TinyGo doesn't seem to treat "fmt.Printf" function any differently from others, so it does compile the same source code as the regular Go compiler and just has better escape analysis, dead code elimination, etc.
TinyGo was instrumental in getting https://github.com/rcarmo/go-rdp to work. It generates very tight, pretty high performing WASM, and that allowed me to push all the RDP decoding to the browser side while making sure I had a sane test suite. Heartily recommended.
We're using TinyGo and the Wazero runtime for our WASM plugin system in ServiceRadar, highly recommend both if you're using golang.
Definitely don't recommend that since it works when it does and doesn't otherwise. Most users will not end up happy trying to make it work since the alternative is more common.
This isn't a fault of TinyGo itself, it is just targeting a space that doesn't really prioritize embedded but got picked up for that just because. But without fixing this Wasm ecosystem issue, compiling Go to Wasm will never be a real thing.
https://github.com/WebAssembly/gc/issues/59
Yay wazero maintainer here, thanks for the shout-out!
It was good to meet at wasm.io!
Wazero is awesome. For anyone wanting to embed in languages other than Go, check out Extism.
TinyGo doesnt have networking in WASI[0] and the WASM websocket module[1] was last updated 5 years ago. Go without stdlib is not Go.
[0] https://github.com/tinygo-org/tinygo/issues/4880
[1] https://github.com/Nerzal/tinywebsocket
Interestingly enough for the C and C++ folks, compiler specific dialects for embedded without standard library, are still argued for as if being C and C++.
For embedded systems, see https://github.com/tinygo-org/net
For WASI, check out WASI Preview 2, https://docs.wasmtime.dev/api/wasmtime_wasi/p2/index.html
Thats the host, but the guess is missing, as stated in the issue last year "I guess we could add sockets to wasip2". To be honest, even BigGo failed me on websocket conns on both wasmtime and wazero (connects and gets into an inf loop on reply). WebWorkers and GOOS=js work like a charm tho on BigGo in FF/chrome.
Yeah, I suspect sockets in wasip2, and wasip2 in general, are just now having the bugs shaken out of them.
The company I work for is just now upgrading its edge WASI support to wasip2.
Could we compile tailscale with tinygo to run it on openwrt? Last time I checked tailscale was too large for 8MB flash routers
Lot of stdlib, especially net, crypto, in tinygo doesn't compile, or if compiles has stubs as implementation that panics with not implemented. Few years ago, I tried compiling small terminal http client app and failed at compile stage.
https://tinygo.org/docs/reference/lang-support/stdlib/
What are the tradeoffs compared to standard Go?
It gets better every release, but there are missing language features:
https://tinygo.org/docs/reference/lang-support/
And parts of the stdlib that don't work:
https://tinygo.org/docs/reference/lang-support/stdlib/
Writing embedded code with an async-aware programming language is wonderful (see Rust's embassy), but wonder how competitive this is when you need to push large quantities of data through a micro controller, I presume this is not suitable for real-time stuff?
You can disable GC in tinygo, so if you allocate all the necessary buffers beforehand it can have good performance with real-time characteristics. If you _need_ dynamic memory allocation then no, because you need GC it can't provide realtime guarantees.
Doesn't seem like those should be mutually exclusive, though the habits involved are quite opposing and I can definitely believe they're uncommon.
E.g. GC doesn't need to be precise. You could reserve CPU budget for GC, and only use that much at a time before yielding control. As long as you still free enough to not OOM, you're fine.
We're streaming RSTP camera feeds through WASM plugins and host-bridge adapters, no problem. I was surprised how well it worked TBH.
https://code.carverauto.dev/carverauto/serviceradar/src/bran...
I've written a fair amount of code for EmbeddedGo. Garbage Collector is not an issue if you avoid heap allocations in your main loop. But if you're CPU bound a goroutine might block others from running for quite some time. If your platform supports async preemption, you might be able to patch the goroutine scheduler with realtime capabilities.
It is more performant than rust but less performant than Zig when not doing GC heavy stuff. https://github.com/tinygo-org/tinybench
Can you elaborate on this and how it would be different from signaling on interrupts and DMA?
Hardware-level async makes sense to me. I can scope it. I can read the data sheet.
Software async in contrast seems difficult to characterize and reason about so I've been intimidated.
It's really not so different! In embassy, DMA transfers and interrupts become things that you can .await on, the process is basically:
It's really not different than event driven state machines you would have written before, it's just "in-band" of the language now, and async/await gives you syntax to do it.Even if you don't know Rust, I'd suggest poking around at some of the examples here:
https://github.com/embassy-rs/embassy/tree/main/examples
And if you want, look into the code behind it.
Precisely, I would say embassy is a satisfying middle-point between "baremetal" firmware and running something like FreeRTOS / NuttX that hides the event loop from you.
Does it support ESP32
Full list is here:
https://tinygo.org/docs/reference/microcontrollers/
A few ESP32s on there.
Another one was added an hour ago, https://github.com/tinygo-org/tinygo/pull/5280
Its a fantastic project, but seems almost inactive now. I have a tiny PR pending for weeks, even reviewed, but not merged. I have another patch I have not submitted as I want to first navigate the earlier PR to completion. Both were bugs that bit me, and I ended up wasting quite a lot of time trying to find it:
1. go:embed supports "all:<pattern" while tinygo silently ignore it. I ripped my hair out trying to figure out why my files were not showing up in embedfs. PR pending.
2. go allows setting some global vars at the build cli (think build version/tag etc). In the code, one can define a default, and then the value provided (if any) on the cli can override it at build time. Tinygo fails to override the value at build-time, silently, if a default value is provided for the var in code. This PR I have not submitted yet, as its more intrusive.
I hope it picks up steam again soon. I love using go for embedded and CF worker use and tinygo makes both of these use cases much more viable than regular go. Honestly, I hope tinygo can be rolled over into the main go toolchain as "target arch".
Looking at the commit history, it seems pretty active:
https://github.com/tinygo-org/tinygo/commits/dev/
Maybe the opposite thing is happening, whereby the maintainers are getting overwhelmed?
It'd be nice to get some help :) TinyGo developers are community driven as we receive no financing.
FWIW the company I work for has been helping out a little here and there, one of our engineers has four commits in so far this year.
There have been four committers in the last week, so it's active!
Consider joining the slack channel #tinygo-dev on gophers.slack.com and pinging them about the PR.