DEV Community

Aaron Powell
Aaron Powell

Posted on • Originally published at aaron-powell.com on

Learning Golang through WebAssembly - Go, WASM, TypeScript and React

Building an Application

This is actually the final article in a series I wrote on my blog that goes through a complete teardown of Go's approach to WebAssembly. If you want to see the whole series it starts with this post.

So far we've looked at a lot of little pieces which would eventually make an application and it's time to tackle that, it's time to build a web application.

I've decided that for this application we're going to piece together some other tools that you might commonly use, we'll use React as a UI library and TypeScript as a compile-to-JavaScript language. But there's no reason you couldn't replace React with Vue, Angular or any other UI library, and drop TypeScript for 'plain old JavaScript'. You'll find the demo app on my GitHub.

Setting up our Application

To get started we'll use create-react-app with TypeScript, I won't go over doing that setup, the React documentation does a good job for me. You don't have to use create-react-app, it's just a really easy way to bootstrap, but if you're confident without it, by all means skip this step.

Once you're created an application though we'll need to eject create-react-app because we need to be able to modify the webpack.config.js file, which can only be done if you eject create-react-app.

Getting all WASM-y

We'll start by adding the loader created in the last post using npm or yarn:

npm install --save-dev golang-wasm-async-loader
# or
yarn add golang-wasm-async-loader
Enter fullscreen mode Exit fullscreen mode

Then editing the configs/webpack.config.js file to add our loader (follow the instructions in the file for where to put it):

{
    test: /\.go$/,
    loader: 'golang-wasm-async-loader'
},
Enter fullscreen mode Exit fullscreen mode

Adding our WASM

I'm going to make a little application that shows at least 2 number input fields and adds all the values together to get a sum, to Go code for it will look like this:

package main

import (
    "strconv"
    "syscall/js"

    "github.com/aaronpowell/webpack-golang-wasm-async-loader/gobridge"
)

func add(i ...js.Value) js.Value {
    ret := 0

    for _, item := range i {
        val, _ := strconv.Atoi(item.String())
        ret += val
    }

    return js.ValueOf(ret)
}

func main() {
    c := make(chan struct{}, 0)
    println("Web Assembly is ready")
    gobridge.RegisterCallback("add", add)
    <-c
}
Enter fullscreen mode Exit fullscreen mode

Pretty basic, we use range to go over the spread of js.Value, convert each one from a string to a number, sum them up and return boxed in js.Value.

Next up in our input field, I've created a file NumberInput.tsx for that:

import * as React from 'react';

export interface NumberInputProps {
    value: number
    onChange: (value: number) => void
}

const NumberInput : React.SFC<NumberInputProps> = ({ value, onChange }) => (
    <input type="number" value={value} onChange={(e) => onChange(parseInt(e.target.value, 10))} />
);

export default NumberInput;
Enter fullscreen mode Exit fullscreen mode

It's a stateless component that receives two properties, a value for the input field and the callback to execute on change of the input field.

Lastly we'll make our <App />:

import * as React from 'react';
import wasm from './main.go';
import NumberInput from './NumberInput';

const { add } = wasm;

interface State {
    value: number[]
    result: string
}

class App extends React.Component<{}, State> {
    constructor(props: {}) {
        super(props);

        this.state = {
            value: [0, 0],
            result: '0'
        };
    }

    async updateValue(index: number, value: number) {
        //todo
    }

    render() {
        return (
            <div>
                <p>Enter a number in the box below, on change it will add all the numbers together. Click the button to add more input boxes.</p>
                {this.state.value.map((value, index) =>
                    <NumberInput key={index} value={value} onChange={i => this.updateValue(index, i)} />
                )}
                <button type="button" onClick={() => this.setState({ value: [...this.state.value, 0]})}>More inputs!</button>
                <p>Value now is {this.state.result}</p>
            </div>
        );
    }
  }

export default App;
Enter fullscreen mode Exit fullscreen mode

Ok, pretty basic, it's component with state (sorry, no redux or hooks here 😝) where state contains an array of input values and the current sum. The render will loop over the input values, create our <NumberInput /> component with the value and give it a function that will call updateValue when done. State it initialised to have 2 inputs, but you can add more with a button shown on screen.

At the top of the file you'll see that we're importing the main.go file from above and using destructing assignment to get out the add function, or more accurately, a reference to it from the Proxy the loader creates for us.

Now it's time to complete our updateValue method. But it turns out that using the add function could be a bit tricky. Sure we can define it as an any property of the WASM, but what if we wanted to be more intelligent in the way it is represented?

    async updateValue(index: number, value: number) {
        let newValues = this.state.value.slice();
        newValues[index] = value
        let result = await add<number, string>(...newValues);
        this.setState({ value: newValues, result });
    }
Enter fullscreen mode Exit fullscreen mode

Using Types with our Proxy

How do we make sure that TypeScript knows what type our arguments are that are to be passed into a function that, well, doesn't exist? Ultimately we want to get away from an any, instead we want to use TypeScript generics!

We can do this in one of two ways, the first is we just create a definition file that creates an explicit interface for our WASM import:

declare module "*.go" {
    interface GoWrapper {
        add: (...params: number[]) => Promise<string>
    }

    var _: GoWrapper
    export default _
}
Enter fullscreen mode Exit fullscreen mode

I've created a file called definitions.d.ts that sits alongside the App.tsx file, and by declaring the module for *.go it means that this declaration file works for any imports of Go files. We can also drop the generic arguments, which is nice, but it is a problem it we want to start adding more Go functions, we keep having to edit this file to include them.

So how about going crazy with generic!

declare module "*.go" {
    interface GoWrapper {
        [K: string]: <T = any, R = any>(...params: T[]) => Promise<R>
    }

    var _: GoWrapper
    export default _
}
Enter fullscreen mode Exit fullscreen mode

Now, stick with me as we break it down:

  • We're saying we have keys of the type (GoWrapper) that are strings with [K: string]
  • Each key has a type that takes two generic arguments, an input and an output, that's <T = any, R = any>
  • These go into a function with T being a params array, denoted by (...params: T[])
  • The return type is a Promise using the specified return type, Promise<R>

So when we do add<number, string> it says that were passing in an indeterminate number of arguments that are all numbers and it'll return a string asynchronously.

This forced type flow down from our state and back, all through the magic of TypeScript types!

If you were working with mixed types in the arguments to the function we could do something like:

let result = await something<string | number, string>("hello", 1, "world");
Enter fullscreen mode Exit fullscreen mode

Using the | tells TypeScript that the arguments into the function are a string or number type, but not function, boolean, etc.. Pretty crazy right!

Deploying our Application

We're done! It works locally! Now it's time to deploy it somewhere.

I'm going to use Azure DevOps Pipelines to build and then deploy it as an Azure Blob Static Website.

Building

To build you'll need to run the following steps:

  • Install our Go dependencies
  • Install our npm packages
  • Run webpack
  • Copy the required files as a build artifact

I've created an Azure DevOps YAML build that is in the GitHub repo. It's modeled on the standard Node.js pipeline but I've added the specific Go steps.

The things of note are that you'll need to install the appropriate Go packages with go get. To use the gobridge I created for the loader you'll need to set the GOOS and GOARCH too:

- script: |
    GOOS=js GOARCH=wasm go get "github.com/aaronpowell/webpack-golang-wasm-async-loader/gobridge"
  displayName: 'install gobridge'
Enter fullscreen mode Exit fullscreen mode

You'll also need to make sure that GOPATH and GOROOT are environment variables available to the loader. By default these aren't set as environment variables in the agent, I just did it inline:

- script: |
    npm install
    GOPATH=$(go env GOPATH) GOROOT=$(go env GOROOT) npm run build
  displayName: 'npm install, run webpack'
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can create them for all tasks:

variables:
  GOBIN:  '$(GOPATH)/bin' # Go binaries path
  GOROOT: '/usr/local/go1.11' # Go installation path
  GOPATH: '$(system.defaultWorkingDirectory)/gopath' # Go workspace path
Enter fullscreen mode Exit fullscreen mode

Here's a completed build! (ignore all the failed ones before it πŸ˜†)

Release

At the time of writing we don't have support for releases in the YAML file for Azure DevOps Pipelines. I use the Azure File Copy task to copy all the files into the storage account I'm running in, followed by the Azure CLI task to set the WASM content type on the WASM file, otherwise it won't be served correctly:

az storage blob update --container-name "$web" --name "hello.wasm" --content-type "application/wasm" --account-name gowasm
Enter fullscreen mode Exit fullscreen mode

Remember to change hello.wasm to whatever your filename is! πŸ˜‰

Here's a completed release!

Conclusion

And we are done folks! Starting with no idea what WebAssembly is or how to write Go we've gone through a bunch of exploration into how it all works, what makes Go's approach to WebAssembly a little tricky as a web developer and ultimately how we can introduce Go into the tool chain that we are familiar with these days building web applications.

If you build anything exciting with Go and WASM please let me know!

Top comments (0)