Go puede compilar a WebAssembly desde la versión 1.11. El binario resultante corre en el navegador con acceso al DOM y a las APIs JavaScript a través del paquete syscall/js. Es una opción real cuando necesitas reutilizar lógica de negocio Go en el frontend sin reescribirla en JavaScript.
Compilar a WebAssembly
GOOS=js GOARCH=wasm go build -o main.wasm main.go
Go incluye el fichero JavaScript necesario para cargar el runtime. Cópialo al directorio web:
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
Integrar en HTML
<!DOCTYPE html>
<html>
<head>
<script src="wasm_exec.js"></script>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject)
.then(result => go.run(result.instance));
</script>
</head>
<body>
<div id="resultado"></div>
</body>
</html>
Manipular el DOM con syscall/js
package main
import (
"fmt"
"syscall/js"
)
func main() {
doc := js.Global().Get("document")
// Leer un elemento del DOM
input := doc.Call("getElementById", "entrada")
valor := input.Get("value").String()
// Crear un elemento y añadirlo al DOM
p := doc.Call("createElement", "p")
p.Set("textContent", "Procesado por Go: "+valor)
contenedor := doc.Call("getElementById", "resultado")
contenedor.Call("appendChild", p)
// Leer y modificar estilos
contenedor.Get("style").Set("color", "blue")
// Llamar a una función JavaScript
js.Global().Call("console.log", "WASM listo")
// Mantener el programa vivo
select {}
}
Exponer funciones Go a JavaScript
El patrón más útil es registrar funciones Go como propiedades del objeto global para que JavaScript las pueda llamar:
package main
import (
"encoding/json"
"syscall/js"
)
type Resultado struct {
Valor float64 `json:"valor"`
Formato string `json:"formato"`
}
func calcularIVA(this js.Value, args []js.Value) any {
if len(args) < 2 {
return js.ValueOf("error: se necesitan base e IVA")
}
base := args[0].Float()
iva := args[1].Float()
total := base * (1 + iva/100)
r := Resultado{
Valor: total,
Formato: fmt.Sprintf("%.2f ", total),
}
b, _ := json.Marshal(r)
return js.ValueOf(string(b))
}
func validarEmail(this js.Value, args []js.Value) any {
if len(args) == 0 {
return js.ValueOf(false)
}
email := args[0].String()
// lógica de validación...
valido := len(email) > 5 && strings.Contains(email, "@")
return js.ValueOf(valido)
}
func main() {
js.Global().Set("goCalcularIVA", js.FuncOf(calcularIVA))
js.Global().Set("goValidarEmail", js.FuncOf(validarEmail))
fmt.Println("Funciones Go registradas en JavaScript")
select {} // mantener el runtime activo
}
Desde JavaScript llamas a esas funciones como cualquier otra:
const resultado = JSON.parse(goCalcularIVA(100, 21));
console.log(resultado.formato); // "121.00 "
const ok = goValidarEmail("[email protected]");
console.log(ok); // true
Callback de JavaScript a Go
func manejarClick(this js.Value, args []js.Value) any {
evento := args[0]
target := evento.Get("target")
id := target.Get("id").String()
fmt.Println("Click en elemento:", id)
return nil
}
boton := js.Global().Get("document").Call("getElementById", "mi-boton")
cb := js.FuncOf(manejarClick)
defer cb.Release() // importante: liberar para no tener memory leaks
boton.Call("addEventListener", "click", cb)
TinyGo: reducir el tamaño del binario
Un binario WASM compilado con el compilador estándar de Go ocupa varios megabytes por el runtime incluido. TinyGo produce binarios mucho más pequeños:
tinygo build -o main.wasm -target wasm main.go
La limitación de TinyGo es que no soporta todo el ecosistema de paquetes de Go, especialmente los que dependen de reflexión intensiva. Para lógica pura sin dependencias grandes funciona perfectamente.
