Go y WebAssembly: compilar a WASM, syscall/js e integrar en el navegador

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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP