Golang to WASM: Basic Setup and Handling HTTP Requests

Introduction

WebAssembly is a technology to make code execution of languages like C, C++, Go, Rust, etc in the browser. This is achieved by creating a binary web assembly file.

This helps our programs to reach native-level performance.

We have an internal collaborative API client Lama2, a CLI tool implemented using go. We need an interface for it, so I decided to implement a small wasm (Web assembly) interface so that we can get a basic web interface. This is a small step towards a full-fledged UI for the future. This article will follow through with the basic hello world and setup http requests in webassembly.

Compatibility check

Go offers robust support for WebAssembly (WASM) compilation, making it a viable choice for web-based projects. When converting Go code to a WASM library, it's essential to ensure that the original code is compatible with the browser environment. Some Go functionalities, like direct file system access, won't work in the browser context. However, with the provided wasm_exec.js bridge and careful code structuring, Go can seamlessly integrate with JavaScript, offering a performant and compact WASM output.

How to setup a basic hello world in wasm

Setting up an infinitely running main file In the main file, make sure to add a select{} line at the end. This will keep the program running continuously. The select{} statement in Go is typically used to wait on multiple channel operations. But when it's empty, like select{}, it simply blocks the function from finishing. This is a handy trick to keep our program running, especially since we want the interface in the browser to stay active and not close prematurely.

package main

import (
   "fmt"
)
func main() {
   fmt.Println(Hello world)
   select {}
}

Register the js function

Next, we need to create functions that are callable from the javascript from the browser. syscall/js provides functions to interact with JavaScript from Go code when running in a WebAssembly context

package main

import (
   "syscall/js"
)
func main() {
   js.Global().Set("helloWasm", js.FuncOf(hello))
   select {}

}

func hello(this js.Value, p []js.Value) interface{} {
   return js.ValueOf("Hello from Go WASM!")
}

js.Value: This represents a JavaScript value in Go. So, when you see this js.Value or p []js.Value, it means the function is receiving JavaScript values as input.

js.ValueOf(...): This is a way to convert a Go value into a JavaScript value. In the function, it's taking the string "Hello from Go WASM!" and turning it into something JavaScript understands.

Setting up HTML and js files

Copy the js glue code wasm_exec.js into your repo directory. 

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

In essence, wasm_exec.js is like a translator that helps Go WebAssembly code understand and interact with the browser's environment. If you're using Go to compile to WebAssembly, you'll typically include this file in your HTML to ensure your WASM code runs smoothly in the browser.

Create html page with wasm code integration

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>HELLO WASM</title>
   <style>
       html {
          font-family: Arial, serif;
       }
   </style>
</head>
<body>
<script src="wasm_exec.js"></script>
<script>
       let wasmLoaded = false;
       const go = new Go();
       WebAssembly.instantiateStreaming(fetch("main.wasm"), go.imprtObject).then((result) => {
           go.run(result.instance);
           wasmLoaded = true; 
       });
</script>
<div>

   <label for"inputField">Enter value</label>
   <input id="inputField" ame="Request" type="text">
   <div id="outputHash" tyle="font-size: 20px"></div>
</div>
</body>

</html>

Build the wasm binary

Run this command to create a binary out of your main.go

GOOS=js GOARCH=wasm go build -o main.wasm  

GOOS=js: This sets the target operating system to JavaScript. It tells the Go compiler that the code should be prepared to run in a JavaScript environment.

GOARCH=wasm: This sets target architecture to WebAssembly . It's instructing the Go compiler to produce output in the WebAssembly format.

Create a go Webserver

package main

import (
   "log"
   "net/http"
   "time"
)

func logger(next http.Handler) http.Handler {
   return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
       startTime := time.Now()
       next.ServeHTTP(w, r)
       log.Printf("%s %s %s %v", r.RemoteAddr, r.Method, r.URL, time.Since(startTime))
   })
}

func main() {
   fs := http.FileServer(http.Dir("./static"))
   http.Handle("/", logger(fs))
   log.Println("Listening on http://localhost:3000/index.html")
   err := http.ListenAndServe(":3000", nil)
   if err != nil {
       log.Fatal(err)
   }
}

Run this server using this command :

go run webserver/main.go 

Call the function from the console

HTTP requests in Golang

If you have some HTTP requests in your go code, after compiling to webassembly if you try making a web request it will give a time-out error.

Network Calls in Go: When Go makes a network call, it waits (or "blocks") until it gets a response. To avoid this, we run these calls in a Goroutine, which is like doing the task in the background. When we use this with JavaScript, we give JavaScript a Promise that says, "I'll get back to you with the answer when I have it."

Intercepting Network Requests: If you want your Go code to catch and handle network requests, then when it sends the response back to JavaScript, it should be in a format JavaScript understands. This format is called a JavaScript Response object. Think of it as packaging the answer in a way that JavaScript likes.

func GoWebRequestFunc() js.Func {
   return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
       // Get the URL as argument
       requestUrl := args[0].String()
       // return a Promise because HTTP requests are blocking in Go
       handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
           resolve := args[0]
           reject := args[1]
           go func() {
               // The HTTP request
               res, err := http.DefaultClient.Get(requestUrl)
               if err != nil {
                   errorConstructor := js.Global().Get("Error")
                   errorObject := errorConstructor.New(err.Error())
                   reject.Invoke(errorObject)
                   return
               }
               defer res.Body.Close()


               // Read the response body
               data, err := ioutil.ReadAll(res.Body)
               if err != nil {
                   // Handle errors here too
                   errorConstructor := js.Global().Get("Error")
                   errorObject := errorConstructor.New(err.Error())
                   reject.Invoke(errorObject)
                   return
               }
               arrayConstructor := js.Global().Get("Uint8Array")
               dataJS := arrayConstructor.New(len(data))
               js.CopyBytesToJS(dataJS, data)
               responseConstructor := js.Global().Get("Response")
               response := responseConstructor.New(dataJS)
               resolve.Invoke(response)
           }()
           return nil
       })
       promiseConstructor := js.Global().Get("Promise")
       return promiseConstructor.New(handler)
   })
}
Register this function into the golang main function, and then call it from js

       async function MyFunc() {
         try {
    const response = await     GoWebRequestFunc("https://api.quotable.io/quotes/random")
           const message = await response.json()
           console.log(message)
           } catch (err) {
               console.error('Caught exception', err)
           }
       }     

This will return the API response.

This article provides a basic introduction to Golang WASM. I hope you find it both enlightening and valuable.