In the evolving landscape of web development, performance has become a critical differentiator. Users expect desktop-like responsiveness from web applications, and developers are constantly seeking technologies that can deliver this experience. WebAssembly (WASM) represents a paradigm shift in web performance, offering near-native execution speeds within the browser’s sandbox. When combined with Go’s efficiency and developer-friendly features, WebAssembly creates a powerful platform for building high-performance web applications that were previously unattainable with JavaScript alone.
Go’s support for WebAssembly compilation has matured significantly, enabling developers to leverage Go’s strengths—strong typing, efficient concurrency, and excellent performance characteristics—in browser environments. This capability opens new possibilities for web applications that require intensive computation, complex data processing, or high-performance graphics, all while maintaining Go’s clean syntax and robust error handling.
This comprehensive guide explores the intersection of Go and WebAssembly, providing a deep dive into compilation processes, application architecture, JavaScript interoperability, and optimization techniques. Whether you’re building data visualization tools, browser-based games, or computation-heavy web applications, understanding how to effectively use Go with WebAssembly will expand your toolkit for delivering exceptional web experiences.
Understanding Go WebAssembly Compilation
WebAssembly represents a fundamental shift in how code executes in browsers. Unlike JavaScript, which is interpreted or JIT-compiled at runtime, WebAssembly is a binary instruction format designed as a compilation target for languages like Go. Understanding this compilation process is essential for effective WASM development.
The WebAssembly Compilation Pipeline
Go’s WebAssembly support involves a sophisticated compilation pipeline that transforms Go code into WASM binaries:
package main
import (
"fmt"
)
// Simple function to demonstrate compilation
func add(a, b int) int {
return a + b
}
func main() {
result := add(5, 7)
fmt.Println("5 + 7 =", result)
}
To compile this Go code to WebAssembly:
GOOS=js GOARCH=wasm go build -o main.wasm main.go
This command sets the target operating system to “js” and architecture to “wasm”, instructing the Go compiler to produce WebAssembly output instead of native machine code.
WebAssembly Module Structure
A compiled WebAssembly module has a specific binary structure that’s important to understand:
package main
import (
"fmt"
"reflect"
"unsafe"
)
// WasmModuleHeader represents a simplified view of a WASM module header
type WasmModuleHeader struct {
MagicNumber uint32 // Always 0x6d736100 (ASCII for "\0asm")
Version uint32 // Current version is 1
// Followed by sections...
}
// WasmSection represents a section in a WASM module
type WasmSection struct {
ID byte
Size uint32
Content []byte
}
// SectionNames maps section IDs to their names
var SectionNames = map[byte]string{
0: "Custom",
1: "Type",
2: "Import",
3: "Function",
4: "Table",
5: "Memory",
6: "Global",
7: "Export",
8: "Start",
9: "Element",
10: "Code",
11: "Data",
}
// Simplified demonstration of WASM structure analysis
func analyzeWasmStructure(wasmBytes []byte) {
// Check magic number and version
if len(wasmBytes) < 8 {
fmt.Println("Invalid WASM binary: too short")
return
}
header := (*WasmModuleHeader)(unsafe.Pointer(&wasmBytes[0]))
fmt.Printf("Magic number: 0x%x\n", header.MagicNumber)
fmt.Printf("Version: %d\n", header.Version)
// In a real implementation, we would parse the sections here
fmt.Println("WASM binary contains these sections:")
// This is simplified - actual parsing would involve proper decoding
offset := 8 // Skip header
for offset < len(wasmBytes) {
if offset+1 >= len(wasmBytes) {
break
}
sectionID := wasmBytes[offset]
offset++
// Read section size (simplified - real implementation would use LEB128)
size := uint32(wasmBytes[offset])
offset++
sectionName := "Unknown"
if name, ok := SectionNames[sectionID]; ok {
sectionName = name
}
fmt.Printf("- Section %d (%s): %d bytes\n", sectionID, sectionName, size)
// Skip section content
offset += int(size)
}
}
// Note: This is a simplified demonstration and wouldn't work on actual WASM binaries
// without proper LEB128 decoding and section parsing
Go’s Runtime in WebAssembly
When compiling Go to WebAssembly, the Go runtime is included in the binary, which has important implications:
package main
import (
"fmt"
"runtime"
"unsafe"
)
func main() {
// Display Go runtime information in WASM environment
fmt.Println("Go Version:", runtime.Version())
fmt.Println("GOOS:", runtime.GOOS)
fmt.Println("GOARCH:", runtime.GOARCH)
fmt.Println("NumCPU:", runtime.NumCPU())
fmt.Println("Compiler:", runtime.Compiler)
// Memory model information
var x int
fmt.Printf("Size of int: %d bytes\n", unsafe.Sizeof(x))
// Garbage collection still works in WASM
memStats := runtime.MemStats{}
runtime.ReadMemStats(&memStats)
fmt.Printf("Allocated memory: %d bytes\n", memStats.Alloc)
fmt.Printf("Total memory allocated: %d bytes\n", memStats.TotalAlloc)
fmt.Printf("System memory: %d bytes\n", memStats.Sys)
fmt.Printf("Number of GC cycles: %d\n", memStats.NumGC)
}
When executed in a browser, this code reveals that the Go runtime is fully functional within the WebAssembly environment, including garbage collection, which is a significant advantage over other languages that compile to WebAssembly.
Size Optimization Techniques
One challenge with Go WebAssembly is the size of the resulting binaries. Here are techniques to reduce binary size:
package main
import (
"fmt"
)
// TinyGoExample demonstrates code that would be ideal for TinyGo compilation
// TinyGo produces much smaller WASM binaries than standard Go
func TinyGoExample() {
fmt.Println("Hello from TinyGo!")
}
func main() {
TinyGoExample()
// Size optimization techniques:
// 1. Use TinyGo for smaller binaries:
// tinygo build -o main.wasm -target wasm main.go
// 2. Use build tags to exclude unnecessary packages
// 3. Use the -ldflags="-s -w" option to strip debug information:
// GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o main.wasm main.go
// 4. Use wasm-opt from the Binaryen toolkit for post-compilation optimization:
// wasm-opt -Oz main.wasm -o optimized.wasm
fmt.Println("Size optimization techniques for Go WASM:")
fmt.Println("1. Standard Go WASM binary: ~2-5MB")
fmt.Println("2. With stripped debug symbols: ~1-3MB")
fmt.Println("3. TinyGo WASM binary: ~80-600KB")
fmt.Println("4. TinyGo + wasm-opt: ~70-500KB")
}
TinyGo is particularly valuable for WebAssembly development as it produces significantly smaller binaries, though with some feature limitations compared to standard Go.
Building Your First Go WASM Application
Let’s walk through the process of creating a complete Go WebAssembly application, from compilation to browser integration.
Setting Up the Development Environment
First, we need to set up our development environment:
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
)
// setupWasmDevelopment demonstrates the setup process for Go WASM development
func setupWasmDevelopment() {
// Step 1: Verify Go installation
cmd := exec.Command("go", "version")
output, err := cmd.Output()
if err != nil {
fmt.Println("Error: Go is not installed or not in PATH")
return
}
fmt.Printf("Using %s\n", output)
// Step 2: Create project structure
projectStructure := `
project/
├── main.go # Go source code
├── index.html # HTML entry point
├── wasm_exec.js # Go's WebAssembly support JS
└── server.go # Simple HTTP server for testing
`
fmt.Println("Create project structure:")
fmt.Println(projectStructure)
// Step 3: Copy wasm_exec.js from Go installation
goRoot := os.Getenv("GOROOT")
if goRoot == "" {
fmt.Println("GOROOT environment variable not set")
return
}
wasmExecPath := filepath.Join(goRoot, "misc", "wasm", "wasm_exec.js")
fmt.Printf("Copy %s to your project directory\n", wasmExecPath)
// Step 4: Create a simple HTTP server for testing
serverCode := `
package main
import (
"flag"
"log"
"net/http"
)
func main() {
port := flag.String("port", "8080", "Port to serve on")
dir := flag.String("dir", ".", "Directory to serve")
flag.Parse()
fs := http.FileServer(http.Dir(*dir))
http.Handle("/", fs)
log.Printf("Serving %s on HTTP port: %s\n", *dir, *port)
log.Fatal(http.ListenAndServe(":"+*port, nil))
}
`
fmt.Println("Create server.go with this content:")
fmt.Println(serverCode)
// Step 5: Compilation command
fmt.Println("\nCompile your Go code to WebAssembly:")
fmt.Println("GOOS=js GOARCH=wasm go build -o main.wasm main.go")
// Step 6: Run the server
fmt.Println("\nRun the server:")
fmt.Println("go run server.go")
fmt.Println("\nAccess your application at http://localhost:8080")
}
func main() {
setupWasmDevelopment()
}
Creating a Basic WASM Application
Now, let’s create a simple Go WebAssembly application:
// main.go
package main
import (
"fmt"
"syscall/js"
)
// Function to be exported to JavaScript
func add(this js.Value, args []js.Value) interface{} {
if len(args) != 2 {
return "Error: Expected 2 arguments"
}
// Convert JS values to Go types
a := args[0].Int()
b := args[1].Int()
// Perform calculation
result := a + b
// Return result to JavaScript
return result
}
// Function to modify the DOM
func updateDOM(this js.Value, args []js.Value) interface{} {
// Get the document object
document := js.Global().Get("document")
// Create a new paragraph element
p := document.Call("createElement", "p")
// Set its text content
p.Set("textContent", "This paragraph was created by Go WebAssembly!")
// Append it to the body
document.Get("body").Call("appendChild", p)
return nil
}
func main() {
fmt.Println("Go WebAssembly Initialized")
// Create a channel to keep the program running
c := make(chan struct{}, 0)
// Register functions to be called from JavaScript
js.Global().Set("goAdd", js.FuncOf(add))
js.Global().Set("goUpdateDOM", js.FuncOf(updateDOM))
// Keep the program running
<-c
}
HTML and JavaScript Integration
Here’s how to integrate the WebAssembly module with HTML and JavaScript:
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Go WebAssembly Example</title>
<script src="wasm_exec.js"></script>
<script>
// Initialize Go WASM runtime
const go = new Go();
// Load and instantiate the WebAssembly module
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject)
.then((result) => {
// Start the Go WASM instance
go.run(result.instance);
// Now we can call Go functions
console.log("2 + 3 =", goAdd(2, 3));
// Add a button to call Go function
const button = document.createElement("button");
button.textContent = "Call Go Function";
button.addEventListener("click", () => {
goUpdateDOM();
});
document.body.appendChild(button);
})
.catch(err => {
console.error("Failed to load WASM:", err);
});
</script>
</head>
<body>
<h1>Go WebAssembly Example</h1>
<p>Open the browser console to see output from Go</p>
</body>
</html>
Complete Application Example
Let’s build a more practical example—a real-time data processing application:
// data_processor.go
package main
import (
"fmt"
"math"
"syscall/js"
)
// DataPoint represents a single data point
type DataPoint struct {
X float64
Y float64
}
// DataProcessor handles data processing operations
type DataProcessor struct {
data []DataPoint
}
// NewDataProcessor creates a new data processor
func NewDataProcessor() *DataProcessor {
return &DataProcessor{
data: make([]DataPoint, 0),
}
}
// AddPoint adds a data point
func (dp *DataProcessor) AddPoint(x, y float64) {
dp.data = append(dp.data, DataPoint{X: x, Y: y})
}
// CalculateStatistics computes basic statistics on the data
func (dp *DataProcessor) CalculateStatistics() (min, max, avg, stdDev float64) {
if len(dp.data) == 0 {
return 0, 0, 0, 0
}
// Calculate min, max, and sum
min = dp.data[0].Y
max = dp.data[0].Y
sum := 0.0
for _, point := range dp.data {
if point.Y < min {
min = point.Y
}
if point.Y > max {
max = point.Y
}
sum += point.Y
}
// Calculate average
avg = sum / float64(len(dp.data))
// Calculate standard deviation
sumSquaredDiff := 0.0
for _, point := range dp.data {
diff := point.Y - avg
sumSquaredDiff += diff * diff
}
stdDev = math.Sqrt(sumSquaredDiff / float64(len(dp.data)))
return min, max, avg, stdDev
}
// ApplyTransformation applies a mathematical transformation to all data points
func (dp *DataProcessor) ApplyTransformation(transformFunc func(float64) float64) {
for i := range dp.data {
dp.data[i].Y = transformFunc(dp.data[i].Y)
}
}
// GetDataAsJSArray returns the data as a JavaScript array
func (dp *DataProcessor) GetDataAsJSArray() js.Value {
// Create a JavaScript array
jsArray := js.Global().Get("Array").New(len(dp.data))
// Populate the array with data points
for i, point := range dp.data {
jsPoint := js.Global().Get("Object").New()
jsPoint.Set("x", point.X)
jsPoint.Set("y", point.Y)
jsArray.SetIndex(i, jsPoint)
}
return jsArray
}
// JavaScript wrapper functions
func jsAddPoint(this js.Value, args []js.Value) interface{} {
if len(args) != 2 {
return "Error: Expected 2 arguments (x, y)"
}
x := args[0].Float()
y := args[1].Float()
processor.AddPoint(x, y)
return nil
}
func jsCalculateStatistics(this js.Value, args []js.Value) interface{} {
min, max, avg, stdDev := processor.CalculateStatistics()
result := js.Global().Get("Object").New()
result.Set("min", min)
result.Set("max", max)
result.Set("average", avg)
result.Set("standardDeviation", stdDev)
return result
}
func jsApplyTransformation(this js.Value, args []js.Value) interface{} {
if len(args) != 1 || !args[0].InstanceOf(js.Global().Get("Function")) {
return "Error: Expected a function argument"
}
jsTransformFunc := args[0]
// Create a Go function that calls the JavaScript function
transformFunc := func(value float64) float64 {
result := jsTransformFunc.Invoke(value)
return result.Float()
}
processor.ApplyTransformation(transformFunc)
return nil
}
func jsGetData(this js.Value, args []js.Value) interface{} {
return processor.GetDataAsJSArray()
}
// Global processor instance
var processor *DataProcessor
func main() {
fmt.Println("Data Processor WebAssembly Module Initialized")
// Initialize the processor
processor = NewDataProcessor()
// Register JavaScript functions
js.Global().Set("addDataPoint", js.FuncOf(jsAddPoint))
js.Global().Set("calculateStatistics", js.FuncOf(jsCalculateStatistics))
js.Global().Set("applyTransformation", js.FuncOf(jsApplyTransformation))
js.Global().Set("getProcessedData", js.FuncOf(jsGetData))
// Keep the program running
<-make(chan struct{})
}
And the corresponding HTML/JavaScript:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Go WASM Data Processor</title>
<script src="wasm_exec.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
.chart-container { height: 400px; }
.controls { margin: 20px 0; }
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin: 20px 0; }
.stat-box { border: 1px solid #ddd; padding: 10px; border-radius: 4px; }
.stat-value { font-size: 1.5em; font-weight: bold; }
</style>
<script>
let chart;
// Initialize Go WASM
const go = new Go();
WebAssembly.instantiateStreaming(fetch("data_processor.wasm"), go.importObject)
.then((result) => {
go.run(result.instance);
initializeApp();
});
function initializeApp() {
// Create chart
const ctx = document.getElementById('dataChart').getContext('2d');
chart = new Chart(ctx, {
type: 'line',
data: {
datasets: [{
label: 'Data Points',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
data: []
}]
},
options: {
scales: {
x: { type: 'linear', position: 'bottom' }
}
}
});
// Add event listeners
document.getElementById('addRandomData').addEventListener('click', addRandomData);
document.getElementById('calculateStats').addEventListener('click', updateStatistics);
document.getElementById('applySquareRoot').addEventListener('click', () => applyTransformation(Math.sqrt));
document.getElementById('applySquare').addEventListener('click', () => applyTransformation(x => x * x));
}
function addRandomData() {
const count = parseInt(document.getElementById('dataCount').value);
for (let i = 0; i < count; i++) {
const x = Math.random() * 100;
const y = Math.random() * 100;
addDataPoint(x, y);
}
updateChart();
updateStatistics();
}
function addDataPoint(x, y) {
// Call Go function to add data point
addDataPoint(x, y);
}
function updateStatistics() {
// Call Go function to calculate statistics
const stats = calculateStatistics();
document.getElementById('minValue').textContent = stats.min.toFixed(2);
document.getElementById('maxValue').textContent = stats.max.toFixed(2);
document.getElementById('avgValue').textContent = stats.average.toFixed(2);
document.getElementById('stdDevValue').textContent = stats.standardDeviation.toFixed(2);
}
function applyTransformation(func) {
// Call Go function to apply transformation
applyTransformation(func);
updateChart();
updateStatistics();
}
function updateChart() {
// Get data from Go
const data = getProcessedData();
// Update chart
chart.data.datasets[0].data = data.map(point => ({
x: point.x,
y: point.y
}));
chart.update();
}
</script>
</head>
<body>
<h1>Go WebAssembly Data Processor</h1>
<div class="chart-container">
<canvas id="dataChart"></canvas>
</div>
<div class="controls">
<label for="dataCount">Number of random data points:</label>
<input type="number" id="dataCount" value="50" min="1" max="1000">
<button id="addRandomData">Add Random Data</button>
<button id="calculateStats">Calculate Statistics</button>
<button id="applySquareRoot">Apply Square Root</button>
<button id="applySquare">Apply Square</button>
</div>
<div class="stats">
<div class="stat-box">
<div>Minimum</div>
<div id="minValue" class="stat-value">0.00</div>
</div>
<div class="stat-box">
<div>Maximum</div>
<div id="maxValue" class="stat-value">0.00</div>
</div>
<div class="stat-box">
<div>Average</div>
<div id="avgValue" class="stat-value">0.00</div>
</div>
<div class="stat-box">
<div>Std Deviation</div>
<div id="stdDevValue" class="stat-value">0.00</div>
</div>
</div>
</body>
</html>
This example demonstrates a practical application of Go WebAssembly for real-time data processing in the browser, showcasing the integration between Go and JavaScript.
Advanced WebAssembly Patterns
As you become more comfortable with basic Go WebAssembly development, you can explore more sophisticated patterns and techniques.
State Management in WASM Applications
Managing state between JavaScript and Go requires careful consideration:
package main
import (
"encoding/json"
"fmt"
"sync"
"syscall/js"
)
// AppState represents the application state
type AppState struct {
mu sync.RWMutex
counter int
items []string
isInitialized bool
userData map[string]interface{}
}
// Global state instance
var state = &AppState{
counter: 0,
items: make([]string, 0),
isInitialized: false,
userData: make(map[string]interface{}),
}
// Initialize the application state
func (s *AppState) Initialize(initialData js.Value) {
s.mu.Lock()
defer s.mu.Unlock()
if initialData.Type() == js.TypeObject {
// Extract counter if present
if counterVal := initialData.Get("counter"); counterVal.Type() == js.TypeNumber {
s.counter = counterVal.Int()
}
// Extract items if present
if itemsVal := initialData.Get("items"); itemsVal.Type() == js.TypeObject && itemsVal.InstanceOf(js.Global().Get("Array")) {
length := itemsVal.Length()
s.items = make([]string, length)
for i := 0; i < length; i++ {
s.items[i] = itemsVal.Index(i).String()
}
}
// Extract userData if present
if userDataVal := initialData.Get("userData"); userDataVal.Type() == js.TypeObject {
// Convert JavaScript object to Go map
keys := js.Global().Get("Object").Call("keys", userDataVal)
length := keys.Length()
for i := 0; i < length; i++ {
key := keys.Index(i).String()
value := userDataVal.Get(key)
// Handle different types
switch value.Type() {
case js.TypeBoolean:
s.userData[key] = value.Bool()
case js.TypeNumber:
s.userData[key] = value.Float()
case js.TypeString:
s.userData[key] = value.String()
default:
// For complex objects, store as JSON string
jsonStr := js.Global().Get("JSON").Call("stringify", value).String()
s.userData[key] = jsonStr
}
}
}
}
s.isInitialized = true
fmt.Println("State initialized:", s)
}
// GetState returns the current state as a JavaScript object
func (s *AppState) GetState() js.Value {
s.mu.RLock()
defer s.mu.RUnlock()
result := js.Global().Get("Object").New()
result.Set("counter", s.counter)
// Convert items to JS array
itemsArray := js.Global().Get("Array").New(len(s.items))
for i, item := range s.items {
itemsArray.SetIndex(i, item)
}
result.Set("items", itemsArray)
// Convert userData to JS object
userData := js.Global().Get("Object").New()
for k, v := range s.userData {
switch val := v.(type) {
case string:
userData.Set(k, val)
case int:
userData.Set(k, val)
case float64:
userData.Set(k, val)
case bool:
userData.Set(k, val)
default:
// Try to convert to JSON string
if jsonBytes, err := json.Marshal(val); err == nil {
userData.Set(k, string(jsonBytes))
} else {
userData.Set(k, fmt.Sprintf("%v", val))
}
}
}
result.Set("userData", userData)
return result
}
// IncrementCounter increases the counter and returns the new value
func (s *AppState) IncrementCounter() int {
s.mu.Lock()
defer s.mu.Unlock()
s.counter++
return s.counter
}
// AddItem adds an item to the items list
func (s *AppState) AddItem(item string) {
s.mu.Lock()
defer s.mu.Unlock()
s.items = append(s.items, item)
}
// SetUserData sets a value in the userData map
func (s *AppState) SetUserData(key string, value interface{}) {
s.mu.Lock()
defer s.mu.Unlock()
s.userData[key] = value
}
// JavaScript wrapper functions
func jsInitializeState(this js.Value, args []js.Value) interface{} {
if len(args) > 0 {
state.Initialize(args[0])
} else {
state.Initialize(js.Null())
}
return nil
}
func jsGetState(this js.Value, args []js.Value) interface{} {
return state.GetState()
}
func jsIncrementCounter(this js.Value, args []js.Value) interface{} {
return state.IncrementCounter()
}
func jsAddItem(this js.Value, args []js.Value) interface{} {
if len(args) > 0 && args[0].Type() == js.TypeString {
state.AddItem(args[0].String())
}
return nil
}
func jsSetUserData(this js.Value, args []js.Value) interface{} {
if len(args) >= 2 && args[0].Type() == js.TypeString {
key := args[0].String()
value := args[1]
// Convert JS value to Go value
var goValue interface{}
switch value.Type() {
case js.TypeBoolean:
goValue = value.Bool()
case js.TypeNumber:
goValue = value.Float()
case js.TypeString:
goValue = value.String()
default:
// For complex objects, store as JSON string
jsonStr := js.Global().Get("JSON").Call("stringify", value).String()
goValue = jsonStr
}
state.SetUserData(key, goValue)
}
return nil
}
func main() {
fmt.Println("State Management WebAssembly Module Initialized")
// Register JavaScript functions
js.Global().Set("initializeState", js.FuncOf(jsInitializeState))
js.Global().Set("getState", js.FuncOf(jsGetState))
js.Global().Set("incrementCounter", js.FuncOf(jsIncrementCounter))
js.Global().Set("addItem", js.FuncOf(jsAddItem))
js.Global().Set("setUserData", js.FuncOf(jsSetUserData))
// Keep the program running
<-make(chan struct{})
}
This pattern demonstrates a thread-safe approach to managing state between JavaScript and Go, with proper synchronization and type conversion.
Component Architecture for WASM Applications
For larger applications, a component-based architecture can help manage complexity:
package main
import (
"fmt"
"syscall/js"
)
// Component represents a UI component with its own state and behavior
type Component interface {
Render() js.Value
HandleEvent(event string, args []js.Value) interface{}
Mount(parent js.Value)
Unmount()
}
// BaseComponent provides common functionality for components
type BaseComponent struct {
ID string
Element js.Value
Children []Component
}
// Counter is a simple counter component
type Counter struct {
BaseComponent
Count int
}
// NewCounter creates a new counter component
func NewCounter(id string) *Counter {
return &Counter{
BaseComponent: BaseComponent{
ID: id,
Children: make([]Component, 0),
},
Count: 0,
}
}
// Render creates the DOM elements for the counter
func (c *Counter) Render() js.Value {
document := js.Global().Get("document")
// Create container div
div := document.Call("createElement", "div")
div.Set("id", c.ID)
// Create count display
countDisplay := document.Call("createElement", "span")
countDisplay.Set("textContent", fmt.Sprintf("Count: %d", c.Count))
countDisplay.Set("id", c.ID+"-display")
// Create increment button
incButton := document.Call("createElement", "button")
incButton.Set("textContent", "Increment")
incButton.Set("id", c.ID+"-increment")
// Add event listener
incButton.Call("addEventListener", "click", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return c.HandleEvent("increment", args)
}))
// Assemble component
div.Call("appendChild", countDisplay)
div.Call("appendChild", document.Call("createElement", "br"))
div.Call("appendChild", incButton)
c.Element = div
return div
}
// HandleEvent handles component events
func (c *Counter) HandleEvent(event string, args []js.Value) interface{} {
switch event {
case "increment":
c.Count++
// Update the display
document := js.Global().Get("document")
display := document.Call("getElementById", c.ID+"-display")
if display.Truthy() {
display.Set("textContent", fmt.Sprintf("Count: %d", c.Count))
}
}
return nil
}
// Mount adds the component to the DOM
func (c *Counter) Mount(parent js.Value) {
element := c.Render()
parent.Call("appendChild", element)
}
// Unmount removes the component from the DOM
func (c *Counter) Unmount() {
if c.Element.Truthy() {
c.Element.Call("remove")
}
}
// TodoList is a more complex component
type TodoList struct {
BaseComponent
Items []string
}
// NewTodoList creates a new todo list component
func NewTodoList(id string) *TodoList {
return &TodoList{
BaseComponent: BaseComponent{
ID: id,
Children: make([]Component, 0),
},
Items: make([]string, 0),
}
}
// Render creates the DOM elements for the todo list
func (t *TodoList) Render() js.Value {
document := js.Global().Get("document")
// Create container div
div := document.Call("createElement", "div")
div.Set("id", t.ID)
// Create title
title := document.Call("createElement", "h3")
title.Set("textContent", "Todo List")
// Create input field
input := document.Call("createElement", "input")
input.Set("type", "text")
input.Set("id", t.ID+"-input")
input.Set("placeholder", "Add new item...")
// Create add button
addButton := document.Call("createElement", "button")
addButton.Set("textContent", "Add")
addButton.Set("id", t.ID+"-add")
// Add event listener
addButton.Call("addEventListener", "click", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return t.HandleEvent("add", args)
}))
// Create list
list := document.Call("createElement", "ul")
list.Set("id", t.ID+"-list")
// Add existing items
for _, item := range t.Items {
li := document.Call("createElement", "li")
li.Set("textContent", item)
list.Call("appendChild", li)
}
// Assemble component
div.Call("appendChild", title)
div.Call("appendChild", input)
div.Call("appendChild", addButton)
div.Call("appendChild", list)
t.Element = div
return div
}
// HandleEvent handles component events
func (t *TodoList) HandleEvent(event string, args []js.Value) interface{} {
switch event {
case "add":
document := js.Global().Get("document")
input := document.Call("getElementById", t.ID+"-input")
if input.Truthy() {
text := input.Get("value").String()
if text != "" {
// Add to items
t.Items = append(t.Items, text)
// Add to DOM
list := document.Call("getElementById", t.ID+"-list")
if list.Truthy() {
li := document.Call("createElement", "li")
li.Set("textContent", text)
list.Call("appendChild", li)
}
// Clear input
input.Set("value", "")
}
}
}
return nil
}
// Mount adds the component to the DOM
func (t *TodoList) Mount(parent js.Value) {
element := t.Render()
parent.Call("appendChild", element)
}
// Unmount removes the component from the DOM
func (t *TodoList) Unmount() {
if t.Element.Truthy() {
t.Element.Call("remove")
}
}
// Application is the main application component
type Application struct {
BaseComponent
Counter *Counter
TodoList *TodoList
}
// NewApplication creates a new application
func NewApplication(id string) *Application {
app := &Application{
BaseComponent: BaseComponent{
ID: id,
Children: make([]Component, 0),
},
}
// Create child components
app.Counter = NewCounter(id + "-counter")
app.TodoList = NewTodoList(id + "-todo")
// Add to children
app.Children = append(app.Children, app.Counter)
app.Children = append(app.Children, app.TodoList)
return app
}
// Render creates the DOM elements for the application
func (a *Application) Render() js.Value {
document := js.Global().Get("document")
// Create container div
div := document.Call("createElement", "div")
div.Set("id", a.ID)
// Create title
title := document.Call("createElement", "h2")
title.Set("textContent", "Go WebAssembly Component Demo")
// Assemble component
div.Call("appendChild", title)
a.Element = div
return div
}
// HandleEvent handles component events
func (a *Application) HandleEvent(event string, args []js.Value) interface{} {
// No events at application level
return nil
}
// Mount adds the application and its children to the DOM
func (a *Application) Mount(parent js.Value) {
element := a.Render()
parent.Call("appendChild", element)
// Mount children
for _, child := range a.Children {
child.Mount(element)
}
}
// Unmount removes the application and its children from the DOM
func (a *Application) Unmount() {
// Unmount children first
for _, child := range a.Children {
child.Unmount()
}
// Unmount self
if a.Element.Truthy() {
a.Element.Call("remove")
}
}
// JavaScript wrapper functions
func jsCreateApplication(this js.Value, args []js.Value) interface{} {
id := "wasm-app"
if len(args) > 0 && args[0].Type() == js.TypeString {
id = args[0].String()
}
app := NewApplication(id)
// Get the mount point
document := js.Global().Get("document")
mountPoint := document.Get("body")
if len(args) > 1 && args[1].Type() == js.TypeString {
mountPointID := args[1].String()
element := document.Call("getElementById", mountPointID)
if element.Truthy() {
mountPoint = element
}
}
// Mount the application
app.Mount(mountPoint)
return nil
}
func main() {
fmt.Println("Component Architecture WebAssembly Module Initialized")
// Register JavaScript functions
js.Global().Set("createApplication", js.FuncOf(jsCreateApplication))
// Keep the program running
<-make(chan struct{})
}
This component architecture demonstrates how to structure larger WebAssembly applications using a component-based approach similar to modern JavaScript frameworks.
JavaScript Interoperability
One of the most powerful aspects of WebAssembly is its ability to interoperate with JavaScript. This section explores advanced techniques for seamless integration between Go and JavaScript.
Bidirectional Function Calls
Effective communication between Go and JavaScript is essential for WebAssembly applications:
package main
import (
"fmt"
"syscall/js"
)
// CallbackRegistry manages JavaScript callbacks from Go
type CallbackRegistry struct {
callbacks map[string]js.Func
}
// NewCallbackRegistry creates a new callback registry
func NewCallbackRegistry() *CallbackRegistry {
return &CallbackRegistry{
callbacks: make(map[string]js.Func),
}
}
// Register adds a JavaScript callback function
func (r *CallbackRegistry) Register(name string, callback js.Func) {
// Release any existing callback with the same name
if existing, ok := r.callbacks[name]; ok {
existing.Release()
}
r.callbacks[name] = callback
}
// Call invokes a registered callback
func (r *CallbackRegistry) Call(name string, args ...interface{}) js.Value {
if callback, ok := r.callbacks[name]; ok {
// Convert Go values to JS values
jsArgs := make([]interface{}, len(args))
for i, arg := range args {
jsArgs[i] = arg
}
return callback.Invoke(jsArgs...)
}
fmt.Printf("Warning: Callback '%s' not registered\n", name)
return js.Undefined()
}
// Release releases all callbacks
func (r *CallbackRegistry) Release() {
for name, callback := range r.callbacks {
callback.Release()
delete(r.callbacks, name)
}
}
// Global callback registry
var registry = NewCallbackRegistry()
// RegisterCallback registers a JavaScript function as a callback
func RegisterCallback(this js.Value, args []js.Value) interface{} {
if len(args) != 2 || args[0].Type() != js.TypeString || !args[1].InstanceOf(js.Global().Get("Function")) {
return "Error: Expected (string, function) arguments"
}
name := args[0].String()
callback := js.FuncOf(func(this js.Value, callbackArgs []js.Value) interface{} {
// Convert args to a slice of interface{} for easier handling
goArgs := make([]interface{}, len(callbackArgs))
for i, arg := range callbackArgs {
// Convert JS values to Go values based on type
switch arg.Type() {
case js.TypeBoolean:
goArgs[i] = arg.Bool()
case js.TypeNumber:
goArgs[i] = arg.Float()
case js.TypeString:
goArgs[i] = arg.String()
default:
goArgs[i] = arg
}
}
// Call the JavaScript function
return args[1].Invoke(goArgs...)
})
registry.Register(name, callback)
return nil
}
// CallJavaScript calls a registered JavaScript function
func CallJavaScript(name string, args ...interface{}) js.Value {
return registry.Call(name, args...)
}
// Example Go function that will call back to JavaScript
func ProcessDataWithCallback(this js.Value, args []js.Value) interface{} {
if len(args) < 1 {
return "Error: Expected at least one argument"
}
// Process the data in Go
data := args[0]
// Prepare result object
result := js.Global().Get("Object").New()
if data.Type() == js.TypeObject && data.InstanceOf(js.Global().Get("Array")) {
length := data.Length()
sum := 0.0
// Calculate sum
for i := 0; i < length; i++ {
sum += data.Index(i).Float()
}
// Calculate average
avg := sum / float64(length)
// Set properties on result object
result.Set("sum", sum)
result.Set("average", avg)
result.Set("count", length)
// Call back to JavaScript with the result
CallJavaScript("onProcessingComplete", result)
}
return result
}
// Example of passing functions from Go to JavaScript
func CreateCalculator(this js.Value, args []js.Value) interface{} {
// Create a calculator object
calculator := js.Global().Get("Object").New()
// Add methods to the calculator
calculator.Set("add", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) < 2 {
return "Error: Expected two arguments"
}
return args[0].Float() + args[1].Float()
}))
calculator.Set("subtract", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) < 2 {
return "Error: Expected two arguments"
}
return args[0].Float() - args[1].Float()
}))
calculator.Set("multiply", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) < 2 {
return "Error: Expected two arguments"
}
return args[0].Float() * args[1].Float()
}))
calculator.Set("divide", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) < 2 {
return "Error: Expected two arguments"
}
if args[1].Float() == 0 {
return "Error: Division by zero"
}
return args[0].Float() / args[1].Float()
}))
return calculator
}
func main() {
fmt.Println("JavaScript Interoperability WebAssembly Module Initialized")
// Register functions to be called from JavaScript
js.Global().Set("registerCallback", js.FuncOf(RegisterCallback))
js.Global().Set("processDataWithCallback", js.FuncOf(ProcessDataWithCallback))
js.Global().Set("createCalculator", js.FuncOf(CreateCalculator))
// Keep the program running
<-make(chan struct{})
}
Example JavaScript usage:
// Register a callback function
registerCallback("onProcessingComplete", function(result) {
console.log("Processing complete!");
console.log("Sum:", result.sum);
console.log("Average:", result.average);
console.log("Count:", result.count);
});
// Call Go function with data
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const result = processDataWithCallback(data);
// Create and use a calculator object from Go
const calculator = createCalculator();
console.log("5 + 3 =", calculator.add(5, 3));
console.log("10 - 4 =", calculator.subtract(10, 4));
console.log("6 * 7 =", calculator.multiply(6, 7));
console.log("20 / 5 =", calculator.divide(20, 5));
Working with DOM and Browser APIs
Go WebAssembly can interact with the DOM and browser APIs through JavaScript:
package main
import (
"fmt"
"syscall/js"
"time"
)
// DOM manipulation functions
func CreateElement(tagName string) js.Value {
document := js.Global().Get("document")
return document.Call("createElement", tagName)
}
func GetElementById(id string) js.Value {
document := js.Global().Get("document")
return document.Call("getElementById", id)
}
func QuerySelector(selector string) js.Value {
document := js.Global().Get("document")
return document.Call("querySelector", selector)
}
func QuerySelectorAll(selector string) js.Value {
document := js.Global().Get("document")
return document.Call("querySelectorAll", selector)
}
// Example: Create a canvas-based animation
func CreateCanvasAnimation(this js.Value, args []js.Value) interface{} {
// Get container element
containerId := "animation-container"
if len(args) > 0 && args[0].Type() == js.TypeString {
containerId = args[0].String()
}
container := GetElementById(containerId)
if !container.Truthy() {
fmt.Println("Container element not found")
return nil
}
// Create canvas element
canvas := CreateElement("canvas")
canvas.Set("width", 400)
canvas.Set("height", 300)
canvas.Set("style", "border: 1px solid black;")
// Append canvas to container
container.Call("appendChild", canvas)
// Get canvas context
ctx := canvas.Call("getContext", "2d")
// Animation variables
x := 50.0
y := 50.0
dx := 2.0
dy := 1.5
radius := 20.0
// Create animation frame callback
var animationCallback js.Func
animationCallback = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
// Clear canvas
ctx.Call("clearRect", 0, 0, canvas.Get("width").Int(), canvas.Get("height").Int())
// Draw circle
ctx.Call("beginPath")
ctx.Call("arc", x, y, radius, 0, 2*3.14159)
ctx.Set("fillStyle", "blue")
ctx.Call("fill")
ctx.Call("closePath")
// Update position
x += dx
y += dy
// Bounce off walls
if x+radius > canvas.Get("width").Float() || x-radius < 0 {
dx = -dx
}
if y+radius > canvas.Get("height").Float() || y-radius < 0 {
dy = -dy
}
// Request next frame
js.Global().Call("requestAnimationFrame", animationCallback)
return nil
})
// Start animation
js.Global().Call("requestAnimationFrame", animationCallback)
// Return a function to stop the animation
return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
animationCallback.Release()
return nil
})
}
// Example: Working with browser APIs
func FetchData(this js.Value, args []js.Value) interface{} {
if len(args) < 2 || args[0].Type() != js.TypeString || !args[1].InstanceOf(js.Global().Get("Function")) {
return "Error: Expected (url, callback) arguments"
}
url := args[0].String()
callback := args[1]
// Create a Promise
fetch := js.Global().Call("fetch", url)
// Handle response
then := fetch.Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
response := args[0]
return response.Call("json")
}))
// Handle JSON data
then.Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
data := args[0]
callback.Invoke(data)
return nil
}))
// Handle errors
then.Call("catch", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
err := args[0]
fmt.Println("Fetch error:", err.Get("message").String())
return nil
}))
return nil
}
// Example: Using browser storage
func StorageExample(this js.Value, args []js.Value) interface{} {
// Get localStorage
localStorage := js.Global().Get("localStorage")
// Set item
localStorage.Call("setItem", "goWasmTimestamp", time.Now().String())
// Get item
timestamp := localStorage.Call("getItem", "goWasmTimestamp").String()
fmt.Println("Stored timestamp:", timestamp)
// Create a result object
result := js.Global().Get("Object").New()
result.Set("timestamp", timestamp)
result.Set("storageAvailable", true)
return result
}
func main() {
fmt.Println("DOM and Browser API WebAssembly Module Initialized")
// Register functions to be called from JavaScript
js.Global().Set("createCanvasAnimation", js.FuncOf(CreateCanvasAnimation))
js.Global().Set("fetchData", js.FuncOf(FetchData))
js.Global().Set("storageExample", js.FuncOf(StorageExample))
// Keep the program running
<-make(chan struct{})
}
Example HTML usage:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Go WASM DOM Example</title>
<script src="wasm_exec.js"></script>
<script>
// Initialize Go WASM
const go = new Go();
WebAssembly.instantiateStreaming(fetch("dom_api.wasm"), go.importObject)
.then((result) => {
go.run(result.instance);
// Create animation
const stopAnimation = createCanvasAnimation("animation-container");
// Fetch data example
fetchData("https://jsonplaceholder.typicode.com/todos/1", function(data) {
console.log("Fetched data:", data);
document.getElementById("fetch-result").textContent =
JSON.stringify(data, null, 2);
});
// Storage example
const storageResult = storageExample();
document.getElementById("storage-result").textContent =
"Timestamp: " + storageResult.timestamp;
// Add stop button functionality
document.getElementById("stop-button").addEventListener("click", function() {
stopAnimation();
this.disabled = true;
});
});
</script>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
.section { margin: 20px 0; padding: 10px; border: 1px solid #ddd; border-radius: 4px; }
pre { background-color: #f5f5f5; padding: 10px; border-radius: 4px; overflow: auto; }
</style>
</head>
<body>
<h1>Go WebAssembly DOM and Browser API Examples</h1>
<div class="section">
<h2>Canvas Animation</h2>
<div id="animation-container"></div>
<button id="stop-button">Stop Animation</button>
</div>
<div class="section">
<h2>Fetch API Example</h2>
<pre id="fetch-result">Loading...</pre>
</div>
<div class="section">
<h2>Storage API Example</h2>
<div id="storage-result"></div>
</div>
</body>
</html>
// Render creates the DOM elements for the counter func (c *Counter) Render() js.Value { document := js.Global().Get(“document”)
// Create container div
div := document.Call("createElement", "div")
div.Set("id", c.ID)
// Create count display
countDisplay := document.Call("createElement", "span")
countDisplay.Set("textContent", fmt.Sprintf("Count: %d", c.Count))
countDisplay.Set("id", c.ID+"-display")
// Create increment button
incButton := document.Call("createElement", "button")
incButton.Set("textContent", "Increment")
incButton.Set("id", c.ID+"-increment")
// Add event listener
incButton.Call("addEventListener", "click", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return c.HandleEvent("increment", args)
}))
// Assemble component
div.Call("appendChild", countDisplay)
div.Call("appendChild", document.Call("createElement", "br"))
div.Call("appendChild", incButton)
c.Element = div
return div
}
// HandleEvent handles component events func (c *Counter) HandleEvent(event string, args []js.Value) interface{} { switch event { case “increment”: c.Count++ // Update the display document := js.Global().Get(“document”) display := document.Call(“getElementById”, c.ID+"-display") if display.Truthy() { display.Set(“textContent”, fmt.Sprintf(“Count: %d”, c.Count)) } } return nil }
// Mount adds the component to the DOM func (c *Counter) Mount(parent js.Value) { element := c.Render() parent.Call(“appendChild”, element) }
// Unmount removes the component from the DOM func (c *Counter) Unmount() { if c.Element.Truthy() { c.Element.Call(“remove”) } }
// TodoList is a more complex component type TodoList struct { BaseComponent Items []string }
// NewTodoList creates a new todo list component func NewTodoList(id string) *TodoList { return &TodoList{ BaseComponent: BaseComponent{ ID: id, Children: make([]Component, 0), }, Items: make([]string, 0), } }
// Render creates the DOM elements for the todo list func (t *TodoList) Render() js.Value { document := js.Global().Get(“document”)
// Create container div
div := document.Call("createElement", "div")
div.Set("id", t.ID)
// Create title
title := document.Call("createElement", "h3")
title.Set("textContent", "Todo List")
// Create input field
input := document.Call("createElement", "input")
input.Set("type", "text")
input.Set("id", t.ID+"-input")
input.Set("placeholder", "Add new item...")
// Create add button
addButton := document.Call("createElement", "button")
addButton.Set("textContent", "Add")
addButton.Set("id", t.ID+"-add")
// Add event listener
addButton.Call("addEventListener", "click", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return t.HandleEvent("add", args)
}))
// Create list
list := document.Call("createElement", "ul")
list.Set("id", t.ID+"-list")
// Add existing items
for _, item := range t.Items {
li := document.Call("createElement", "li")
li.Set("textContent", item)
list.Call("appendChild", li)
}
// Assemble component
div.Call("appendChild", title)
div.Call("appendChild", input)
div.Call("appendChild", addButton)
div.Call("appendChild", list)
t.Element = div
return div
}
// HandleEvent handles component events func (t *TodoList) HandleEvent(event string, args []js.Value) interface{} { switch event { case “add”: document := js.Global().Get(“document”) input := document.Call(“getElementById”, t.ID+"-input") if input.Truthy() { text := input.Get(“value”).String() if text != "" { // Add to items t.Items = append(t.Items, text)
// Add to DOM
list := document.Call("getElementById", t.ID+"-list")
if list.Truthy() {
li := document.Call("createElement", "li")
li.Set("textContent", text)
list.Call("appendChild", li)
}
// Clear input
input.Set("value", "")
}
}
}
return nil
}
// Mount adds the component to the DOM func (t *TodoList) Mount(parent js.Value) { element := t.Render() parent.Call(“appendChild”, element) }
// Unmount removes the component from the DOM func (t *TodoList) Unmount() { if t.Element.Truthy() { t.Element.Call(“remove”) } }
// Application is the main application component type Application struct { BaseComponent Counter *Counter TodoList *TodoList }
// NewApplication creates a new application func NewApplication(id string) *Application { app := &Application{ BaseComponent: BaseComponent{ ID: id, Children: make([]Component, 0), }, }
// Create child components
app.Counter = NewCounter(id + "-counter")
app.TodoList = NewTodoList(id + "-todo")
// Add to children
app.Children = append(app.Children, app.Counter)
app.Children = append(app.Children, app.TodoList)
return app
}
// Render creates the DOM elements for the application func (a *Application) Render() js.Value { document := js.Global().Get(“document”)
// Create container div
div := document.Call("createElement", "div")
div.Set("id", a.ID)
// Create title
title := document.Call("createElement", "h2")
title.Set("textContent", "Go WebAssembly Component Demo")
// Assemble component
div.Call("appendChild", title)
a.Element = div
return div
}
// HandleEvent handles component events func (a *Application) HandleEvent(event string, args []js.Value) interface{} { // No events at application level return nil }
// Mount adds the application and its children to the DOM func (a *Application) Mount(parent js.Value) { element := a.Render() parent.Call(“appendChild”, element)
// Mount children
for _, child := range a.Children {
child.Mount(element)
}
}
// Unmount removes the application and its children from the DOM func (a *Application) Unmount() { // Unmount children first for _, child := range a.Children { child.Unmount() }
// Unmount self
if a.Element.Truthy() {
a.Element.Call("remove")
}
}
// JavaScript wrapper functions func jsCreateApplication(this js.Value, args []js.Value) interface{} { id := “wasm-app” if len(args) > 0 && args[0].Type() == js.TypeString { id = args[0].String() }
app := NewApplication(id)
// Get the mount point
document := js.Global().Get("document")
mountPoint := document.Get("body")
if len(args) > 1 && args[1].Type() == js.TypeString {
mountPointID := args[1].String()
element := document.Call("getElementById", mountPointID)
if element.Truthy() {
mountPoint = element
}
}
// Mount the application
app.Mount(mountPoint)
return nil
}
func main() { fmt.Println(“Component Architecture WebAssembly Module Initialized”)
// Register JavaScript functions
js.Global().Set("createApplication", js.FuncOf(jsCreateApplication))
// Keep the program running
<-make(chan struct{})
}
This component architecture demonstrates how to structure larger WebAssembly applications using a component-based approach similar to modern JavaScript frameworks.
---
### JavaScript Interoperability
One of the most powerful aspects of WebAssembly is its ability to interoperate with JavaScript. This section explores advanced techniques for seamless integration between Go and JavaScript.
#### Bidirectional Function Calls
Effective communication between Go and JavaScript is essential for WebAssembly applications:
```go
package main
import (
"fmt"
"syscall/js"
)
// CallbackRegistry manages JavaScript callbacks from Go
type CallbackRegistry struct {
callbacks map[string]js.Func
}
// NewCallbackRegistry creates a new callback registry
func NewCallbackRegistry() *CallbackRegistry {
return &CallbackRegistry{
callbacks: make(map[string]js.Func),
}
}
// Register adds a JavaScript callback function
func (r *CallbackRegistry) Register(name string, callback js.Func) {
// Release any existing callback with the same name
if existing, ok := r.callbacks[name]; ok {
existing.Release()
}
r.callbacks[name] = callback
}
// Call invokes a registered callback
func (r *CallbackRegistry) Call(name string, args ...interface{}) js.Value {
if callback, ok := r.callbacks[name]; ok {
// Convert Go values to JS values
jsArgs := make([]interface{}, len(args))
for i, arg := range args {
jsArgs[i] = arg
}
return callback.Invoke(jsArgs...)
}
fmt.Printf("Warning: Callback '%s' not registered\n", name)
return js.Undefined()
}
// Release releases all callbacks
func (r *CallbackRegistry) Release() {
for name, callback := range r.callbacks {
callback.Release()
delete(r.callbacks, name)
}
}
// Global callback registry
var registry = NewCallbackRegistry()
// RegisterCallback registers a JavaScript function as a callback
func RegisterCallback(this js.Value, args []js.Value) interface{} {
if len(args) != 2 || args[0].Type() != js.TypeString || !args[1].InstanceOf(js.Global().Get("Function")) {
return "Error: Expected (string, function) arguments"
}
name := args[0].String()
callback := js.FuncOf(func(this js.Value, callbackArgs []js.Value) interface{} {
// Convert args to a slice of interface{} for easier handling
goArgs := make([]interface{}, len(callbackArgs))
for i, arg := range callbackArgs {
// Convert JS values to Go values based on type
switch arg.Type() {
case js.TypeBoolean:
goArgs[i] = arg.Bool()
case js.TypeNumber:
goArgs[i] = arg.Float()
case js.TypeString:
goArgs[i] = arg.String()
default:
goArgs[i] = arg
}
}
// Call the JavaScript function
return args[1].Invoke(goArgs...)
})
registry.Register(name, callback)
return nil
}
// CallJavaScript calls a registered JavaScript function
func CallJavaScript(name string, args ...interface{}) js.Value {
return registry.Call(name, args...)
}
// Example Go function that will call back to JavaScript
func ProcessDataWithCallback(this js.Value, args []js.Value) interface{} {
if len(args) < 1 {
return "Error: Expected at least one argument"
}
// Process the data in Go
data := args[0]
// Prepare result object
result := js.Global().Get("Object").New()
if data.Type() == js.TypeObject && data.InstanceOf(js.Global().Get("Array")) {
length := data.Length()
sum := 0.0
// Calculate sum
for i := 0; i < length; i++ {
sum += data.Index(i).Float()
}
// Calculate average
avg := sum / float64(length)
// Set properties on result object
result.Set("sum", sum)
result.Set("average", avg)
result.Set("count", length)
// Call back to JavaScript with the result
CallJavaScript("onProcessingComplete", result)
}
return result
}
// Example of passing functions from Go to JavaScript
func CreateCalculator(this js.Value, args []js.Value) interface{} {
// Create a calculator object
calculator := js.Global().Get("Object").New()
// Add methods to the calculator
calculator.Set("add", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) < 2 {
return "Error: Expected two arguments"
}
return args[0].Float() + args[1].Float()
}))
calculator.Set("subtract", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) < 2 {
return "Error: Expected two arguments"
}
return args[0].Float() - args[1].Float()
}))
calculator.Set("multiply", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) < 2 {
return "Error: Expected two arguments"
}
return args[0].Float() * args[1].Float()
}))
calculator.Set("divide", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) < 2 {
return "Error: Expected two arguments"
}
if args[1].Float() == 0 {
return "Error: Division by zero"
}
return args[0].Float() / args[1].Float()
}))
return calculator
}
func main() {
fmt.Println("JavaScript Interoperability WebAssembly Module Initialized")
// Register functions to be called from JavaScript
js.Global().Set("registerCallback", js.FuncOf(RegisterCallback))
js.Global().Set("processDataWithCallback", js.FuncOf(ProcessDataWithCallback))
js.Global().Set("createCalculator", js.FuncOf(CreateCalculator))
// Keep the program running
<-make(chan struct{})
}
Example JavaScript usage:
// Register a callback function
registerCallback("onProcessingComplete", function(result) {
console.log("Processing complete!");
console.log("Sum:", result.sum);
console.log("Average:", result.average);
console.log("Count:", result.count);
});
// Call Go function with data
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const result = processDataWithCallback(data);
// Create and use a calculator object from Go
const calculator = createCalculator();
console.log("5 + 3 =", calculator.add(5, 3));
console.log("10 - 4 =", calculator.subtract(10, 4));
console.log("6 * 7 =", calculator.multiply(6, 7));
console.log("20 / 5 =", calculator.divide(20, 5));
Working with DOM and Browser APIs
Go WebAssembly can interact with the DOM and browser APIs through JavaScript:
package main
import (
"fmt"
"syscall/js"
"time"
)
// DOM manipulation functions
func CreateElement(tagName string) js.Value {
document := js.Global().Get("document")
return document.Call("createElement", tagName)
}
func GetElementById(id string) js.Value {
document := js.Global().Get("document")
return document.Call("getElementById", id)
}
func QuerySelector(selector string) js.Value {
document := js.Global().Get("document")
return document.Call("querySelector", selector)
}
func QuerySelectorAll(selector string) js.Value {
document := js.Global().Get("document")
return document.Call("querySelectorAll", selector)
}
// Example: Create a canvas-based animation
func CreateCanvasAnimation(this js.Value, args []js.Value) interface{} {
// Get container element
containerId := "animation-container"
if len(args) > 0 && args[0].Type() == js.TypeString {
containerId = args[0].String()
}
container := GetElementById(containerId)
if !container.Truthy() {
fmt.Println("Container element not found")
return nil
}
// Create canvas element
canvas := CreateElement("canvas")
canvas.Set("width", 400)
canvas.Set("height", 300)
canvas.Set("style", "border: 1px solid black;")
// Append canvas to container
container.Call("appendChild", canvas)
// Get canvas context
ctx := canvas.Call("getContext", "2d")
// Animation variables
x := 50.0
y := 50.0
dx := 2.0
dy := 1.5
radius := 20.0
// Create animation frame callback
var animationCallback js.Func
animationCallback = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
// Clear canvas
ctx.Call("clearRect", 0, 0, canvas.Get("width").Int(), canvas.Get("height").Int())
// Draw circle
ctx.Call("beginPath")
ctx.Call("arc", x, y, radius, 0, 2*3.14159)
ctx.Set("fillStyle", "blue")
ctx.Call("fill")
ctx.Call("closePath")
// Update position
x += dx
y += dy
// Bounce off walls
if x+radius > canvas.Get("width").Float() || x-radius < 0 {
dx = -dx
}
if y+radius > canvas.Get("height").Float() || y-radius < 0 {
dy = -dy
}
// Request next frame
js.Global().Call("requestAnimationFrame", animationCallback)
return nil
})
// Start animation
js.Global().Call("requestAnimationFrame", animationCallback)
// Return a function to stop the animation
return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
animationCallback.Release()
return nil
})
}
// Example: Working with browser APIs
func FetchData(this js.Value, args []js.Value) interface{} {
if len(args) < 2 || args[0].Type() != js.TypeString || !args[1].InstanceOf(js.Global().Get("Function")) {
return "Error: Expected (url, callback) arguments"
}
url := args[0].String()
callback := args[1]
// Create a Promise
fetch := js.Global().Call("fetch", url)
// Handle response
then := fetch.Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
response := args[0]
return response.Call("json")
}))
// Handle JSON data
then.Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
data := args[0]
callback.Invoke(data)
return nil
}))
// Handle errors
then.Call("catch", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
err := args[0]
fmt.Println("Fetch error:", err.Get("message").String())
return nil
}))
return nil
}
// Example: Using browser storage
func StorageExample(this js.Value, args []js.Value) interface{} {
// Get localStorage
localStorage := js.Global().Get("localStorage")
// Set item
localStorage.Call("setItem", "goWasmTimestamp", time.Now().String())
// Get item
timestamp := localStorage.Call("getItem", "goWasmTimestamp").String()
fmt.Println("Stored timestamp:", timestamp)
// Create a result object
result := js.Global().Get("Object").New()
result.Set("timestamp", timestamp)
result.Set("storageAvailable", true)
return result
}
func main() {
fmt.Println("DOM and Browser API WebAssembly Module Initialized")
// Register functions to be called from JavaScript
js.Global().Set("createCanvasAnimation", js.FuncOf(CreateCanvasAnimation))
js.Global().Set("fetchData", js.FuncOf(FetchData))
js.Global().Set("storageExample", js.FuncOf(StorageExample))
// Keep the program running
<-make(chan struct{})
}
Example HTML usage:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Go WASM DOM Example</title>
<script src="wasm_exec.js"></script>
<script>
// Initialize Go WASM
const go = new Go();
WebAssembly.instantiateStreaming(fetch("dom_api.wasm"), go.importObject)
.then((result) => {
go.run(result.instance);
// Create animation
const stopAnimation = createCanvasAnimation("animation-container");
// Fetch data example
fetchData("https://jsonplaceholder.typicode.com/todos/1", function(data) {
console.log("Fetched data:", data);
document.getElementById("fetch-result").textContent =
JSON.stringify(data, null, 2);
});
// Storage example
const storageResult = storageExample();
document.getElementById("storage-result").textContent =
"Timestamp: " + storageResult.timestamp;
// Add stop button functionality
document.getElementById("stop-button").addEventListener("click", function() {
stopAnimation();
this.disabled = true;
});
});
</script>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
.section { margin: 20px 0; padding: 10px; border: 1px solid #ddd; border-radius: 4px; }
pre { background-color: #f5f5f5; padding: 10px; border-radius: 4px; overflow: auto; }
</style>
</head>
<body>
<h1>Go WebAssembly DOM and Browser API Examples</h1>
<div class="section">
<h2>Canvas Animation</h2>
<div id="animation-container"></div>
<button id="stop-button">Stop Animation</button>
</div>
<div class="section">
<h2>Fetch API Example</h2>
<pre id="fetch-result">Loading...</pre>
</div>
<div class="section">
<h2>Storage API Example</h2>
<div id="storage-result"></div>
</div>
</body>
</html>
Handling Events and Callbacks
Managing events and callbacks between Go and JavaScript requires careful consideration:
package main
import (
"fmt"
"syscall/js"
"time"
)
// EventEmitter provides a way to register and trigger events
type EventEmitter struct {
listeners map[string][]js.Func
}
// NewEventEmitter creates a new event emitter
func NewEventEmitter() *EventEmitter {
return &EventEmitter{
listeners: make(map[string][]js.Func),
}
}
// On registers an event listener
func (e *EventEmitter) On(event string, listener js.Func) {
if _, ok := e.listeners[event]; !ok {
e.listeners[event] = make([]js.Func, 0)
}
e.listeners[event] = append(e.listeners[event], listener)
}
// Off removes an event listener
func (e *EventEmitter) Off(event string, listener js.Func) {
if listeners, ok := e.listeners[event]; ok {
for i, l := range listeners {
// Compare function references
if l.Value == listener.Value {
// Remove the listener
e.listeners[event] = append(listeners[:i], listeners[i+1:]...)
break
}
}
}
}
// Emit triggers an event
func (e *EventEmitter) Emit(event string, args ...interface{}) {
if listeners, ok := e.listeners[event]; ok {
for _, listener := range listeners {
listener.Invoke(args...)
}
}
}
// Release releases all event listeners
func (e *EventEmitter) Release() {
for _, listeners := range e.listeners {
for _, listener := range listeners {
listener.Release()
}
}
e.listeners = make(map[string][]js.Func)
}
// Global event emitter
var emitter = NewEventEmitter()
// JavaScript wrapper functions
func jsOn(this js.Value, args []js.Value) interface{} {
if len(args) != 2 || args[0].Type() != js.TypeString || !args[1].InstanceOf(js.Global().Get("Function")) {
return "Error: Expected (event, callback) arguments"
}
event := args[0].String()
callback := js.FuncOf(func(this js.Value, callbackArgs []js.Value) interface{} {
return args[1].Invoke(callbackArgs...)
})
emitter.On(event, callback)
return nil
}
func jsOff(this js.Value, args []js.Value) interface{} {
if len(args) != 2 || args[0].Type() != js.TypeString || !args[1].InstanceOf(js.Global().Get("Function")) {
return "Error: Expected (event, callback) arguments"
}
event := args[0].String()
callback := js.FuncOf(func(this js.Value, callbackArgs []js.Value) interface{} {
return args[1].Invoke(callbackArgs...)
})
emitter.Off(event, callback)
callback.Release()
return nil
}
func jsEmit(this js.Value, args []js.Value) interface{} {
if len(args) < 1 || args[0].Type() != js.TypeString {
return "Error: Expected event name as first argument"
}
event := args[0].String()
eventArgs := make([]interface{}, len(args)-1)
for i := 1; i < len(args); i++ {
eventArgs[i-1] = args[i]
}
emitter.Emit(event, eventArgs...)
return nil
}
// Example: Long-running task with progress updates
func LongRunningTask(this js.Value, args []js.Value) interface{} {
totalSteps := 10
if len(args) > 0 && args[0].Type() == js.TypeNumber {
totalSteps = args[0].Int()
}
// Create a channel to signal completion
done := make(chan struct{})
// Start the task in a goroutine
go func() {
for i := 1; i <= totalSteps; i++ {
// Simulate work
time.Sleep(500 * time.Millisecond)
// Report progress
progress := float64(i) / float64(totalSteps)
emitter.Emit("progress", progress, i, totalSteps)
}
// Signal completion
emitter.Emit("complete", "Task completed successfully")
close(done)
}()
// Return a function to cancel the task
return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
// Signal completion early
select {
case <-done:
// Task already completed
default:
emitter.Emit("cancel", "Task cancelled by user")
close(done)
}
return nil
})
}
func main() {
fmt.Println("Event Handling WebAssembly Module Initialized")
// Register JavaScript functions
js.Global().Set("on", js.FuncOf(jsOn))
js.Global().Set("off", js.FuncOf(jsOff))
js.Global().Set("emit", js.FuncOf(jsEmit))
js.Global().Set("startLongRunningTask", js.FuncOf(LongRunningTask))
// Keep the program running
<-make(chan struct{})
}
Example JavaScript usage:
// Register event listeners
on("progress", function(progress, step, total) {
console.log(`Progress: ${Math.round(progress * 100)}% (Step ${step}/${total})`);
document.getElementById("progress-bar").style.width = `${progress * 100}%`;
document.getElementById("progress-text").textContent = `${Math.round(progress * 100)}%`;
});
on("complete", function(message) {
console.log("Task completed:", message);
document.getElementById("status").textContent = message;
document.getElementById("start-button").disabled = false;
});
on("cancel", function(message) {
console.log("Task cancelled:", message);
document.getElementById("status").textContent = message;
document.getElementById("start-button").disabled = false;
});
// Start button click handler
document.getElementById("start-button").addEventListener("click", function() {
this.disabled = true;
document.getElementById("status").textContent = "Task running...";
// Start the task and get the cancel function
const cancelTask = startLongRunningTask(20); // 20 steps
// Set up cancel button
document.getElementById("cancel-button").onclick = function() {
cancelTask();
this.disabled = true;
};
});
Performance Optimization Techniques
While WebAssembly offers significant performance advantages over JavaScript, optimizing Go WASM applications requires specific techniques to achieve the best possible performance.
Memory Management Optimization
Efficient memory management is crucial for WebAssembly performance:
package main
import (
"fmt"
"syscall/js"
"unsafe"
)
// MemoryPool implements a simple object pool to reduce allocations
type MemoryPool struct {
pool []interface{}
size int
}
// NewMemoryPool creates a new memory pool with the specified size
func NewMemoryPool(size int, factory func() interface{}) *MemoryPool {
pool := &MemoryPool{
pool: make([]interface{}, size),
size: size,
}
// Pre-allocate objects
for i := 0; i < size; i++ {
pool.pool[i] = factory()
}
return pool
}
// Get retrieves an object from the pool
func (p *MemoryPool) Get() interface{} {
if len(p.pool) == 0 {
// Pool is empty, create a new object
return nil
}
// Get the last object
obj := p.pool[len(p.pool)-1]
p.pool = p.pool[:len(p.pool)-1]
return obj
}
// Put returns an object to the pool
func (p *MemoryPool) Put(obj interface{}) {
if len(p.pool) < p.size {
p.pool = append(p.pool, obj)
}
}
// Vector3 represents a 3D vector
type Vector3 struct {
X, Y, Z float64
}
// Reset resets the vector to zero
func (v *Vector3) Reset() {
v.X = 0
v.Y = 0
v.Z = 0
}
// Set sets the vector components
func (v *Vector3) Set(x, y, z float64) {
v.X = x
v.Y = y
v.Z = z
}
// Add adds another vector to this vector
func (v *Vector3) Add(other *Vector3) {
v.X += other.X
v.Y += other.Y
v.Z += other.Z
}
// Global vector pool
var vectorPool = NewMemoryPool(1000, func() interface{} {
return &Vector3{}
})
// Example: Particle system with pooled objects
func CreateParticleSystem(this js.Value, args []js.Value) interface{} {
particleCount := 1000
if len(args) > 0 && args[0].Type() == js.TypeNumber {
particleCount = args[0].Int()
}
// Create typed arrays for particle data
// This allows direct sharing of memory between Go and JavaScript
positions := js.Global().Get("Float64Array").New(particleCount * 3)
velocities := js.Global().Get("Float64Array").New(particleCount * 3)
// Update function that minimizes allocations
updateFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
// Get delta time
dt := 0.016 // Default to 16ms
if len(args) > 0 && args[0].Type() == js.TypeNumber {
dt = args[0].Float()
}
// Update particles without allocations
for i := 0; i < particleCount; i++ {
// Get position and velocity
px := positions.Index(i*3).Float()
py := positions.Index(i*3+1).Float()
pz := positions.Index(i*3+2).Float()
vx := velocities.Index(i*3).Float()
vy := velocities.Index(i*3+1).Float()
vz := velocities.Index(i*3+2).Float()
// Update position
px += vx * dt
py += vy * dt
pz += vz * dt
// Bounce off walls
if px > 10 || px < -10 {
vx = -vx
}
if py > 10 || py < -10 {
vy = -vy
}
if pz > 10 || pz < -10 {
vz = -vz
}
// Update arrays
positions.SetIndex(i*3, px)
positions.SetIndex(i*3+1, py)
positions.SetIndex(i*3+2, pz)
velocities.SetIndex(i*3, vx)
velocities.SetIndex(i*3+1, vy)
velocities.SetIndex(i*3+2, vz)
}
return nil
})
// Initialize particles
for i := 0; i < particleCount; i++ {
// Use pooled vector for initialization
pos := vectorPool.Get().(*Vector3)
vel := vectorPool.Get().(*Vector3)
// Set random position and velocity
pos.Set(
(js.Global().Get("Math").Call("random").Float()-0.5)*20,
(js.Global().Get("Math").Call("random").Float()-0.5)*20,
(js.Global().Get("Math").Call("random").Float()-0.5)*20,
)
vel.Set(
(js.Global().Get("Math").Call("random").Float()-0.5)*2,
(js.Global().Get("Math").Call("random").Float()-0.5)*2,
(js.Global().Get("Math").Call("random").Float()-0.5)*2,
)
// Copy to typed arrays
positions.SetIndex(i*3, pos.X)
positions.SetIndex(i*3+1, pos.Y)
positions.SetIndex(i*3+2, pos.Z)
velocities.SetIndex(i*3, vel.X)
velocities.SetIndex(i*3+1, vel.Y)
velocities.SetIndex(i*3+2, vel.Z)
// Return vectors to pool
pos.Reset()
vel.Reset()
vectorPool.Put(pos)
vectorPool.Put(vel)
}
// Create result object
result := js.Global().Get("Object").New()
result.Set("positions", positions)
result.Set("velocities", velocities)
result.Set("update", updateFunc)
return result
}
// Example: Direct memory manipulation for performance
func CreateImageProcessor(this js.Value, args []js.Value) interface{} {
if len(args) < 1 || !args[0].InstanceOf(js.Global().Get("Uint8ClampedArray")) {
return "Error: Expected Uint8ClampedArray argument"
}
// Get image data
imageData := args[0]
length := imageData.Length()
// Create processor object
processor := js.Global().Get("Object").New()
// Add grayscale method
processor.Set("grayscale", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
// Process image data in chunks for better performance
const chunkSize = 1024
for offset := 0; offset < length; offset += chunkSize*4 {
end := offset + chunkSize*4
if end > length {
end = length
}
// Process a chunk
for i := offset; i < end; i += 4 {
r := imageData.Index(i).Int()
g := imageData.Index(i+1).Int()
b := imageData.Index(i+2).Int()
// Calculate grayscale value
gray := uint8((r*299 + g*587 + b*114) / 1000)
// Set RGB channels to gray
imageData.SetIndex(i, gray)
imageData.SetIndex(i+1, gray)
imageData.SetIndex(i+2, gray)
// Alpha channel (i+3) remains unchanged
}
}
return nil
}))
// Add invert method
processor.Set("invert", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
// Process image data in chunks for better performance
const chunkSize = 1024
for offset := 0; offset < length; offset += chunkSize*4 {
end := offset + chunkSize*4
if end > length {
end = length
}
// Process a chunk
for i := offset; i < end; i += 4 {
// Invert RGB channels
imageData.SetIndex(i, 255-imageData.Index(i).Int())
imageData.SetIndex(i+1, 255-imageData.Index(i+1).Int())
imageData.SetIndex(i+2, 255-imageData.Index(i+2).Int())
// Alpha channel (i+3) remains unchanged
}
}
return nil
}))
return processor
}
func main() {
fmt.Println("Performance Optimization WebAssembly Module Initialized")
// Register JavaScript functions
js.Global().Set("createParticleSystem", js.FuncOf(CreateParticleSystem))
js.Global().Set("createImageProcessor", js.FuncOf(CreateImageProcessor))
// Keep the program running
<-make(chan struct{})
}
Minimizing JavaScript/Go Boundary Crossings
Each call between JavaScript and Go incurs overhead, so minimizing these crossings is important:
package main
import (
"fmt"
"syscall/js"
)
// BatchProcessor demonstrates batching operations to reduce JS/Go boundary crossings
func CreateBatchProcessor(this js.Value, args []js.Value) interface{} {
processor := js.Global().Get("Object").New()
// Bad approach: Process items one at a time
processor.Set("processItemsIndividually", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) < 1 || !args[0].InstanceOf(js.Global().Get("Array")) {
return "Error: Expected array argument"
}
items := args[0]
length := items.Length()
results := js.Global().Get("Array").New(length)
// Process each item individually (many JS/Go crossings)
for i := 0; i < length; i++ {
item := items.Index(i)
// Process the item
result := processItem(item)
// Store the result
results.SetIndex(i, result)
}
return results
}))
// Good approach: Process items in a batch
processor.Set("processItemsBatch", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) < 1 || !args[0].InstanceOf(js.Global().Get("Array")) {
return "Error: Expected array argument"
}
items := args[0]
length := items.Length()
results := js.Global().Get("Array").New(length)
// Extract all items at once
goItems := make([]interface{}, length)
for i := 0; i < length; i++ {
goItems[i] = items.Index(i).Interface()
}
// Process all items in Go
goResults := processItemsBatch(goItems)
// Return all results at once
for i, result := range goResults {
results.SetIndex(i, result)
}
return results
}))
return processor
}
// Process a single item
func processItem(item js.Value) interface{} {
// Example processing: square a number
if item.Type() == js.TypeNumber {
value := item.Float()
return value * value
}
return item
}
// Process multiple items in a batch
func processItemsBatch(items []interface{}) []interface{} {
results := make([]interface{}, len(items))
for i, item := range items {
// Example processing: square a number
switch v := item.(type) {
case float64:
results[i] = v * v
default:
results[i] = item
}
}
return results
}
// Example: Transferring large data efficiently
func CreateDataTransferer(this js.Value, args []js.Value) interface{} {
transferer := js.Global().Get("Object").New()
// Transfer data using typed arrays for efficiency
transferer.Set("processLargeData", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) < 1 || !args[0].InstanceOf(js.Global().Get("Float64Array")) {
return "Error: Expected Float64Array argument"
}
// Get the typed array
data := args[0]
length := data.Length()
// Create a result array of the same size
result := js.Global().Get("Float64Array").New(length)
// Process the data directly without copying
for i := 0; i < length; i++ {
// Example processing: square root
value := data.Index(i).Float()
result.SetIndex(i, value * value)
}
return result
}))
return transferer
}
func main() {
fmt.Println("Boundary Crossing Optimization WebAssembly Module Initialized")
// Register JavaScript functions
js.Global().Set("createBatchProcessor", js.FuncOf(CreateBatchProcessor))
js.Global().Set("createDataTransferer", js.FuncOf(CreateDataTransferer))
// Keep the program running
<-make(chan struct{})
}
Performance Benchmarking: WASM vs JavaScript
Comparing WebAssembly and JavaScript performance helps identify where WASM provides the most benefit:
package main
import (
"fmt"
"math"
"syscall/js"
"time"
)
// BenchmarkResult represents the result of a benchmark
type BenchmarkResult struct {
Name string
ExecutionTimeMs float64
OperationsPerSec float64
}
// RunBenchmark runs a benchmark function multiple times and returns statistics
func RunBenchmark(name string, iterations int, fn func()) BenchmarkResult {
// Warm up
for i := 0; i < 5; i++ {
fn()
}
// Run benchmark
start := time.Now()
for i := 0; i < iterations; i++ {
fn()
}
elapsed := time.Since(start)
// Calculate statistics
executionTimeMs := float64(elapsed.Milliseconds()) / float64(iterations)
operationsPerSec := 1000.0 / executionTimeMs
return BenchmarkResult{
Name: name,
ExecutionTimeMs: executionTimeMs,
OperationsPerSec: operationsPerSec,
}
}
// CreateBenchmarkSuite creates a benchmark suite
func CreateBenchmarkSuite(this js.Value, args []js.Value) interface{} {
suite := js.Global().Get("Object").New()
// Benchmark: Matrix multiplication
suite.Set("runMatrixMultiplication", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
size := 100
iterations := 10
if len(args) > 0 && args[0].Type() == js.TypeNumber {
size = args[0].Int()
}
if len(args) > 1 && args[1].Type() == js.TypeNumber {
iterations = args[1].Int()
}
// Create matrices
matrixA := make([][]float64, size)
matrixB := make([][]float64, size)
matrixC := make([][]float64, size)
for i := 0; i < size; i++ {
matrixA[i] = make([]float64, size)
matrixB[i] = make([]float64, size)
matrixC[i] = make([]float64, size)
for j := 0; j < size; j++ {
matrixA[i][j] = float64(i + j)
matrixB[i][j] = float64(i - j)
}
}
// Run Go benchmark
goBenchmark := RunBenchmark("Go Matrix Multiplication", iterations, func() {
// Matrix multiplication
for i := 0; i < size; i++ {
for j := 0; j < size; j++ {
sum := 0.0
for k := 0; k < size; k++ {
sum += matrixA[i][k] * matrixB[k][j]
}
matrixC[i][j] = sum
}
}
})
// Create result object
result := js.Global().Get("Object").New()
result.Set("name", goBenchmark.Name)
result.Set("executionTimeMs", goBenchmark.ExecutionTimeMs)
result.Set("operationsPerSec", goBenchmark.OperationsPerSec)
return result
}))
// Benchmark: Fibonacci calculation
suite.Set("runFibonacci", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
n := 40
iterations := 10
if len(args) > 0 && args[0].Type() == js.TypeNumber {
n = args[0].Int()
}
if len(args) > 1 && args[1].Type() == js.TypeNumber {
iterations = args[1].Int()
}
// Run Go benchmark
goBenchmark := RunBenchmark("Go Fibonacci", iterations, func() {
fibonacci(n)
})
// Create result object
result := js.Global().Get("Object").New()
result.Set("name", goBenchmark.Name)
result.Set("executionTimeMs", goBenchmark.ExecutionTimeMs)
result.Set("operationsPerSec", goBenchmark.OperationsPerSec)
return result
}))
// Benchmark: Image processing
suite.Set("runImageProcessing", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
width := 1000
height := 1000
iterations := 5
if len(args) > 0 && args[0].Type() == js.TypeNumber {
width = args[0].Int()
}
if len(args) > 1 && args[1].Type() == js.TypeNumber {
height = args[1].Int()
}
if len(args) > 2 && args[2].Type() == js.TypeNumber {
iterations = args[2].Int()
}
// Create image data
imageData := make([]uint8, width * height * 4)
for i := 0; i < width*height*4; i += 4 {
imageData[i] = uint8(i % 256) // R
imageData[i+1] = uint8((i+85) % 256) // G
imageData[i+2] = uint8((i+170) % 256) // B
imageData[i+3] = 255 // A
}
// Run Go benchmark
goBenchmark := RunBenchmark("Go Image Processing", iterations, func() {
// Apply a simple blur filter
for y := 1; y < height-1; y++ {
for x := 1; x < width-1; x++ {
idx := (y*width + x) * 4
// Average with neighbors
for c := 0; c < 3; c++ {
sum := int(imageData[idx+c])
sum += int(imageData[idx-4+c]) // left
sum += int(imageData[idx+4+c]) // right
sum += int(imageData[idx-width*4+c]) // up
sum += int(imageData[idx+width*4+c]) // down
imageData[idx+c] = uint8(sum / 5)
}
}
}
})
// Create result object
result := js.Global().Get("Object").New()
result.Set("name", goBenchmark.Name)
result.Set("executionTimeMs", goBenchmark.ExecutionTimeMs)
result.Set("operationsPerSec", goBenchmark.OperationsPerSec)
return result
}))
return suite
}
// Recursive Fibonacci implementation
func fibonacci(n int) int {
if n <= 1 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}
func main() {
fmt.Println("Benchmark WebAssembly Module Initialized")
// Register JavaScript functions
js.Global().Set("createBenchmarkSuite", js.FuncOf(CreateBenchmarkSuite))
// Keep the program running
<-make(chan struct{})
}
Example JavaScript benchmark comparison:
// Create benchmark suite
const benchmarkSuite = createBenchmarkSuite();
// JavaScript matrix multiplication implementation
function jsMatrixMultiplication(size) {
// Create matrices
const matrixA = Array(size).fill().map((_, i) =>
Array(size).fill().map((_, j) => i + j));
const matrixB = Array(size).fill().map((_, i) =>
Array(size).fill().map((_, j) => i - j));
const matrixC = Array(size).fill().map(() => Array(size).fill(0));
// Matrix multiplication
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
let sum = 0;
for (let k = 0; k < size; k++) {
sum += matrixA[i][k] * matrixB[k][j];
}
matrixC[i][j] = sum;
}
}
return matrixC;
}
// JavaScript Fibonacci implementation
function jsFibonacci(n) {
if (n <= 1) return n;
return jsFibonacci(n - 1) + jsFibonacci(n - 2);
}
// Run benchmarks and compare
function runBenchmarks() {
// Matrix multiplication benchmark
console.log("Running matrix multiplication benchmark...");
const size = 100;
console.time("JS Matrix Multiplication");
jsMatrixMultiplication(size);
console.timeEnd("JS Matrix Multiplication");
const wasmMatrixResult = benchmarkSuite.runMatrixMultiplication(size);
console.log(`WASM Matrix Multiplication: ${wasmMatrixResult.executionTimeMs.toFixed(2)}ms`);
// Fibonacci benchmark
console.log("\nRunning Fibonacci benchmark...");
const n = 40;
console.time("JS Fibonacci");
jsFibonacci(n);
console.timeEnd("JS Fibonacci");
const wasmFibResult = benchmarkSuite.runFibonacci(n);
console.log(`WASM Fibonacci: ${wasmFibResult.executionTimeMs.toFixed(2)}ms`);
// Display performance comparison
document.getElementById("benchmark-results").innerHTML = `
<h3>Performance Comparison</h3>
<table>
<tr>
<th>Benchmark</th>
<th>JavaScript</th>
<th>WebAssembly</th>
<th>Speedup</th>
</tr>
<tr>
<td>Matrix Multiplication</td>
<td>${jsMatrixTime.toFixed(2)}ms</td>
<td>${wasmMatrixResult.executionTimeMs.toFixed(2)}ms</td>
<td>${(jsMatrixTime / wasmMatrixResult.executionTimeMs).toFixed(2)}x</td>
</tr>
<tr>
<td>Fibonacci(40)</td>
<td>${jsFibTime.toFixed(2)}ms</td>
<td>${wasmFibResult.executionTimeMs.toFixed(2)}ms</td>
<td>${(jsFibTime / wasmFibResult.executionTimeMs).toFixed(2)}x</td>
</tr>
</table>
`;
}
Production Deployment and Best Practices
Deploying Go WebAssembly applications to production environments requires careful consideration of several factors to ensure optimal performance, reliability, and user experience.
Binary Size Optimization
WebAssembly binaries can become large, especially with the Go runtime included. Here are techniques to reduce binary size:
// main.go
package main
import (
"syscall/js"
)
// Minimal Go WASM application
func main() {
js.Global().Set("goAdd", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) != 2 {
return "Error: Expected exactly 2 arguments"
}
a := args[0].Int()
b := args[1].Int()
return a + b
}))
// Keep the program running
select {}
}
Build with size optimization flags:
GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o main.wasm main.go
Further optimize with additional tools:
# Install wasm-opt
npm install -g wasm-opt
# Optimize the WebAssembly binary
wasm-opt -Oz -o optimized.wasm main.wasm
Loading Strategies
Efficient loading strategies improve user experience:
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Go WASM Application</title>
<script>
// Show loading indicator
document.addEventListener("DOMContentLoaded", function() {
document.getElementById("loading").style.display = "block";
});
// Progressive loading strategy
const wasmImport = {
// Staged loading with progress reporting
loadWasm: async function() {
// Check for WebAssembly support
if (!('WebAssembly' in window)) {
document.getElementById("error").textContent =
"WebAssembly is not supported in your browser";
document.getElementById("error").style.display = "block";
document.getElementById("loading").style.display = "none";
return;
}
try {
// Fetch WebAssembly dependencies
const goWasmExec = await fetch("wasm_exec.js");
const goWasmExecText = await goWasmExec.text();
eval(goWasmExecText);
// Initialize Go runtime
const go = new Go();
// Fetch and instantiate the WebAssembly module
const wasmResponse = await fetch("main.wasm");
const wasmBuffer = await wasmResponse.arrayBuffer();
const wasmInstance = await WebAssembly.instantiate(wasmBuffer, go.importObject);
// Execute the WebAssembly module
go.run(wasmInstance.instance);
// Hide loading indicator
document.getElementById("loading").style.display = "none";
document.getElementById("app").style.display = "block";
// Initialize application
initApp();
} catch (err) {
console.error("Failed to load WebAssembly module:", err);
document.getElementById("error").textContent =
"Failed to load WebAssembly module: " + err.message;
document.getElementById("error").style.display = "block";
document.getElementById("loading").style.display = "none";
}
}
};
// Start loading when the page is ready
document.addEventListener("DOMContentLoaded", wasmImport.loadWasm);
// Initialize application after WASM is loaded
function initApp() {
// Application initialization code
console.log("WebAssembly module loaded successfully");
// Test the exported function
const result = goAdd(21, 21);
document.getElementById("result").textContent = `21 + 21 = ${result}`;
}
</script>
</head>
<body>
<div id="loading" style="display: none;">
Loading WebAssembly module...
<div class="progress-bar">
<div class="progress"></div>
</div>
</div>
<div id="error" style="display: none; color: red;"></div>
<div id="app" style="display: none;">
<h1>Go WebAssembly Application</h1>
<p id="result"></p>
</div>
</body>
</html>
Cross-Browser Compatibility
Ensuring compatibility across browsers:
// browser-compatibility.js
function checkWasmSupport() {
const report = {
supported: false,
features: {
basic: false,
streaming: false,
threads: false,
simd: false,
exceptions: false,
tailCall: false,
referenceTypes: false,
multiValue: false,
bulkMemory: false,
simd128: false
}
};
// Check basic WebAssembly support
if (typeof WebAssembly === 'object') {
report.supported = true;
report.features.basic = true;
// Check for streaming compilation
report.features.streaming = typeof WebAssembly.instantiateStreaming === 'function';
// Check for threads
report.features.threads = typeof SharedArrayBuffer === 'function';
// Check for SIMD support
WebAssembly.compile(new Uint8Array([
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x05, 0x01,
0x60, 0x00, 0x01, 0x7b, 0x03, 0x02, 0x01, 0x00, 0x07, 0x08, 0x01,
0x04, 0x74, 0x65, 0x73, 0x74, 0x00, 0x00, 0x0a, 0x09, 0x01, 0x07,
0x00, 0xfd, 0x0f, 0x00, 0x00, 0x0b
])).then(() => {
report.features.simd = true;
}).catch(() => {
report.features.simd = false;
});
}
return report;
}
// Polyfill for browsers without streaming instantiation
if (!WebAssembly.instantiateStreaming) {
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer();
return await WebAssembly.instantiate(source, importObject);
};
}
// Load appropriate version based on browser capabilities
async function loadAppropriateWasmVersion() {
const support = checkWasmSupport();
if (!support.supported) {
// Load fallback JavaScript version
loadJavaScriptFallback();
return;
}
if (support.features.simd) {
// Load optimized SIMD version
loadWasmVersion('main-simd.wasm');
} else {
// Load standard version
loadWasmVersion('main.wasm');
}
}
Error Handling and Debugging
Effective error handling and debugging are crucial for WebAssembly applications:
package main
import (
"errors"
"fmt"
"syscall/js"
)
// ErrorResponse represents a structured error response
type ErrorResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Details string `json:"details,omitempty"`
}
// NewErrorResponse creates a new error response
func NewErrorResponse(code int, message string, details string) ErrorResponse {
return ErrorResponse{
Code: code,
Message: message,
Details: details,
}
}
// ToJSValue converts the error response to a JavaScript object
func (e ErrorResponse) ToJSValue() js.Value {
result := js.Global().Get("Object").New()
result.Set("code", e.Code)
result.Set("message", e.Message)
if e.Details != "" {
result.Set("details", e.Details)
}
return result
}
// ErrorHandler wraps a function with error handling
func ErrorHandler(fn func(js.Value, []js.Value) (interface{}, error)) js.Func {
return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
result, err := fn(this, args)
if err != nil {
// Log error on Go side
fmt.Printf("Error: %v\n", err)
// Convert to JavaScript error object
var errorResp ErrorResponse
// Check for custom error types
switch e := err.(type) {
case *ValidationError:
errorResp = NewErrorResponse(400, "Validation Error", e.Error())
case *BusinessLogicError:
errorResp = NewErrorResponse(500, "Business Logic Error", e.Error())
default:
errorResp = NewErrorResponse(500, "Internal Error", err.Error())
}
// Return structured error to JavaScript
return errorResp.ToJSValue()
}
return result
})
}
// Custom error types
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("Field '%s': %s", e.Field, e.Message)
}
type BusinessLogicError struct {
Operation string
Message string
}
func (e *BusinessLogicError) Error() string {
return fmt.Sprintf("Operation '%s' failed: %s", e.Operation, e.Message)
}
// Example function with error handling
func divide(this js.Value, args []js.Value) (interface{}, error) {
// Validate arguments
if len(args) < 2 {
return nil, &ValidationError{
Field: "arguments",
Message: "Expected 2 arguments",
}
}
// Extract values
a := args[0].Float()
b := args[1].Float()
// Business logic validation
if b == 0 {
return nil, &BusinessLogicError{
Operation: "division",
Message: "Cannot divide by zero",
}
}
// Perform operation
return a / b, nil
}
// Debug logging function
func setupDebugLogging() js.Func {
return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) < 2 {
return nil
}
level := args[0].String()
message := args[1].String()
// Additional context if provided
context := ""
if len(args) > 2 && args[2].Type() == js.TypeObject {
context = fmt.Sprintf(" - Context: %v", args[2])
}
// Log based on level
switch level {
case "debug":
fmt.Printf("[DEBUG] %s%s\n", message, context)
case "info":
fmt.Printf("[INFO] %s%s\n", message, context)
case "warn":
fmt.Printf("[WARN] %s%s\n", message, context)
case "error":
fmt.Printf("[ERROR] %s%s\n", message, context)
}
return nil
})
}
// Setup console redirection for debugging
func setupConsoleRedirection() {
js.Global().Get("console").Set("goLog", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) > 0 {
fmt.Println(args[0].String())
}
return nil
}))
// Inject JavaScript to redirect console.log
js.Global().Get("eval").Invoke(`
(function() {
const originalLog = console.log;
console.log = function(...args) {
if (typeof goLog === 'function') {
goLog(args.map(arg => String(arg)).join(' '));
}
return originalLog.apply(console, args);
};
})();
`)
}
func main() {
fmt.Println("Initializing WebAssembly module with error handling")
// Set up error handling and debugging
js.Global().Set("goDivide", ErrorHandler(divide))
js.Global().Set("goDebugLog", setupDebugLogging())
setupConsoleRedirection()
// Register stack trace capture
js.Global().Set("captureGoStackTrace", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
// This is a simplified version - in a real app you'd use runtime.Stack
return "Go Stack Trace: main.main -> main.captureGoStackTrace"
}))
// Keep the program running
<-make(chan struct{})
}
JavaScript side for error handling:
// Error handling in JavaScript
function handleGoFunction(goFunc, ...args) {
try {
const result = goFunc(...args);
// Check if result is an error object
if (result && typeof result === 'object' && result.code !== undefined) {
console.error(`Go error (${result.code}): ${result.message}`);
if (result.details) {
console.error(`Details: ${result.details}`);
}
// Display error to user
showError(result.message);
return null;
}
return result;
} catch (jsError) {
console.error("JavaScript error:", jsError);
// Capture Go stack trace if available
if (typeof captureGoStackTrace === 'function') {
const goStack = captureGoStackTrace();
console.error("Go stack trace:", goStack);
}
// Display error to user
showError("An unexpected error occurred");
return null;
}
}
// Example usage
function calculateAndDisplay() {
const a = parseFloat(document.getElementById("valueA").value);
const b = parseFloat(document.getElementById("valueB").value);
// Use the error handling wrapper
const result = handleGoFunction(goDivide, a, b);
if (result !== null) {
document.getElementById("result").textContent = `Result: ${result}`;
}
}
// Debug logging
function logWithContext(level, message, context) {
// Log to browser console
console[level](message, context);
// Also log to Go side
if (typeof goDebugLog === 'function') {
goDebugLog(level, message, context);
}
}
// Example debug usage
logWithContext('info', 'Application initialized', { timestamp: Date.now() });
logWithContext('debug', 'Rendering component', { component: 'Calculator' });
Conclusion
Go WebAssembly represents a significant advancement in web application development, offering a powerful alternative to JavaScript for performance-critical components. Throughout this article, we’ve explored the complete lifecycle of Go WASM applications—from compilation to production deployment.
The ability to compile Go code to WebAssembly opens new possibilities for web developers. Computationally intensive tasks that would typically struggle in JavaScript can now leverage Go’s performance characteristics while running directly in the browser. This capability is particularly valuable for applications involving complex calculations, data processing, graphics rendering, or any scenario where performance is paramount.
We’ve seen how Go’s strong type system and concurrency model can be effectively utilized in browser environments through WebAssembly. The interoperability patterns we’ve explored demonstrate that Go and JavaScript can work together seamlessly, combining their respective strengths. JavaScript handles DOM manipulation and user interface concerns, while Go manages complex business logic and performance-critical operations.
As WebAssembly continues to evolve with new features like interface types, garbage collection, and threading, the integration between Go and browsers will become even more powerful and streamlined. The current need for JavaScript glue code will diminish, and the development experience will improve further.
For developers looking to push the boundaries of web application performance, Go WebAssembly provides a compelling path forward. By following the patterns, optimization techniques, and best practices outlined in this article, you can harness the full potential of this technology combination to deliver exceptional user experiences that were previously unattainable in browser environments.
The future of web development increasingly includes multiple languages working in concert, each applied where its strengths provide the most benefit. Go WebAssembly stands as an excellent example of this multi-language approach, bringing Go’s performance and reliability to the ubiquitous platform of the web browser.