Artigo
originalEm fevereiro de 2017, um membro da equipe do go Brad Fitzpatrick
propôs dar suporte ao WebAssembly no idioma. Quatro meses depois, em novembro de 2017, o autor do
GopherJS , Richard Muziol, começou a implementar a ideia. E, finalmente, a implementação completa foi encontrada no master. Os desenvolvedores receberão o wasm por volta de agosto de 2018, com a versão
1.11 . Como resultado, a biblioteca padrão assume quase todas as dificuldades técnicas na importação e exportação de funções familiares para você, se você já tentou compilar C no wasm. Isso parece promissor. Vamos ver o que pode ser feito com a primeira versão.
Todos os exemplos deste artigo podem ser iniciados a partir de contêineres do docker que estão
no repositório do autor :
docker container run -dP nlepage/golang_wasm:examples
Em seguida, vá para
localhost : 32XXX / e vá de um link para outro.
Oi Wasm!
A criação do “olá mundo” básico e o conceito já estão
bem documentados (mesmo
em russo ), então vamos passar para as coisas mais sutis.
O mais essencial é uma versão recém-compilada do Go que suporta wasm. Não
descreverei passo a passo a instalação , apenas sei que o que é necessário já está no mestre.
Se você não quer se preocupar com isso, o
Dockerfile c go está disponível no
repositório golub-wasm no github , ou você pode tirar uma imagem do
nlepage / golang_wasm ainda mais rapidamente.
Agora você pode escrever o
helloworld.go
tradicional e compilá-lo com o seguinte comando:
GOOS=js GOARCH=wasm go build -o test.wasm helioworld.go
As variáveis de ambiente GOOS e GOARCH já estão definidas na imagem
nlepage / golang_wasm , para que você possa usar um arquivo
Dockerfile
como este para compilar:
FROM nlepage/golang_wasm COPY helloworld.go /go/src/hello/ RUN go build -o test.wasm hello
A última etapa é usar os arquivos
wasm_exec.js
e
wasm_exec.js
disponíveis no repositório go no
misc/wasm
ou na imagem do docker
nlepage / golang_wasm no
/usr/local/go/misc/wasm/
para executar
test.wasm
em browser (wasm_exec.js espera o arquivo binário
test.wasm
, portanto, usamos esse nome).
Você só precisa fornecer 3 arquivos estáticos usando o nginx, por exemplo, então wasm_exec.html exibirá o botão "executar" (ele será
test.wasm
apenas se o
test.wasm
carregado corretamente).
Vale ressaltar que
test.wasm
deve ser servido com o
application/wasm
tipo MIME, caso contrário, o navegador se recusará a executá-lo. (por exemplo, nginx precisa de um
arquivo mime.types atualizado ).
Você pode usar a imagem nginx do
nlepage / golang_wasm , que já inclui o tipo MIME fixo,
wasm_exec.html
e
wasm_exec.js
no diretório> / usr / share / nginx / html /.
Agora clique no botão “executar”, abra o console do navegador e você verá a saudação console.log (“Hello Wasm!”).
Um exemplo completo está disponível
aqui .
Ligar para JS de Go
Agora que lançamos com sucesso o primeiro binário do WebAssembly compilado a partir do Go, vamos dar uma olhada nos recursos fornecidos.
O novo pacote syscall / js foi adicionado à biblioteca padrão. Considere o arquivo principal,
js.go
Está
js.Value
um novo tipo
js.Value
que representa um valor JavaScript.
Ele oferece uma API simples para gerenciar variáveis JavaScript:
js.Value.Get()
e js.Value.Set()
retornam e definem os valores do campo do objeto.js.Value.Index()
e js.Value.SetIndex()
acessam o objeto pelo índice de leitura e gravação.js.Value.Call()
chama o método de objeto como uma função.js.Value.Invoke()
chama o próprio objeto como uma função.js.Value.New()
chama o novo operador e usa seu próprio conhecimento como construtor.- Mais alguns métodos para obter o valor JavaScript no tipo Go correspondente, por exemplo
js.Value.Int()
ou js.Value.Bool()
.
E métodos interessantes adicionais:
js.Undefined()
fornecerá a js.Value o valor undefined
correspondente.js.Null()
dará a js.Value
null
correspondente.js.Global()
retornará js.Value
dando acesso ao escopo global.js.ValueOf()
aceita tipos Go primitivos e retorna o js.Value
correto
Em vez de exibir a mensagem no os.StdOut, vamos exibi-la na janela de notificação usando
window.alert()
.
Como estamos no navegador, o escopo global é uma janela; primeiro, você precisa receber alert () do escopo global:
alert := js.Global().Get("alert")
Agora, temos uma variável de
alert
, na forma de
js.Value
, que é uma referência ao
window.alert
JS, e você pode usar a função para chamar
js.Value.Invoke()
:
alert.Invoke("Hello wasm!")
Como você pode ver, não há necessidade de chamar js.ValueOf () antes de passar os argumentos para Invoke, é necessária uma quantidade arbitrária de
interface{}
e passa os valores pelo próprio ValueOf.
Agora, nosso novo programa deve ficar assim:
package main import ( "syscall/js" ) func main() { alert := js.Global().Get("alert") alert.Invoke("Hello Wasm!") }
Como no primeiro exemplo, você só precisa criar um arquivo chamado
test.wasm
e deixar
wasm_exec.html
e
wasm_exec.js
como estavam.
Agora, quando clicamos no botão "Executar", uma janela de alerta aparece com a nossa mensagem.
Um exemplo de trabalho está na pasta
examples/js-call
.
Chame Go from JS.
Chamar JS a partir do Go é bem simples, vamos dar uma olhada no pacote
syscall/js
, o segundo arquivo a ser visualizado é
callback.go
.
js.Callback
tipo de wrapper para a função Go, para uso em JS.js.NewCallback()
função que aceita uma função (aceita uma fatia de js.Value
e não retorna nada) e retorna js.Callback
.- Algumas mecânicas para gerenciar retornos de chamada ativos e
js.Callback.Release()
, que devem ser chamados para destruir o retorno de chamada. js.NewEventCallback()
semelhante a js.NewCallback()
, mas a função js.NewCallback()
aceita apenas 1 argumento - um evento.
Vamos tentar fazer algo simples: execute Go
fmt.Println()
no lado JS.
Faremos algumas alterações no
wasm_exec.html
para obter um retorno de chamada de Go para chamá-lo.
async function run() { console.clear(); await go.run(inst); inst = await WebAssembly.instantiate(mod, go.ImportObject);
Isso inicia o wasm binário e aguarda a conclusão, e reinicializa-o para a próxima execução.
Vamos adicionar uma nova função que receba e salve o retorno de chamada Go e altere o estado da
Promise
após a conclusão:
let printMessage
Agora vamos adaptar a função
run()
para usar o retorno de chamada:
async function run() { console.clear()
E isso está do lado do JS!
Agora, na parte Ir, você precisa criar um retorno de chamada, enviá-lo para o lado JS e aguardar a função ser necessária.
var done = make(chan struct{})
Em seguida, eles devem escrever a função real
printMessage()
:
func printMessage(args []js.Value) { message := args[0].Strlng() fmt.Println(message) done <- struct{}{}
Os argumentos são passados pela fatia
[]js.Value
, portanto, é necessário chamar
js.Value.String()
no primeiro elemento da fatia para obter a mensagem na linha Ir.
Agora podemos agrupar essa função em um retorno de chamada:
callback := js.NewCallback(printMessage) defer callback.Release()
Em seguida, chame a função JS
setPrintMessage()
, assim como chamar
window.alert()
:
setPrintMessage := js.Global.Get("setPrintMessage") setPrintMessage.Invoke(callback)
A última coisa a fazer é esperar que o retorno de chamada seja chamado principal:
<-done
Esta última parte é importante porque os retornos de chamada são executados em uma goroutine dedicada, e a goroutine principal deve aguardar a chamada do retorno, caso contrário, o wasm binário será interrompido prematuramente.
O programa Go resultante deve ficar assim:
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 nos exemplos anteriores, crie um arquivo chamado
test.wasm
. Também precisamos substituir
wasm_exec.html
por nossa versão e
wasm_exec.js
reutilizar
wasm_exec.js
.
Agora, quando você pressiona o botão "executar", como no nosso primeiro exemplo, a mensagem é impressa no console do navegador, mas desta vez é muito melhor! (E mais difícil.)
Um exemplo de trabalho em um lance de arquivo docker está disponível na pasta
examples/go-call
.
Trabalho longo
Chamar Go de JS é um pouco mais complicado do que chamar JS de Go, especialmente no lado JS.
Isso ocorre principalmente pelo fato de você precisar aguardar até que o resultado do retorno de chamada Go seja passado para o lado JS.
Vamos tentar outra coisa: por que não organizar o binário wasm, que não termina logo após a chamada de retorno de chamada, mas continua a funcionar e aceita outras chamadas.
Agora, vamos começar pelo lado Ir e, como no exemplo anterior, precisamos criar um retorno de chamada e enviá-lo para o lado JS.
Adicione um contador de chamadas para rastrear quantas vezes a função foi chamada.
Nossa nova função
printMessage()
imprimirá a mensagem recebida e o valor do contador:
var no int func printMessage(args []js.Value) { message := args[0].String() no++ fmt.Printf("Message no %d: %s\n", no, message) }
Criar um retorno de chamada e enviá-lo para o lado JS é o mesmo que no exemplo anterior:
callback := js.NewCallback(printMessage) defer callback.Release() setPrintMessage := js.Global().Get("setPrintMessage") setPrIntMessage.Invoke(callback)
Mas desta vez não temos canal para nos notificar sobre o término da goroutina principal. Uma maneira seria bloquear permanentemente a goroutin principal com a
select{}
vazia
select{}
:
select{}
Isso não é satisfatório, nosso wasm binário ficará travado na memória até que a guia do navegador seja fechada.
Você pode ouvir o evento
beforeunload
na página, precisará de um segundo retorno de chamada para receber o evento e notificar a goroutine principal por meio do canal:
var beforeUnloadCh = make(chan struct{})
Dessa vez, a nova função
beforeUnload()
aceitará apenas o evento como um único argumento
js.Value
:
func beforeUnload(event js.Value) { beforeUnloadCh <- struct{}{} }
Em seguida, envolva-o em um retorno de chamada usando
js.NewEventCallback()
e registre-o no lado JS:
beforeUnloadCb := js.NewEventCallback(0, beforeUnload) defer beforeUnloadCb.Release() addEventLtstener := js.Global().Get("addEventListener") addEventListener.Invoke("beforeunload", beforeUnloadCb)
Por fim, substitua a
select
bloqueio vazia pela leitura do canal
beforeUnloadCh
:
<-beforeUnloadCh fmt.Prtntln("Bye Wasm!")
O programa final é assim:
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, no lado JS, o download do binário wasm era assim:
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 adaptá-lo para executar o binário imediatamente após o carregamento:
(async function() { const go = new Go() const { instance } = await WebAssembly.instantiateStreaming( fetch("test.wasm"), go.importObject ) go.run(instance) })()
E substitua o botão "Executar" por um campo de mensagem e um botão para chamar
printMessage()
:
<input id="messageInput" type="text" value="Hello Wasm!"> <button onClick="printMessage(document.querySelector('#messagelnput').value);" id="prtntMessageButton" disabled> Print message </button>
Por fim, a função
setPrintMessage()
, que aceita e armazena o retorno de chamada, deve ser mais simples:
let printMessage; function setPrintMessage(callback) { printMessage = callback; document.querySelector('#printMessageButton').disabled = false; }
Agora, quando clicarmos no botão "Imprimir mensagem", você verá uma mensagem de nossa escolha e um contador de chamadas impresso no console do navegador.
Se marcarmos a caixa Preserve log do console do navegador e atualizar a página, veremos a mensagem “Bye Wasm!”.
As fontes estão disponíveis na pasta
examples/long-running
no github.
E então?
Como você pode ver, a API
syscall/js
aprendida faz seu trabalho e permite que você escreva coisas complexas com um pouco de código. Você pode escrever para o
autor se souber um método mais simples.
Atualmente, não é possível retornar um valor para JS diretamente do retorno de chamada Go.
Lembre-se de que todos os retornos de chamada são executados na mesma goroutin; portanto, se você realizar algumas operações de bloqueio no retorno de chamada, não esqueça de criar uma nova goroutin; caso contrário, você bloqueará a execução de todos os outros retornos de chamada.
Todos os recursos básicos do idioma já estão disponíveis, incluindo simultaneidade. Por enquanto, todas as goroutins funcionarão em um segmento, mas isso
mudará no futuro .
Em nossos exemplos, usamos apenas o pacote fmt da biblioteca padrão, mas tudo está disponível que não tenta escapar da sandbox.
O sistema de arquivos parece ser suportado pelo Node.js.
Finalmente, e o desempenho? Seria interessante executar alguns testes para ver como o Go wasm se compara ao código JS puro equivalente. Alguém
hajimehoshi fez medições de como diferentes ambientes trabalham com números inteiros, mas a técnica não é muito clara.
Não esqueça que o Go 1.11 ainda não foi lançado oficialmente. Na minha opinião, é muito bom para a tecnologia experimental. Aqueles que estão interessados em testes de desempenho
podem atormentar o navegador .
O nicho principal, como observa o autor, é a transferência do servidor para o cliente do código go existente. Mas com novos padrões, você pode criar
aplicativos completamente offline e o código wasm é salvo na forma compilada. Você pode transferir muitos utilitários para a web, concorda, convenientemente?