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.