WebAssembly: Introduction to WebAssembly using Go
Welcome to tutorial no. 1 of our WebAssembly tutorial series.
Series Index
Introduction to WebAssembly Using Go
DOM Access and Error Handling
What is WebAssembly?
JavaScript has been the only programming language that the browser understands. JavaScript has stood the test of time and it has been able to deliver the performance needed by most web applications. But when it comes to 3D games, VR, AR, and image editing apps, JavaScript is not quite up to the mark since it is interpreted. Although JavaScript engines such as Gecko and V8 have Just in Time compilation capabilities, JavaScript is not able to provide the high performance required by modern web applications.
WebAssembly(also known as wasm) is meant to solve this problem. WebAssembly is a virtual assembly language for the browser. When we say virtual, it means that it cannot be run natively on the underlying hardware. Since the browser can be running on any CPU architecture, it is not possible for the browser to run WebAssembly directly on the underlying hardware. But this highly optimized virtual assembly format can be processed much quicker than vanilla JavaScript by modern browsers since it is compiled and is more close to the hardware architecture than JavaScript. The following figure shows where WebAssembly stands in the stack when compared to Javascript. It is closer to the Hardware than JavaScript.
The existing JavaScript engines have support to run WebAssembly’s Virtual Assembly Code.
WebAssembly is not meant to replace JavaScript. It is meant to operate hand in hand with JavaScript to take care of performance critical components of a web application. It is possible to make calls from JavaScript to WebAssembly and vice versa.
WebAssembly is not generally coded by hand, but rather, it is cross compiled from other high level programming languages. For example, it is possible to cross compile Go, C, C++, and Rust code to WebAssembly. Thus the module which has already been coded in some other programming language can be cross compiled to WebAssembly and used in the browser directly.
What are we developing?
In this tutorial, we will cross compile a Go application to WebAssembly and run it on the browser.
We will create a simple application that is used to format JSON ๐. If a JSON without any formatting is passed as input, it will be formatted and printed.
For example, if the input JSON is
{"website":"golangbot.com", "tutorials": [{"title": "Strings", "url":"/strings/"}, {"title":"maps", "url":"/maps/"}, {"title": "goroutines","url":"/goroutines/"}]}
It will be formatted as shown below and displayed in the browser.
{
"website": "golangbot.com",
"tutorials": [
{
"title": "Strings",
"url": "/strings/"
},
{
"title": "maps",
"url": "/maps/"
},
{
"title": "goroutines",
"url": "/goroutines/"
}
]
}
We will also be creating a UI for this application and manipulating the browser’s DOM from Go using Javascript, but that’s in the next tutorial.
Hello World WebAssembly Program Cross Compiled from Go
Let’s start by writing a simple hello world program in Go, cross compile it to WebAssembly, and run it on the browser. We will further modify this application as the tutorial progress and add the functionality to format JSON.
Let’s create the following directory structure inside the Documents
directory.
Documents/
โโโ webassembly
โโโ assets
โโโ cmd
โโโ server
โโโ wasm
Running the following command will create the above directory structure
mkdir -p ~/Documents/webassembly/assets \
~/Documents/webassembly/cmd \
~/Documents/webassembly/cmd/server \
~/Documents/webassembly/cmd/wasm
The use of each of these folders will be clear as the tutorial progresses.
The first step is to create a go module with name github.com/golangbot/webassembly
. (Please visit https://golangbot.com/go-packages/ if you would like to know more about go modules) Please fell free to use any name for the module.
Go to ~/Documents/webassembly
and run
go mod init github.com/golangbot/webassembly
This will create a go module named github.com/golangbot/webassembly
Create a file named main.go
with the following contents inside ~/Documents/webassembly/cmd/wasm
.
1package main
2
3import (
4 "fmt"
5)
6
7func main() {
8 fmt.Println("Go Web Assembly")
9}
Let’s cross compile the above Go program into WebAssembly. The following command will cross compile this Go program and place the output binary inside the assets
folder.
cd ~/Documents/webassembly/cmd/wasm/
GOOS=js GOARCH=wasm go build -o ../../assets/json.wasm
The above command uses js
as GOOS
and wasm
(short form for WebAssembly) as the architecture. Running the above command will create the WebAssembly module json.wasm
in the assets
directory. Congrats, we have successfully cross compiled our first Go program to WebAssembly ๐.
One important fact is that it is possible to cross compile only the main
package to WebAssembly. Hence we have written our code in the main package.
If you try to run this compiled binary in the terminal,
$]~/Documents/webassembly/assets/json.wasm
-bash: json.wasm: cannot execute binary file: Exec format error
we will get the above error. This is because this binary is a wasm
binary and is supposed to be run inside a browser sandbox. The Linux/Mac OSes don’t understand the format of this binary. Hence we get this error.
Javascript Glue
As we already discussed, WebAssembly is supposed to exist hand in hand with JavaScript. Hence some JavaScript glue code is needed to import the WebAssembly Module we just created and run it in the browser. This code is already available in the Go installation. Let’s go ahead and copy it to our assets directory.
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" ~/Documents/webassembly/assets/
The above command copies the wasm_exec.js
that contains the glue code to run WebAssembly into the assets
directory.
As you would have guessed by now, the assets
folder will contain all the HTML, JavaScript, and wasm code which will be served using a web server later.
Index.html
Now we have the wasm binary ready and also have the glue code, the next step is to create the index.html
file and import our wasm binary.
Let’s create a file named index.html
in the assets
directory with the following contents. This file contains boilerplate code to run the WebAssembly module and it can be found in the WebAssembly Wiki.
1<!doctype html>
2<html>
3 <head>
4 <meta charset="utf-8"/>
5 <script src="wasm_exec.js"></script>
6 <script>
7 const go = new Go();
8 WebAssembly.instantiateStreaming(fetch("json.wasm"), go.importObject).then((result) => {
9 go.run(result.instance);
10 });
11 </script>
12 </head>
13 <body></body>
14</html>
The current directory structure after creating index.html
is provided below.
1Documents/
2โโโwebassembly/
3 โโโ assets
4 โย ย โโโ index.html
5 โย ย โโโ json.wasm
6 โย ย โโโ wasm_exec.js
7 โโโ cmd
8 โย ย โโโ server
9 โย ย โโโ wasm
10 โย ย โโโ main.go
11 โโโ go.mod
Although the contents of index.html
is standard boilerplate, a little understanding doesn’t hurt. Let’s try to understand the code in index.html
a little. The instantiateStreaming function is used to initialize our json.wasm
WebAssembly module. This function returns a WebAssembly instance
which contains the list of WebAssembly functions that can be called from JavaScript. This is required to call our wasm functions from JavaScript. The use of this will be more clear as the tutorial progresses.
WebServer
Now we have our JavaScript glue, index.html, and our wasm binary ready. The only missing piece is that we need to create a web server to serve the contents of the assets folder. Let’s do that now.
Create a file named main.go
inside the server
directory. The directory structure after creating main.go
is provided below.
1Documents/
2โโโ webassembly
3 โโโ assets
4 โย ย โโโ index.html
5 โย ย โโโ json.wasm
6 โย ย โโโ wasm_exec.js
7 โโโ cmd
8 โย ย โโโ server
9 โย ย โย ย โโโ main.go
10 โย ย โโโ wasm
11 โย ย โโโ main.go
12 โโโ go.mod
Copy the following code to ~/Documents/webassembly/cmd/server/main.go
.
1package main
2
3import (
4 "fmt"
5 "net/http"
6)
7
8func main() {
9 err := http.ListenAndServe(":9090", http.FileServer(http.Dir("../../assets")))
10 if err != nil {
11 fmt.Println("Failed to start server", err)
12 return
13 }
14}
The above program creates a file server listening at port 9090
with the root at our assets
folder. Just what we wanted. Let’s run the server and see our first WebAssembly program running.
cd ~/Documents/webassembly/cmd/server/
go run main.go
Now the server is listening at port 9090
. Go to your favorite web browser and type http://localhost:9090/
. You can see that the page is empty. Don’t worry about it, we will create the UI in the upcoming sections.
Our interest right now is to see the JavaScript console. Right click and select inspect element
in the browser.
This will open the developer console. Tap on the tab named “console”.
You can see the text Go Web Assembly
printed in the console. Awesome, we have successfully run our first Web Assembly program written using Go. Our web assembly module cross compiled from Go has been delivered by our server to the browser and it has been executed successfully by the browser’s Javascript engine.
Let’s take this tutorial to the next level and write the code for our JSON formatter.
Coding the JSON formatter
Our JSON formatter will take an unformatted JSON as input, format it, and return the formatted JSON string as output. We will be using the MarshalIndent function to accomplish this.
Add the following function to ~/Documents/webassembly/cmd/wasm/main.go
1func prettyJson(input string) (string, error) {
2 var raw any
3 if err := json.Unmarshal([]byte(input), &raw); err != nil {
4 return "", err
5 }
6 pretty, err := json.MarshalIndent(raw, "", " ")
7 if err != nil {
8 return "", err
9 }
10 return string(pretty), nil
11}
The MarshalIndent
function takes 3 parameters as input. The first one is the raw unformatted JSON, the second one is the prefix to add to each new line of the JSON. In this case, we don’t add a prefix. The third parameter is the string to be appended for each indent of our JSON. In our case, we give pass two spaces. Simply put, for each new indent of the JSON, two spaces will be added and hence the JSON will be formatted.
If the string {"website":"golangbot.com", "tutorials": [{"title": "Strings", "url":"/strings/"}]}
is passed as input to the above function, it will return the following formatted JSON string as output.
{
"website": "golangbot.com",
"tutorials": [
{
"title": "Strings",
"url": "/strings/"
}
]
}
Exposing a function from Go to Javascript
Now we have the function ready but we are yet to expose this function to Javascript so that it can be called from the front end.
Go provides the syscall/js package which helps in exposing functions from Go to Javascript.
The first step in exposing a function from Go
to JavaScript
is to create a Func type. Func is a wrapped Go function that can be called by JavaScript. The FuncOf function can be used to create a Func
type.
Add the following function to ~/Documents/webassembly/cmd/wasm/main.go
1func jsonWrapper() js.Func {
2 jsonFunc := js.FuncOf(func(this js.Value, args []js.Value) any {
3 if len(args) != 1 {
4 return "Invalid no of arguments passed"
5 }
6 inputJSON := args[0].String()
7 fmt.Printf("input %s\n", inputJSON)
8 pretty, err := prettyJson(inputJSON)
9 if err != nil {
10 fmt.Printf("unable to convert to json %s\n", err)
11 return err.Error()
12 }
13 return pretty
14 })
15 return jsonFunc
16}
The FuncOf
function takes a first class function function with two js.Value
parameters and a any
return type as input. The function which is passed to FuncOf
will be called synchronously from Javascript. The first parameter of this function is Javascript’s this
keyword. this
refers to JavaScript’s global
object. The second parameter is a slice of []js.Value
which represents the arguments that will be passed to the Javascript function call. In our case, it will be the unformatted JSON input string. Don’t worry if this doesn’t make sense. Once the program is complete, you will be able to understand better :).
After importing "syscall/js"
you might see the following error in your editor.
error while importing syscall/js: build constraints exclude all Go files in /usr/local/go/src/syscall/js
The reason for the above error is because syscall/js
package is supposed to be compiled on wasm
architecture with js
as the OS and our editor is not aware of this. The architecture and OS requirements are specified in the source code of the syscall/js
package at https://cs.opensource.google/go/go/+/refs/tags/go1.19.2:src/syscall/js/func.go;l=5. The line //go:build js && wasm
in the source code specifies that this file will build on js
OS and wasm
architecture.
This error can be ignored since we are already specifying the OS and architecture when compiling the wasm binary from the CLI. I have provided the command again below for reference where the OS and ARCH is specified.
cd ~/Documents/webassembly/cmd/wasm/
GOOS=js GOARCH=wasm go build -o ../../assets/json.wasm
In the jsonWrapper
function mentioned above, we first check whether only one argument has been passed from Javascript in line no. 3. This check is needed because we expect only one JSON string argument. If not we return a string message stating Invalid no of arguments passed
. We do not explicitly return any error type from Go to Javascript. Error handling will be taken care of in the next tutorial.
We get the JSON input using args[0].String()
. This represents the first parameter passed from JavaScript. This will be more clear as the tutorial progresses. After the input JSON is obtained, we call the prettyJson
function in line no. 8, and return the output.
When returning a value from Go to Javascript, the ValueOf function will be used automatically by the compiler to convert the Go value to a JavaScript value. In this case, we are returning a string
from Go, hence it will be converted to the corresponding JavaScript’s string type using js.ValueOf()
by the compiler.
We assign the FuncOf
’s return value to jsonFunc
. Now jsonFunc
contains the function which will be called from Javascript. We return jsonFunc
in line no. 15.
Now we have the function ready which can be called from Javascript. We are still one step away.
We need to expose the function we just created so that it can be called from Javascript. The way we will expose the Go function to Javascript is by setting the formatJSON
string property of JavaScript’s global object to the js.Func
returned by jsonWrapper()
.
The line of code that does this is,
js.Global().Set("formatJSON", jsonWrapper())
Add this to the end of the main()
function. In the above code, we have set the formatJSON
property of Javascript’s Global object to the return value of jsonWrapper()
function. Now the jsonFunc
which format’s the JSON can be called from JavaScript using the function name formatJSON
.
The complete program is provided below.
1package main
2
3import (
4 "fmt"
5 "encoding/json"
6 "syscall/js"
7)
8
9func prettyJson(input string) (string, error) {
10 var raw any
11 if err := json.Unmarshal([]byte(input), &raw); err != nil {
12 return "", err
13 }
14 pretty, err := json.MarshalIndent(raw, "", " ")
15 if err != nil {
16 return "", err
17 }
18 return string(pretty), nil
19}
20
21func jsonWrapper() js.Func {
22 jsonFunc := js.FuncOf(func(this js.Value, args []js.Value) any {
23 if len(args) != 1 {
24 return "Invalid no of arguments passed"
25 }
26 inputJSON := args[0].String()
27 fmt.Printf("input %s\n", inputJSON)
28 pretty, err := prettyJson(inputJSON)
29 if err != nil {
30 fmt.Printf("unable to convert to json %s\n", err)
31 return err.Error()
32 }
33 return pretty
34 })
35 return jsonFunc
36}
37
38func main() {
39 fmt.Println("Go Web Assembly")
40 js.Global().Set("formatJSON", jsonWrapper())
41}
Let’s compile and test our program.
cd ~/Documents/webassembly/cmd/wasm/
GOOS=js GOARCH=wasm go build -o ../../assets/json.wasm
cd ~/Documents/webassembly/cmd/server/
go run main.go
The above commands will compile the wasm binary and start our webserver.
Calling the Go function from JavaScript
We have exposed the Go function to JavaScript successfully. Let’s check whether it works.
Go the browser and open the same URL http://localhost:9090/
again and open the Javascript console.
Type the following command in the Javascript console.
formatJSON('{"website":"golangbot.com", "tutorials": [{"title": "Strings", "url":"/strings/"}]}')
The above command calls the formatJSON
JavaScript function which we exposed from Go and passes it as JSON string as an argument. Hit enter. Did it work?
Sorry :) bummer. You would have got the error Error: Go program has already exited
The reason as the error mentions is our Go program has already exited when it was called from Javascript. How do we fix this ๐ค? Well, it’s pretty simple. We must ensure that the Go program is running when JavaScript calls it. The easy way to do this in Go is to keep waiting on a channel.
1func main() {
2 fmt.Println("Go Web Assembly")
3 js.Global().Set("formatJSON", jsonWrapper())
4 <-make(chan struct{})
5}
In the above snippet, we are waiting on a channel. Please add the last line of the above snippet to ~/Documents/webassembly/cmd/wasm/main.go
and compile and rerun the program. Try running the following command in the browser again.
formatJSON('{"website":"golangbot.com", "tutorials": [{"title": "Strings", "url":"/strings/"}]}')
Now the JSON will be formatted and printed.
If no parameters are passed,
formatJSON()
we will get the message,
"Invalid no of arguments passed"
in the output.
Great. We have successfully called a function written using Go from JavaScript.
The source code for this tutorial is available at https://github.com/golangbot/webassembly/tree/tutorial1/
In the next tutorial, we will create a UI for our application, handle errors, and also modify the browser’s DOM from Go.
I hope you liked this tutorial. Please leave your feedback and comments. Please consider sharing this tutorial on twitter or LinkedIn. Have a good day.
Next tutorial - DOM Access and Error Handling