DEV Community

Lane Wagner
Lane Wagner

Posted on • Originally published at qvault.io on

Running Go in the Browser With Web Assembly (WASM)

golang wasm gopher

The post Running Go in the Browser With Web Assembly (WASM) appeared first on Qvault.

If you are familiar with the Go Playground, then you know how convenient it is to be able to have a Go scratchpad in the browser. Want to show someone a code snippet? Want to quickly test some syntax? Browser-based code pads a helpful. On that note, I created a new playground. The cool thing about this new playground that it doesn’t use a remote server to run code, just to compile it. The code runs in your browser using web assembly (WASM).

Try out the Qvault Playground here: https://classroom.qvault.io/#/playground/go

How Does It Work?

When a user clicks “run”, the code (as text) is sent back to our servers. The server is written in Go. As such the handler for the API looks something like this:

func compileCodeHandler(w http.ResponseWriter, r *http.Request) {
    defer r.Body.Close()

    // Get code from params
    type parameters struct {
        Code string
    }
    decoder := json.NewDecoder(r.Body)
    params := parameters{}
    err := decoder.Decode(&params)
    if err != nil {
        respondWithError(w, 500, "Couldn't decode parameters")
        return
    }

    // create file system location for compilation path
    usr, err := user.Current()
    if err != nil {
        respondWithError(w, 500, "Couldn't get system user")
        return
    }
    workingDir := filepath.Join(usr.HomeDir, ".wasm", uuid.New().String())
    err = os.MkdirAll(workingDir, os.ModePerm)
    if err != nil {
        respondWithError(w, 500, "Couldn't create directory for compilation")
        return
    }
    defer func() {
        err = os.RemoveAll(workingDir)
        if err != nil {
            respondWithError(w, 500, "Couldn't clean up code from compilation")
            return
        }
    }()
    f, err := os.Create(filepath.Join(workingDir, "main.go"))
    if err != nil {
        respondWithError(w, 500, "Couldn't create code file for compilation")
        return
    }
    defer f.Close()
    dat := []byte(params.Code)
    _, err = f.Write(dat)
    if err != nil {
        respondWithError(w, 500, "Couldn't write code to file for compilation")
        return
    }

    // compile the wasm
    const outputBinary = "main.wasm"
    os.Setenv("GOOS", "js")
    os.Setenv("GOARCH", "wasm")
    cmd := exec.Command("go", "build", "-o", outputBinary)
    cmd.Dir = workingDir
    stderr, err := cmd.StderrPipe()
    if err != nil {
        respondWithError(w, 500, err.Error())
        return
    }
    if err := cmd.Start(); err != nil {
        respondWithError(w, 500, err.Error())
        return
    }
    stdErr, err := ioutil.ReadAll(stderr)
    if err != nil {
        respondWithError(w, 500, err.Error())
        return
    }
    stdErrString := string(stdErr)
    if stdErrString != "" {
        parts := strings.Split(stdErrString, workingDir)
        if len(parts) < 2 {
            respondWithError(w, 500, stdErrString)
            return
        }
        respondWithError(w, 400, parts[1])
        return
    }
    if err := cmd.Wait(); err != nil {
        respondWithError(w, 500, err.Error())
        return
    }

    // write wasm binary to response
    dat, err = ioutil.ReadFile(filepath.Join(workingDir, outputBinary))
    if err != nil {
        respondWithError(w, 500, err.Error())
        return
    }
    w.Write(dat)
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the handler simply takes code as input and responds with a slice of WASM bytes.

What About the Front-End?

The front end is quite simple. First, we need to include the official Go WASM executor in our page. Assuming you have a go installation on your machine, this JavaScript file can be found at:

$(go env GOROOT)/misc/wasm/wasm_exec.js
Enter fullscreen mode Exit fullscreen mode

Then include the script in the body of your html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Qvault Classroom - Learn Coding</title>
  </head>
  <body>
    <script src="wasm_exec.js"></script>
  </body>
</html>

Enter fullscreen mode Exit fullscreen mode

Because Qvault Classroom’s front-end is written as a Vue.js single page app, I’ve created a small es6 module that runs a WASM byte array and returns the output as an array of lines:

const go = new window.Go();

export default async function runGoWasm(rawData) {
  const result = await WebAssembly.instantiate(rawData, go.importObject);
  let oldLog = console.log;
  let stdOut = [];
  console.log = (line) => {stdOut.push(line);};
  await go.run(result.instance);
  console.log = oldLog;
  return stdOut;
}
Enter fullscreen mode Exit fullscreen mode

That’s it! Running Go in the browser is pretty easy 🙂

Thanks For Reading

Hit me up on twitter @wagslane if you have any questions or comments.

Take your coding career to the next level with courses on Qvault Classroom

Follow me on Dev.to: wagslane

The post Running Go in the Browser With Web Assembly (WASM) appeared first on Qvault.

Discussion (2)

Collapse
redeving profile image
Kelvin Thompson • Edited on

That's very cool! It would be great if it would act like

dev.to/chr15m/i-built-an-online-co...

A single file that could do with Go what his does with JS, with hot reload. Wish I was good enough to do that or help!

Thanks man, excellent!
Cheers!

Collapse
delta456 profile image
Swastik Baranwal

I think you should include all import modules.