Building Your First Application
There’s something magical about seeing your Go code run in a browser for the first time. I still remember the excitement when my first WebAssembly app loaded and actually worked. But I also remember the confusion - the development workflow felt different, and debugging was unlike anything I’d done before.
Let’s build a simple calculator that demonstrates the core patterns you’ll use in every WebAssembly project. It’s not glamorous, but it teaches the fundamentals without getting lost in complexity.
Setting Up the Project
I always start WebAssembly projects with a clear structure. You’ll have Go code, HTML files, JavaScript glue, and build artifacts. Keeping these organized saves headaches later:
calculator/
├── main.go
├── index.html
├── main.js
├── Makefile
└── README.md
This structure separates concerns cleanly. The Go code handles computation, HTML provides the interface, JavaScript manages the integration, and the Makefile automates everything.
The Go Side
Here’s the calculator’s Go code. Notice how different it feels from a typical Go program:
package main
import (
"fmt"
"strconv"
"syscall/js"
)
func add(this js.Value, args []js.Value) interface{} {
if len(args) != 2 {
return "Error: need exactly 2 numbers"
}
a, err1 := strconv.ParseFloat(args[0].String(), 64)
b, err2 := strconv.ParseFloat(args[1].String(), 64)
if err1 != nil || err2 != nil {
return "Error: invalid numbers"
}
return a + b
}
func main() {
js.Global().Set("goAdd", js.FuncOf(add))
fmt.Println("Calculator loaded")
select {} // Keep running
}
The key differences from normal Go: we’re using syscall/js
to interact with JavaScript, our functions have a specific signature that JavaScript can call, and we use select {}
to keep the program alive.
The HTML Interface
The HTML is straightforward, but notice how it loads both the WebAssembly support and our module:
<!DOCTYPE html>
<html>
<head>
<title>Go WebAssembly Calculator</title>
</head>
<body>
<h1>Calculator</h1>
<input type="number" id="num1" placeholder="First number">
<input type="number" id="num2" placeholder="Second number">
<button onclick="calculate()">Add</button>
<div id="result"></div>
<div id="status">Loading...</div>
<script src="wasm_exec.js"></script>
<script src="main.js"></script>
</body>
</html>
The order matters here. We load wasm_exec.js
first (the Go runtime support), then our own JavaScript that handles the WebAssembly loading.
JavaScript Integration
This is where the magic happens - loading the WebAssembly module and connecting it to the UI:
// main.js
let wasmReady = false;
async function loadWasm() {
const go = new Go();
try {
const result = await WebAssembly.instantiateStreaming(
fetch("main.wasm"),
go.importObject
);
go.run(result.instance);
wasmReady = true;
document.getElementById('status').textContent = 'Ready!';
} catch (error) {
console.error('Failed to load WebAssembly:', error);
document.getElementById('status').textContent = 'Failed to load';
}
}
function calculate() {
if (!wasmReady) {
alert('WebAssembly not ready yet');
return;
}
const num1 = document.getElementById('num1').value;
const num2 = document.getElementById('num2').value;
const result = window.goAdd(num1, num2);
document.getElementById('result').textContent = `Result: ${result}`;
}
// Load WebAssembly when page loads
document.addEventListener('DOMContentLoaded', loadWasm);
This JavaScript handles the asynchronous loading of WebAssembly, provides user feedback during loading, and bridges between the HTML interface and Go functions.
Build Automation
Manual compilation gets old fast. Here’s the Makefile I use:
.PHONY: build clean serve
build:
GOOS=js GOARCH=wasm go build -o main.wasm .
cp "$$(go env GOROOT)/misc/wasm/wasm_exec.js" .
clean:
rm -f main.wasm wasm_exec.js
serve: build
@echo "Starting server on http://localhost:8080"
python3 -m http.server 8080
Run make serve
and you have a working calculator running in your browser. The Python server is important - WebAssembly won’t load from file://
URLs due to CORS restrictions.
Common Gotchas
I’ve made every mistake possible with WebAssembly, so let me save you some time:
CORS Issues: Always use a web server, even for development. Opening HTML files directly won’t work.
Error Handling: JavaScript errors don’t automatically show up in Go, and Go panics can crash your entire WebAssembly module. Handle errors explicitly on both sides.
Function Signatures: WebAssembly functions must have the exact signature func(this js.Value, args []js.Value) interface{}
. Get this wrong and nothing works.
Memory Management: Go’s garbage collector runs in WebAssembly, but the interaction with JavaScript’s memory model can be tricky. For simple apps like this calculator, it’s not a concern.
Development Workflow
Here’s the workflow I use for WebAssembly development:
- Write and test Go logic with regular Go tests
- Add WebAssembly bindings and compile
- Test in browser with simple HTML interface
- Iterate on both Go and JavaScript sides
- Add error handling and edge cases
The key insight is that you can test most of your Go logic with normal Go tests before adding WebAssembly complexity. Only the browser integration needs to be tested in a browser.
Performance Considerations
Even this simple calculator demonstrates WebAssembly’s characteristics. The initial load includes downloading and instantiating a 2MB WebAssembly module. For two simple additions, that’s overkill.
But imagine if instead of addition, you were doing complex mathematical operations on large datasets. The startup cost becomes negligible compared to the performance benefits.
This is the WebAssembly trade-off: higher startup cost, but much better performance for sustained computational work.
Debugging Tips
When things go wrong (and they will), here’s how to debug:
- Check the browser console for JavaScript errors
- Use
fmt.Println()
in Go - it shows up in the console - Test Go functions independently before adding WebAssembly bindings
- Verify the WebAssembly module loads successfully before calling functions
The debugging experience isn’t as smooth as native Go development, but it’s workable once you know the patterns.
Expanding the Calculator
This basic calculator demonstrates all the fundamental patterns: Go functions exposed to JavaScript, error handling across the boundary, and asynchronous WebAssembly loading.
You could extend it with more operations, better error handling, or a more sophisticated UI. The patterns remain the same - Go handles computation, JavaScript manages the interface.
What’s Next
This calculator might seem simple, but it contains every pattern you’ll use in complex WebAssembly applications. The next part explores how to structure larger applications with better architecture and more sophisticated patterns.
We’ll look at organizing code for maintainability, handling complex data types, and implementing patterns that scale beyond simple function calls.