Golang: How To Guide

Build Powerful Client-Side Web Apps Using Go

Learn how to write and run Go code for a web browser with Web Assembly (WASM), and minimize the size of client-side code using TinyGo

Maina Wycliffe
The GoDev Corner

--

Picture of the Golang gopher on a raft with a chalkboard saying “Using Go with WASM: What You Need To Know”

From the beginning, web browsers have always been strict on what languages they support (i.e., HTML, CSS, and Javascript), naturally limiting the ability to build apps targeting web browsers.

But even though Javascript has evolved quite admirably into a modern general-purpose language (see NodeJS and Deno), it still has inherent weaknesses, ultimately restricting its ability to create web apps that are powerful and performant.

Subsequently, the Web Assembly standard (WASM) by TC39 was created a few years ago, allowing developers to compile code from any programming language (such as Go) to run in the web browser.

WASM burst the doors wide open for the number of languages supported by web browsers. With it, you can now write applications that run in near-native performance, creating web apps that are both powerful and performant.

👉 Note: Web Assembly is a binary instruction format for a stack-based virtual machine. It was designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications.

Currently, it is supported by all the major browsers — Chromium-based browsers (Chrome, Edge, Brave, etc.), Mozilla, and Safari.

The number of possibilities to create web apps in the browser suddenly became exponential, and languages like Rust, Python, and Blazor all shared in massive usage increases, especially over the last year. (Source)

But there’s also another language climbing the chart in popularity — Go.

So in this article, we’ll run through how to write Go code that optimally runs in the browser using WASM and give a little insight into how best to approach it.

Survey Questions: (Left) Which language do you currently use for WebAssembly development?; (Right) Which language do you most want to use for WebAssembly development? (n=299)

❗️TL;DR

Web Assembly (WASM) can be used to build web apps that are powerful and performant as they run at near native speed. This opens up the door for powerful web apps such as video and photo editors to run on the web browser similar to their native counterparts.

Now, you can write those same applications in Go with the help of WASM, helping you prioritize the requirements for web app success and future growth. This article walks you through the process of using Go with WASM.

The GoDev Corner unique separator

Short Preface

What you’ll learn

Requirements

  • Download TinyGo if you’d like to follow along. Otherwise, we’ll walk through everything.
The GoDev Corner unique separator

Getting Started

Cross-compiling the WASM/Go application

Let’s start by creating a simple Go application that just prints "Hello World from Go” with WASM in the browser console:

👉 Note: Feel free to test this by running go run main.go in your terminal

To actually run this in the browser, though, we need to do a few things. First, we need some Javascript glue code that will import the web assembly binary into our browser to then execute it.

To do so, we can copy the code below from our Go installation, as it comes standard in the misc/wasm/wasm_exec.js directory inside GOROOT (the location of the Go SDK).

Let’s copy it into a directory called “public” in our project directory:

cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" ./public/

👉 Note: To see what the Javascript glue code we copied looks like, click here.

Then, we will need to cross-compile our WASM/Go application by setting the environment variables GOOS to js , and GOARCH to WASM. This allows us to compile our Go application for WASM to run within the WASM environment.

Now let’s save our WASM binary in the same public directory we saved our JS glue code, naming it wasm.wasm.

GOOS=js GOARCH=wasm go build -o public/wasm.wasm

Lastly, we need to create an index.html file for our server. Inside it, we need to load the Javascript glue code we copied from GOROOT and then load out the cross-compiled WASM file. Then can we execute it:

👉 Note: The important part is the second script, which is going to fetch our WASM binary and then execute it by calling the go.run function. That will subsequently fire up the go.main function.

Once you do this, you should seeHello World from Go (from our Go code earlier) written into the browser console.

Creating and running a Go web server

Next, we need a web server to serve our web app so that we can view the results in the browser. There are several ways we can do this, but the most straightforward way is to create a Go web server that serves our public directory. We will place the code for the server in the servers directory within our project directory.

Here’s what that looks like:

package main

import (
"log"
"net/http"
)

func main() {
fs := http.FileServer(http.Dir("../public"))
http.Handle("/", fs)

log.Print("Listening on :3000...")
err := http.ListenAndServe(":3000", nil)
if err != nil {
log.Fatal(err)
}
}

And then, we can run the Go web server by using the go run server/main.go command and then visit the URL http://localhost:3000 to view the results.

If you open the browser console, you should see our “Hello World From Go” message printed there:

Picture of an output reading “Hello world from Go”

Congratulations! Now you’ve created a Go application that runs in the browser.

The GoDev Corner unique separator

Calling Go Functions From Our Browser

Example: Setting up a simple calculator

Now that we’ve created a simple application let’s execute some Go functions and use Go to perform some functionality in our browser.

To demonstrate, we can start by creating a simple calculator, for example, that can help us square and cube a number. The functions to do so will be written in Go.

Here is what the UI looks like:

UI for the example calculator we created as a Go application

From our browser, we’ll be able to call the functions and execute the code, just like any other JS code and browser APIs.

Here’s what our functions should look like:

func calculateSquare(x int) int {
return x * x
}

func calculateCube(x int) int {
return x * x * x
}

Next, we can expose our functions to Javascript so they are callable using the syscall/js package. This will give our Go application access to the WASM host environment, which in our case, is the browser.

Using js.Func

Now we need to create a new function that returns js.Func, a wrapped Go function to be called by JavaScript. That will then return js.FuncOf, which will accept a callback function as an argument.

The callback will receive the Javascriptthis” object as the first argument, then any function arguments when the function itself is called as an array.

So, for example, if we called calculateSquare(5), the second parameter will contain an array with “5” as the first index.

Inside the callback function, let’s define the number as an integer and call calculateSquare:


func calculateSquareWrapper() js.Func {
return js.FuncOf(func(this js.Value, args []js.Value) any {
if len(args) != 1 {
return "Invalid no of arguments passed"
}
x := args[0].Int()
return calculateSquare(x)
})
}

Then, let’s do the same for the calculateCube function:

func calculateCubeWrapper() js.Func {
return js.FuncOf(func(this js.Value, args []js.Value) any {
if len(args) != 1 {
return "Invalid no of arguments passed"
}
x := args[0].Int()
return calculateCube(x)
})
}

❗️ Remember: Double-check that the callback function is called with the correct number of arguments

This will wrap up our Go functions so they’re callable by Javascript, but we still need to expose them to our browser.

To do so, add both functions to the Javascript “global” object using the js.Global().Set()function, which accepts two arguments: (1) the name of the function; and (2) the function to be called. Here’s what that looks like:

func main() {
js.Global().Set("calculateCube", calculateCubeWrapper())
js.Global().Set("calculateSquare", calculateSquareWrapper())
}

Now let’s open a channel to block our Go application from exiting, as we need to ensure our functions from Javascript are called first.

func main() {
js.Global().Set("calculateCube", calculateCubeWrapper())
js.Global().Set("calculateSquare", calculateSquareWrapper())

// make sure the program doesn't exit
<-make(chan bool)
}

Lastly, cross-compile the Go application targeting WASM. Here, we’ll save the WASM binaries in the public directory and name it calculator.wasm:

GOOS=js GOARCH=wasm go build -o public/calculator.wasm ./wasm/main.go

Implementing the UI

The next step here is to modify our index.html. We want to add both the form and the function necessary for the calculations.

Let’s keep it simple — a single input and two buttons should do:

<html>
<head>
<meta charset="utf-8" />
<script src="wasm_exec.js"></script>
<script>
const go = new Go();
<!-- please note the wasm file name is calculator.wasm -->
WebAssembly.instantiateStreaming(fetch('calculator.wasm'), go.importObject).then((result) => {
go.run(result.instance);
});
</script>
</head>
<body>
<h1>Calculator</h1>
<input type="text" id="input" />
<button id="calculateSquare">Calculate Square</button>
<button id="calculateCube">Calculate Cube</button>
</body>

</html>

Then, let’s attach “event listeners” to our two buttons above ☝️. This will call the Go functions whenever the buttons are clicked.

For instance, to calculate the square of the input, attach the “event listener” to the calculateSquarebutton, read the input value, and then call the calculateSquare function passing the input to it.

❗️ Remember: Make sure the data type passed to the Go application is correct, as the Go application will panic if the type do not match the expected type.

<script>
document.getElementById('calculateSquare').addEventListener('click', () => {
console.log('Hello from JS');
const input = parseInt(document.getElementById('input').value);
const result = calculateSquare(input);
alert(`Square of ${input} is ${result}`);
});

</script>

Rinse and repeat for calculating the cube of the input:

<script>
// ...

document.getElementById('calculateCube').addEventListener('click', () => {
console.log('Hello from JS');
const input = parseInt(document.getElementById('input').value);
// this is where the go code is called
const result = calculateCube(input);
alert(`Cube of ${input} is ${result}`);
});
</script>
The GoDev Corner unique separator

Reducing our WASM Final Bundle Size using TinyGo

So far, we have managed to get WASM working as expected using Go’s built-in tools. However, there is a downside…

The emitted WASM code of our small calculator app above is very huge — over 2 MBs, as shown below:

Original code input into TinyGo which turned out to be over 2MBs
Original code over 2MB

Fortunately, there’s an easy solution — TinyGo, a Go compiler. With it, we can likely reduce the final build size by over 70% (shown below) without changing a single line of code:

Updated code using TinyGo
Updated code using TinyGo

Let’s see how it works…

[To follow along, you can install TinyGo on your computer using the instructions here.]

👉 Note: TinyGo can also produce WebAssembly (WASM) code which is very compact in size. You can compile programs for web browsers, as well as for server and edge computing environments that support the WebAssembly System Interface (WASI) family of interfaces.

Using TinyGo

Let’s use the TinyGo version of the glue code instead of the Go code we are using.

💡 Big Idea: Since the emitted WASM code of our app will be completely different from the version emitted when using the Go compiler, it only makes sense that we need a different mechanism to load and run our compiled WASM code.

To get started, head to /targets/wasm_exec.js inside the TINYROOT directory. We want to copy our JS glue code to the same directory we copied our Go glue code (public/wasm_exec.js), and overwrite what’s there:

cp $(tinygo env TINYGOROOT)/targets/wasm_exec.js public/wasm_exec.js

Now it’s time to use TinyGo — we’ll target WASM, and save the output in the same public directory as we did before with the Go compiler.

To do so, use the tinygo build command, as shown below:

tinygo build -o public/calculator.wasm -target wasm ./wasm/main.go

📖 Further Reading: You can read more about using the tinygo build command here

And that’s it!

We can now run the Go server we had created earlier using the go run server/main.go command and then visiting the URL http://localhost:3000 to view the results; no other changes are needed.

The GoDev Corner unique separator

Conclusion

In this article, we learned what Web Assembly is and, in the big picture, why it matters for writing applications for the web browser.

We learned how to cross-compile Go applications and run Go code in WASM. Then we ensured our code was fully interactive with the browser by exposing the relative Go functions and calling them using Javascript.

Finally, we used TinyGo to reduce the WASM build size by over 70%, allowing for enhanced speed and performance.

Key Takeaways

  • Web Assembly (WASM) allows you to write web apps that are powerful and performant, opening the door for the kind of apps that can be built for the Web.
  • With WASM, you can write high-performance applications in Go and run them in the browser, reducing the number of resources used and adding the necessary speed, scalability, and security for future growth
  • Along with Rust, Python, and Blazor, Go is one of the most sought-after languages to use for Web Assembly development

Source Code

You can find the project where all the code snippets from above came from here.

The GoDev Corner unique separator

Other Articles To Consider

Disclosure: In accordance with Medium.com’s rules and guidelines, I publicly acknowledge financial compensation from UniDoc for this article. All thoughts, opinions, code, pictures, writing, etc. are my own.

--

--