Artículo
originalEn febrero de 2017, un miembro del equipo de Brad Fitzpatrick
propuso hacer que WebAssembly sea compatible con el idioma. Cuatro meses después, en noviembre de 2017, el autor de GopherJS Richard Muziol comenzó a implementar la idea. Y finalmente, la implementación completa se encontró en master. Los desarrolladores recibirán wasm alrededor de agosto de 2018, con la versión
1.11 de go . Como resultado, la biblioteca estándar asume casi todas las dificultades técnicas para importar y exportar funciones que le son familiares si ya ha intentado compilar C en wasm. Eso suena prometedor. Veamos qué se puede hacer con la primera versión.
Todos los ejemplos en este artículo se pueden iniciar desde los contenedores acoplables que se encuentran
en el repositorio del autor :
docker container run -dP nlepage/golang_wasm:examples
Luego vaya a
localhost : 32XXX /, y vaya de un enlace a otro.
Hola wasm
La creación del "mundo hola" básico y el concepto ya están bastante
bien documentados (incluso
en ruso ), así que pasemos a las cosas más sutiles.
Lo más esencial es una versión recién compilada de Go que admita wasm. No
describiré paso a paso la instalación , solo sé que lo que se necesita ya está en el maestro.
Si no quiere preocuparse por esto,
Dockerfile c go está disponible en
el repositorio golub-wasm en github , o puede tomar una imagen de
nlepage / golang_wasm aún más rápido.
Ahora puede escribir el
helloworld.go
tradicional y compilarlo con el siguiente comando:
GOOS=js GOARCH=wasm go build -o test.wasm helioworld.go
Las variables de entorno GOOS y GOARCH ya están configuradas en la imagen
nlepage / golang_wasm , por lo que puede usar un archivo
Dockerfile
como este para compilar:
FROM nlepage/golang_wasm COPY helloworld.go /go/src/hello/ RUN go build -o test.wasm hello
El último paso es usar los archivos
wasm_exec.html
y
wasm_exec.js
disponibles en el repositorio go en el
misc/wasm
o en la imagen del
acoplador nlepage / golang_wasm en el
directorio /usr/local/go/misc/wasm/
para ejecutar
test.wasm
en navegador (wasm_exec.js espera el archivo binario
test.wasm
, así que usamos este nombre).
Solo necesita dar 3 archivos estáticos usando nginx, por ejemplo, luego wasm_exec.html mostrará el botón "ejecutar" (se
test.wasm
solo si
test.wasm
carga correctamente).
Es de destacar que
test.wasm
debe servirse con la
application/wasm
tipo MIME, de lo contrario el navegador se negará a ejecutarlo. (por ejemplo, nginx necesita un
archivo mime.types actualizado ).
Puede usar la imagen nginx de
nlepage / golang_wasm , que ya incluye el tipo MIME fijo,
wasm_exec.html
y
wasm_exec.js
en el
wasm_exec.js
de código> / usr / share / nginx / html /.
Ahora haga clic en el botón "Ejecutar", luego abra la consola de su navegador y verá el saludo console.log ("¡Hola Wasm!").
Un ejemplo completo está disponible
aquí .
Llama a JS desde Go
Ahora que hemos lanzado con éxito el primer binario WebAssembly compilado desde Go, echemos un vistazo más de cerca a las características proporcionadas.
El nuevo paquete syscall / js se ha agregado a la biblioteca estándar. Considere el archivo principal,
js.go
Está
js.Value
un nuevo tipo
js.Value
que representa un valor de JavaScript.
Ofrece una API simple para administrar variables de JavaScript:
js.Value.Get()
y js.Value.Set()
devuelven y establecen los valores de campo del objeto.js.Value.Index()
y js.Value.SetIndex()
acceden al objeto mediante el índice de lectura y escritura.js.Value.Call()
llama al método objeto como una función.js.Value.Invoke()
llama al objeto en sí mismo como una función.js.Value.New()
llama al nuevo operador y utiliza su propio conocimiento como constructor.- Algunos métodos más para obtener el valor de JavaScript en el tipo Go correspondiente, por ejemplo
js.Value.Int()
o js.Value.Bool()
.
Y métodos interesantes adicionales:
js.Undefined()
le dará a js.Value el correspondiente undefined
.js.Null()
le dará a js.Value
null
correspondiente.js.Global()
devolverá js.Value
dando acceso al alcance global.js.ValueOf()
acepta tipos Go primitivos y devuelve el js.Value
correcto
En lugar de mostrar el mensaje en os.StdOut, vamos a mostrarlo en la ventana de notificación usando
window.alert()
.
Dado que estamos en el navegador, el alcance global es una ventana, por lo que primero debe obtener una alerta () del alcance global:
alert := js.Global().Get("alert")
Ahora tenemos una variable de
alert
, en forma de
js.Value
, que es una referencia a
window.alert
JS, y puede usar la función para llamar a través de
js.Value.Invoke()
:
alert.Invoke("Hello wasm!")
Como puede ver, no es necesario llamar a js.ValueOf () antes de pasar los argumentos a Invoke, toma una cantidad arbitraria de
interface{}
y pasa los valores a través de ValueOf.
Ahora nuestro nuevo programa debería verse así:
package main import ( "syscall/js" ) func main() { alert := js.Global().Get("alert") alert.Invoke("Hello Wasm!") }
Como en el primer ejemplo, solo necesita crear un archivo llamado
test.wasm
, y dejar
wasm_exec.html
y
wasm_exec.js
como estaban.
Ahora, cuando hacemos clic en el botón "Ejecutar", aparece una ventana de alerta con nuestro mensaje.
Un ejemplo de trabajo está en la carpeta
examples/js-call
.
Llame Go desde JS.
Llamar a JS desde Go es bastante simple, echemos un vistazo más de cerca al paquete
syscall/js
, el segundo archivo para ver es
callback.go
.
js.Callback
contenedor js.Callback
para la función Go, para usar en JS.js.NewCallback()
función que toma una función (acepta una porción de js.Value
y no devuelve nada), y devuelve js.Callback
.- Algunas mecánicas para administrar devoluciones de llamada activas y
js.Callback.Release()
, que deben js.Callback.Release()
para destruir la devolución de llamada. js.NewEventCallback()
similar a js.NewCallback()
, pero la función envuelta acepta solo 1 argumento: un evento.
Intentemos hacer algo simple: ejecutar Go
fmt.Println()
desde el lado JS.
Haremos algunos cambios en
wasm_exec.html
para poder recibir una devolución de llamada de Ir para llamarlo.
async function run() { console.clear(); await go.run(inst); inst = await WebAssembly.instantiate(mod, go.ImportObject);
Esto inicia el wasm binario y espera a que se complete, luego lo reinicializa para la próxima ejecución.
Agreguemos una nueva función que recibirá y guardará la devolución de llamada Go y cambiará el estado de
Promise
al finalizar:
let printMessage
Ahora adaptemos la función
run()
para usar la devolución de llamada:
async function run() { console.clear()
¡Y esto está del lado de JS!
Ahora, en la parte Ir, debe crear una devolución de llamada, enviarla al lado JS y esperar a que se necesite la función.
var done = make(chan struct{})
Luego deberían escribir la función real
printMessage()
:
func printMessage(args []js.Value) { message := args[0].Strlng() fmt.Println(message) done <- struct{}{}
Los argumentos se pasan a través del segmento
[]js.Value
, por lo que debe llamar a
js.Value.String()
en el primer elemento de segmento para obtener el mensaje en la línea Go.
Ahora podemos ajustar esta función en una devolución de llamada:
callback := js.NewCallback(printMessage) defer callback.Release()
Luego llame a la función JS
setPrintMessage()
, al igual que llamando a
window.alert()
:
setPrintMessage := js.Global.Get("setPrintMessage") setPrintMessage.Invoke(callback)
Lo último que debe hacer es esperar a que se llame a la devolución de llamada en main:
<-done
Esta última parte es importante porque las devoluciones de llamada se ejecutan en una rutina rutinaria dedicada, y la rutina principal debe esperar a que se llame la devolución de llamada, de lo contrario, el binario wasm se detendrá prematuramente.
El programa Go resultante debería tener este aspecto:
package main import ( "fmt" "syscall/js" ) var done = make(chan struct{}) func main() { callback := js.NewCallback(prtntMessage) defer callback.Release() setPrintMessage := js.Global().Get("setPrintMessage") setPrIntMessage.Invoke(callback) <-done } func printMessage(args []js.Value) { message := args[0].Strlng() fmt.PrintIn(message) done <- struct{}{} }
Como en los ejemplos anteriores, cree un archivo llamado
test.wasm
. También necesitamos reemplazar
wasm_exec.html
con nuestra versión, y
wasm_exec.js
reutilizar
wasm_exec.js
.
Ahora, cuando presiona el botón "Ejecutar", como en nuestro primer ejemplo, el mensaje se imprime en la consola del navegador, ¡pero esta vez es mucho mejor! (Y más duro)
Un ejemplo de trabajo en una oferta de archivo acoplable está disponible en la carpeta
examples/go-call
.
Largo trabajo
Llamar a Go desde JS es un poco más engorroso que llamar a JS desde Go, especialmente en el lado de JS.
Esto se debe principalmente al hecho de que debe esperar hasta que el resultado de la devolución de llamada Go se pase al lado JS.
Intentemos algo más: por qué no organizar el wasm binario, que no terminará justo después de la llamada de devolución de llamada, sino que continuará funcionando y aceptará otras llamadas.
Esta vez, comencemos desde el lado Go, y como en nuestro ejemplo anterior, necesitamos crear una devolución de llamada y enviarla al lado JS.
Agregue un contador de llamadas para rastrear cuántas veces se ha llamado a la función.
Nuestra nueva función
printMessage()
imprimirá el mensaje recibido y el valor del contador:
var no int func printMessage(args []js.Value) { message := args[0].String() no++ fmt.Printf("Message no %d: %s\n", no, message) }
Crear una devolución de llamada y enviarla al lado JS es lo mismo que en el ejemplo anterior:
callback := js.NewCallback(printMessage) defer callback.Release() setPrintMessage := js.Global().Get("setPrintMessage") setPrIntMessage.Invoke(callback)
Pero esta vez no hemos
done
ningún canal para notificarnos la finalización de la rutina principal. Una forma podría ser bloquear permanentemente el goroutin principal con la
select{}
vacía
select{}
:
select{}
Esto no es satisfactorio, nuestro wasm binario se quedará en la memoria hasta que se cierre la pestaña del navegador.
Puede escuchar el evento antes de
beforeunload
en la página, necesitará una segunda devolución de llamada para recibir el evento y notificar a la goroutina principal a través del canal:
var beforeUnloadCh = make(chan struct{})
Esta vez, la nueva función
beforeUnload()
solo aceptará el evento, como un solo argumento
js.Value
:
func beforeUnload(event js.Value) { beforeUnloadCh <- struct{}{} }
Luego envuélvalo en una devolución de llamada usando
js.NewEventCallback()
y regístrelo en el lado JS:
beforeUnloadCb := js.NewEventCallback(0, beforeUnload) defer beforeUnloadCb.Release() addEventLtstener := js.Global().Get("addEventListener") addEventListener.Invoke("beforeunload", beforeUnloadCb)
Finalmente, reemplace la
select
bloqueo vacía con lectura del canal
beforeUnloadCh
:
<-beforeUnloadCh fmt.Prtntln("Bye Wasm!")
El programa final se ve así:
package main import ( "fmt" "syscall/js" ) var ( no int beforeUnloadCh = make(chan struct{}) ) func main() { callback := js.NewCallback(printMessage) defer callback.Release() setPrintMessage := js.Global().Get("setPrintMessage") setPrIntMessage.Invoke(callback) beforeUnloadCb := js.NewEventCallback(0, beforeUnload) defer beforeUnloadCb.Release() addEventLtstener := js.Global().Get("addEventListener") addEventListener.Invoke("beforeunload", beforeUnloadCb) <-beforeUnloadCh fmt.Prtntln("Bye Wasm!") } func printMessage(args []js.Value) { message := args[0].String() no++ fmt.Prtntf("Message no %d: %s\n", no, message) } func beforeUnload(event js.Value) { beforeUnloadCh <- struct{}{} }
Anteriormente, en el lado de JS, la descarga del binario wasm se veía así:
const go = new Go() let mod, inst WebAssembly .instantiateStreaming(fetch("test.wasm"), go.importObject) .then((result) => { mod = result.module inst = result.Instance document.getElementById("runButton").disabled = false })
Vamos a adaptarlo para ejecutar el binario inmediatamente después de cargar:
(async function() { const go = new Go() const { instance } = await WebAssembly.instantiateStreaming( fetch("test.wasm"), go.importObject ) go.run(instance) })()
Y reemplace el botón "Ejecutar" con un campo de mensaje y un botón para llamar a
printMessage()
:
<input id="messageInput" type="text" value="Hello Wasm!"> <button onClick="printMessage(document.querySelector('#messagelnput').value);" id="prtntMessageButton" disabled> Print message </button>
Finalmente, la función
setPrintMessage()
, que acepta y almacena la devolución de llamada, debería ser más simple:
let printMessage; function setPrintMessage(callback) { printMessage = callback; document.querySelector('#printMessageButton').disabled = false; }
Ahora, cuando hacemos clic en el botón "Imprimir mensaje", debería ver un mensaje de nuestra elección y un contador de llamadas impreso en la consola del navegador.
Si marcamos la casilla Conservar registro de la consola del navegador y actualizamos la página, veremos el mensaje "¡Adiós Wasm!".
Las fuentes están disponibles en la carpeta
examples/long-running
en github.
Y entonces?
Como puede ver, la API
syscall/js
aprendida hace su trabajo y le permite escribir cosas complejas con un poco de código. Puede escribir al
autor si conoce un método más simple.
Actualmente no es posible devolver un valor a JS directamente desde la devolución de llamada Go.
Tenga en cuenta que todas las devoluciones de llamada se realizan en el mismo goroutin, por lo que si realiza algunas operaciones de bloqueo en la devolución de llamada, no olvide crear un nuevo goroutin, de lo contrario, bloqueará la ejecución de todas las demás devoluciones de llamada.
Todas las funciones básicas del lenguaje ya están disponibles, incluida la concurrencia. Por ahora, todas las goroutinas funcionarán en un hilo, pero esto
cambiará en el futuro .
En nuestros ejemplos, utilizamos solo el paquete fmt de la biblioteca estándar, pero todo está disponible que no intente escapar del entorno limitado.
El sistema de archivos parece ser compatible a través de Node.js.
Finalmente, ¿qué pasa con el rendimiento? Sería interesante ejecutar algunas pruebas para ver cómo Go wasm se compara con el código JS puro equivalente. Alguien
hajimehoshi hizo mediciones de cómo funcionan los diferentes entornos con enteros, pero la técnica no es muy clara.
No olvide que Go 1.11 aún no se ha lanzado oficialmente. En mi opinión, es muy bueno para la tecnología experimental. Quienes estén interesados en las pruebas de rendimiento
pueden atormentar su navegador .
El nicho principal, como señala el autor, es la transferencia del servidor al cliente del código go existente. Pero con los nuevos estándares, puede crear
aplicaciones completamente fuera de línea , y el código wasm se guarda en forma compilada. Puede transferir muchas utilidades a la web, ¿está de acuerdo, convenientemente?