A bit about me
I'm Philippe, and I'm Technical Account Manager at GitLab. I am french, but you cannot hear my lovely accent. I love a lot speaking in front of people, mainly in French (it's, of course, easier for me), so I wrote this blog post like if I have to prepare an English talk for a technical event (speaking in English is probably the most challenging exercise I have to face). I hope I'll reuse this stuff one day publicly.
What's the plan?
We will see how to take your first steps in Wasm and some recipes to help you go further through this blog post.
Wasm?
Well, the title says: "Wasm in Golang is fantastic.", but what is "Wasm" in a few words?.
The WebAssembly homepage says: "WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications."
I would say:
- "Wasm is a portable format (like Java or .Net), and you can execute it wherever you have a host capable of it. Initially, the primary host was JavaScript with the browser".
Now, you can run Wasm with JavaScript and NodeJS, and we saw the birth of Wasm runtimes like the Wasmer project recently, allowing running Wasm everywhere.
I like to say that "a wasm file is like a container image but smaller and without an operating system".
Wasm is polyglot, but...
You can compile a Wasm file with several languages: C/C++, Rust, GoLang, Swift, ... And we even saw the emergence of languages dedicated to building Wasm, like AssemblyScript or the promising Grain (keep an eye on it, the grammar is lovely).
This summer, I decided to get started with Wasm. The tendency seems to use Rust for this, but I understood quickly that my baby steps would be complicated. The difficulty did not necessarily come from the language itself. The most tedious and difficult part was all the tools I needed to run a simple "Hello World" in a browser (1)
. After some search, I discovered that Golang provides pretty simple support for Wasm (a lot simpler than with Rust). So, my vacation homework was done with Golang.
The Wasm support with Golang is fantastic. Typically, WebAssembly has four data types (32 & 64 Bit Integer, 32 & 64 Bit Floating-Point Number), and it could be a mess to use functions with String parameters (or even JSON Objects). Thankfully, Go provides the wasm_exec.js
file that facilitates the interaction with the JavaScript API.
(1)
: A month later, I dug more seriously, and I think it's mostly the documentation and examples that are not suitable for beginners. "First steps with Wasm in Rust" could be the subject of a future article.
Prerequisites
To run the examples of this blog post, you'll need:
- Golang 1.16
- TinyGo 0.19.0 (note: TinyGo 0.19.0 doesn't work with GoLang 1.17)
- An http server to serve your webpages
Btw, to serve my pages, I use the Fastify project with this code:
index.js
const fastify = require('fastify')({ logger: true })
const path = require('path')
// Serve the static assets
fastify.register(require('fastify-static'), {
root: path.join(__dirname, ''),
prefix: '/'
})
const start = async () => {
try {
await fastify.listen(8080, "0.0.0.0")
fastify.log.info(`server listening on ${fastify.server.address().port}`)
} catch (error) {
fastify.log.error(error)
}
}
start()
and I use this package.json
file to install Fastify (with npm install
):
package.json
{
"dependencies": {
"fastify": "^3.6.0",
"fastify-static": "^3.2.1"
}
}
For lazy people like me, I created a project here https://gitlab.com/k33g_org/suborbital-demo, and if you open it with GitPod, you'll get a ready to use development environment and you don't need to install anything (with this URL: https://gitpod.io/#https://gitlab.com/k33g_org/suborbital-demo).
The essential "hello world!"
Create a quick'n dirty project
First, create a hello-world
directory and then inside this directory create 2 files:
main.go
index.html
with the following source code:
main.go
package main
import (
"fmt"
)
func main() {
fmt.Println("👋 Hello World 🌍")
// Prevent the function from returning, which is required in a wasm module
<-make(chan bool)
}
index.html
<html>
<head>
<meta charset="utf-8"/>
<script src="wasm_exec.js"></script>
</head>
<body>
<h1>WASM Experiments</h1>
<script>
// This is a polyfill for FireFox and Safari
if (!WebAssembly.instantiateStreaming) {
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer()
return await WebAssembly.instantiate(source, importObject)
}
}
// Promise to load the wasm file
function loadWasm(path) {
const go = new Go()
return new Promise((resolve, reject) => {
WebAssembly.instantiateStreaming(fetch(path), go.importObject)
.then(result => {
go.run(result.instance)
resolve(result.instance)
})
.catch(error => {
reject(error)
})
})
}
// Load the wasm file
loadWasm("main.wasm").then(wasm => {
console.log("main.wasm is loaded 👋")
}).catch(error => {
console.log("ouch", error)
})
</script>
</body>
</html>
Remark: the most important parts are:
- This line
<script src="wasm_exec.js"></script>
- And this line
WebAssembly.instantiateStreaming
, it's the JavaScript API that allows to load the wasm file.
You also need to generate a go.mod
file, by using this command: go mod init hello-world
You should get this content:
module hello-world
go 1.16
Build your first Wasm module
Before building the Wasm module, you need to get the wasm_exec.js
file, and then you'll be able to launch the compilation:
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
GOOS=js GOARCH=wasm go build -o main.wasm
And now, serve your html page with this command node index.js
to run the Fastify http server and navigate to http://localhost:8080 with your favorite browser and open the console developer tools:
So, it's pretty simple to get started, but if you look at the size of main.wasm
, you'll discover that the size of the generated file is around 2.1M!!! And honestly, I find this unacceptable. Fortunately, we have a friendly solution with TinyGo. Let's see that.
source code: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/01-hello-world
The essential "hello world!" with TinyGo
First, What is TinyGo? TinyGo allows compiling Golang source code for microcontrollers, and it can compile Go code to Wasm too. TinyGo is a compiler intended for use in "small places", so the generated files are much smaller.
Duplicate your hello-world
project to a new directory hello-world-tinygo
and change the content of the go.mod
file:
module hello-world-tinygo
go 1.16
Before building the Wasm file, this time, you need to get the wasm_exec.js
related to TinyGo and then you'll be able to launch the compilation:
wget https://raw.githubusercontent.com/tinygo-org/tinygo/v0.19.0/targets/wasm_exec.js
tinygo build -o main.wasm -target wasm ./main.go
If you serve your html page, you'll get the same result as the previous example. But look at the size of main.wasm
. Now, the size is 223K, and it's a lot better.
Keep in mind that TinyGo supports a subset of the Go language, so not everything is available yet (tinygo.org/docs/reference/lang-support). For my experiments, it was enough; otherwise, continue with "pure" Go.
source code: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/02-hello-world-tinygo
My little Cookbook
I have seen too many long tutorials which finally stopped at this simple "hello world" without going any further. They even don't explain how to parameters to functions. Often, it's only a decoration of the "getting started" of projects without ever going further.
So today, I give you all the little recipes that have allowed me to go a little further.
Here are the different interactions between Wasm and the browser that I will cover today:
- Interacting with DOM
- Get a String by calling a Golang function with a String as a parameter
- How to return an object "readable" by JavaScript?
- How to use a JSON object as parameter?
- How to use an array as parameter?
- How to return an array?
Interacting with DOM
We'll use the "syscall/js"
Golang package to add child tags to the Html document object model from Go code. According to the documentation: "Package js gives access to the WebAssembly host environment when using the js/wasm architecture. Its API is based on JavaScript semantics.". This package exposes a small set of features: the type Value
(the Go JavaScript data representation) and the way to request Go from the JavaScript host.
- Create a new directory by copying the previous one and name it
dom
Update the
go.mod
file:module dom go 1.16
Just change the code of main.go
:
package main
import (
"syscall/js"
)
func main() {
message := "👋 Hello World 🌍"
document := js.Global().Get("document")
h2 := document.Call("createElement", "h2")
h2.Set("innerHTML", message)
document.Get("body").Call("appendChild", h2)
<-make(chan bool)
}
Build the code tinygo build -o main.wasm -target wasm ./main.go
and serve the html page node index.js
, then navigate to http://localhost:8080
- We got a reference to the DOM with
js.Global().Get("document")
- We created the
<h2></h2>
element withdocument.Call("createElement", "h2")
- We set the value of the
innerHTML
withh2.Set("innerHTML", message)
- And finally add the element to the body with
document.Get("body").Call("appendChild", h2)
source code: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/03-dom
Now, let's see how to make a callable Go function, that we'll use in our html page.
Call a Go function
This time, we need to "export" the function to the global context (i.e. window
in the browser, global
in NodeJS). Again the "syscall/js"
GoLang package provides the necessary helpers to do that.
As usual, create a new directory first-function
(use the previous example) and update the go.mod
file by changin the value of the module: module first-function
.
This is the source code of main.go
:
package main
import (
"syscall/js"
)
func Hello(this js.Value, args []js.Value) interface{} {
message := args[0].String() // get the parameters
return "Hello " + message
}
func main() {
js.Global().Set("Hello", js.FuncOf(Hello))
<-make(chan bool)
}
- To export the function to the global context, we used the
FuncOf
function:js.Global().Set("Hello", js.FuncOf(Hello))
. TheFuncOf
function is used to create aFunc
type. - The
Hello
function takes two parameters and returns aninterface{}
type. The function will be called synchronously from Javascript. The first parameter (this
) refers to JavaScript'sglobal
object. The second parameter is a slice of[]js.Value
representing the arguments passed to the Javascript function call.
We need to modify the index.html
file to call the Hello
Go function:
index.html
:
<html>
<head>
<meta charset="utf-8"/>
<script src="wasm_exec.js"></script>
</head>
<body>
<h1>WASM Experiments</h1>
<script>
// polyfill
if (!WebAssembly.instantiateStreaming) {
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer()
return await WebAssembly.instantiate(source, importObject)
}
}
function loadWasm(path) {
const go = new Go()
return new Promise((resolve, reject) => {
WebAssembly.instantiateStreaming(fetch(path), go.importObject)
.then(result => {
go.run(result.instance)
resolve(result.instance)
})
.catch(error => {
reject(error)
})
})
}
loadWasm("main.wasm").then(wasm => {
console.log("main.wasm is loaded 👋")
console.log(Hello("Bob Morane"))
document.querySelector("h1").innerHTML = Hello("Bob Morane")
}).catch(error => {
console.log("ouch", error)
})
</script>
</body>
</html>
What changed?, only these 2 lines:
console.log(Hello("Bob Morane"))
: calling theHello
Go function with"Bob Morane"
as parameter and display the result in the browser console.document.querySelector("h1").innerHTML = Hello("Bob Morane")
calling theHello
Go function with"Bob Morane"
as parameter and change theh1
thag value with the result.
So,
- Build the Wasm file:
tinygo build -o main.wasm -target wasm ./main.go
- Serve the html page:
node index.js
You can see that the content of the page is updated, but that we have some error messages in the console. No worries, it's easy to fix; it's a know bug https://github.com/tinygo-org/tinygo/issues/1140, but the workaround is simple:
function loadWasm(path) {
const go = new Go()
//remove the message: syscall/js.finalizeRef not implemented
go.importObject.env["syscall/js.finalizeRef"] = () => {}
return new Promise((resolve, reject) => {
WebAssembly.instantiateStreaming(fetch(path), go.importObject)
.then(result => {
go.run(result.instance)
resolve(result.instance)
})
.catch(error => {
reject(error)
})
})
}
- I only added this line
go.importObject.env["syscall/js.finalizeRef"] = () => {}
to avoid the error message.
Refresh the page, no more issue!:
Right now, you have almost everything you need in your hands to go further. But let me bring to you my other recipes.
source code: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/04-first-function
My other recipes
How to return an object "readable" by JavaScript?
This time, we pass 2 String parameters to the Hello
function (firstName
and lastName
), and to return a json object we use the type map[string]interface{}
:
GoLang function:
func Hello(this js.Value, args []js.Value) interface{} {
firstName := args[0].String()
lastName := args[1].String()
return map[string]interface{}{
"message": "👋 Hello " + firstName + " " + lastName,
"author": "@k33g_org",
}
}
Calling the Hello function from JavaScript is simple:
loadWasm("main.wasm").then(wasm => {
let jsonData = Hello("Bob", "Morane")
console.log(jsonData)
document.querySelector("h1").innerHTML = JSON.stringify(jsonData)
}).catch(error => {
console.log("ouch", error)
})
Serve your page node index.js
:
source code: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/05-return-object
How to use a Json object as parameter when calling Hello?
If I want to use a Json object as parameter in JavaScript, like that:
let jsonData = Hello({firstName: "Bob", lastName: "Morane"})
I will write my GoLang function like this
func Hello(this js.Value, args []js.Value) interface{} {
// get an object
human := args[0]
// get members of an object
firstName := human.Get("firstName")
lastName := human.Get("lastName")
return map[string]interface{}{
"message": "👋 Hello " + firstName.String() + " " + lastName.String(),
"author": "@k33g_org",
}
}
args[0]
contains the Json object- Use the
Get(field_name)
method to retrieve the value of fields
source code: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/06-json-as-parameter
How to use an array as parameter when calling Hello?
JavaScript call:
let jsonData = Hello(["Bob", "Morane", 42, 1.80])
Golang function:
func Hello(this js.Value, args []js.Value) interface{} {
// get members of an array
firstName := args[0].Index(0)
lastName := args[0].Index(1)
age := args[0].Index(2)
size := args[0].Index(3)
return map[string]interface{}{
"message": "👋 Hello",
"firstName": firstName.String(),
"lastName": lastName.String(),
"age": age.Int(),
"size": size.Float(),
"author": "@k33g_org",
}
}
source code: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/07-array-as-parameter
How to return an array?
Golang function:
func GiveMeNumbers(_ js.Value, args []js.Value) interface{} {
return []interface{} {1, 2, 3, 4, 5}
}
source code: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/08-return-an-array
So, that's it for this time. I'm still learning about Wasm and the Js package of Golang, but I've already had some serious fun with all of this.
Note from the editor
This delightful guest post from Philippe is the third in our Foundations series, focused on helping developers learn how to build with WebAssembly. You can take a look at part 1 and part 2, which talk about getting started with TypeScript and WebAssembly. Our newsletter (below) is the best way to stay up to date with our Foundations series and other WebAssembly-related news. Thanks Philippe!