Originalartikel .
Im Februar 2017 schlug ein Mitglied des go Brad Fitzpatrick-Teams
vor , WebAssembly in der Sprache zu unterstützen. Vier Monate später, im November 2017, begann der
GopherJS- Autor Richard Muziol, die Idee umzusetzen. Und schließlich wurde die vollständige Implementierung in master gefunden. Entwickler erhalten um August 2018 wasm mit
go Version
1.11 . Infolgedessen übernimmt die Standardbibliothek fast alle technischen Schwierigkeiten beim Importieren und Exportieren von Funktionen, die Ihnen bekannt sind, wenn Sie bereits versucht haben, C in wasm zu kompilieren. Das klingt vielversprechend. Mal sehen, was mit der ersten Version gemacht werden kann.
Alle Beispiele in diesem Artikel können aus Docker-Containern gestartet werden, die sich
im Repository des Autors befinden :
docker container run -dP nlepage/golang_wasm:examples
Gehen Sie dann zu
localhost : 32XXX / und wechseln Sie von einem Link zum anderen.
Hallo Wasm!
Die Entstehung der grundlegenden „Hallo Welt“ und das Konzept sind bereits recht
gut dokumentiert (auch
auf Russisch ). Kommen wir also zu den subtileren Dingen.
Am wichtigsten ist eine frisch kompilierte Version von Go, die wasm unterstützt. Ich werde
die Installation nicht
Schritt für Schritt beschreiben , sondern nur wissen, dass das, was benötigt wird, bereits im Master vorhanden ist.
Wenn Sie sich darüber keine Sorgen machen möchten, ist
Dockerfile c go im
golub-wasm-Repository auf github verfügbar , oder Sie können ein Bild von
nlepage / golang_wasm noch schneller aufnehmen.
Jetzt können Sie die traditionelle
helloworld.go
schreiben und mit dem folgenden Befehl kompilieren:
GOOS=js GOARCH=wasm go build -o test.wasm helioworld.go
Die Umgebungsvariablen GOOS und GOARCH sind bereits im Bild
nlepage / golang_wasm festgelegt , sodass Sie eine
Dockerfile
Datei wie diese zum Kompilieren verwenden können:
FROM nlepage/golang_wasm COPY helloworld.go /go/src/hello/ RUN go build -o test.wasm hello
Der letzte Schritt besteht darin, die Dateien
wasm_exec.html
und
wasm_exec.js
verwenden, die im go-Repository im
misc/wasm
misc / wasm oder im Docker-Image
nlepage / golang_wasm im
Verzeichnis /usr/local/go/misc/wasm/
misc / wasm / verfügbar sind , um
test.wasm
browser (wasm_exec.js erwartet die Binärdatei
test.wasm
, daher verwenden wir diesen Namen).
Sie müssen nur 3 statische Dateien mit nginx angeben, dann zeigt wasm_exec.html die Schaltfläche "Ausführen" an (sie wird nur
test.wasm
wenn
test.wasm
korrekt geladen ist).
Es ist bemerkenswert, dass
test.wasm
mit der
application/wasm
test.wasm
vom Typ MIME
test.wasm
muss, da der Browser sonst die Ausführung verweigert. (z. B. benötigt nginx eine
aktualisierte Datei mime.types ).
Sie können das nginx-Image aus
nlepage / golang_wasm verwenden , das bereits den festen MIME-Typ
wasm_exec.html
und
wasm_exec.js
im Code> / usr / share / nginx / html / directory enthält.
Klicken Sie nun auf die Schaltfläche "Ausführen", öffnen Sie Ihre Browserkonsole und Sie sehen die Begrüßung console.log ("Hallo Wasm!").
Ein vollständiges Beispiel finden Sie
hier .
Rufen Sie JS von Go an
Nachdem wir die erste aus Go kompilierte WebAssembly-Binärdatei erfolgreich gestartet haben, schauen wir uns die bereitgestellten Funktionen genauer an.
Das neue Paket syscall / js wurde der Standardbibliothek hinzugefügt. Betrachten Sie die Hauptdatei
js.go
Es ist ein neuer
js.Value
Typ
js.Value
, der einen JavaScript-Wert darstellt.
Es bietet eine einfache API zum Verwalten von JavaScript-Variablen:
js.Value.Get()
und js.Value.Set()
geben die Feldwerte des Objekts zurück und legen sie fest.js.Value.Index()
und js.Value.SetIndex()
greifen über den Lese- und js.Value.SetIndex()
auf das Objekt zu.js.Value.Call()
ruft die Objektmethode als Funktion auf.js.Value.Invoke()
ruft das Objekt selbst als Funktion auf.js.Value.New()
ruft den neuen Operator auf und verwendet sein eigenes Wissen als Konstruktor.- Einige weitere Methoden, um den JavaScript-Wert im entsprechenden Go-Typ
js.Value.Int()
, z. B. js.Value.Int()
oder js.Value.Bool()
.
Und weitere interessante Methoden:
js.Undefined()
gibt js.Value das entsprechende undefined
.js.Null()
gibt js.Value
entsprechende null
.js.Global()
gibt js.Value
und js.Value
Zugriff auf den globalen Bereich.js.ValueOf()
akzeptiert primitive Go-Typen und gibt den korrekten js.Value
Anstatt die Nachricht in os.StdOut anzuzeigen, zeigen wir sie im Benachrichtigungsfenster mit
window.alert()
.
Da wir uns im Browser befinden, ist der globale Bereich ein Fenster. Sie müssen also zuerst alert () vom globalen Bereich abrufen:
alert := js.Global().Get("alert")
Jetzt haben wir eine
js.Value
in Form von
js.Value
, die auf
window.alert
JS
window.alert
, und Sie können die Funktion verwenden, um
js.Value.Invoke()
:
alert.Invoke("Hello wasm!")
Wie Sie sehen, muss js.ValueOf () nicht aufgerufen werden, bevor die Argumente an Invoke übergeben werden. Es wird eine beliebige Menge an
interface{}
und die Werte werden über ValueOf selbst übergeben.
Jetzt sollte unser neues Programm so aussehen:
package main import ( "syscall/js" ) func main() { alert := js.Global().Get("alert") alert.Invoke("Hello Wasm!") }
Wie im ersten Beispiel müssen Sie nur eine Datei mit dem Namen
test.wasm
erstellen und
wasm_exec.html
und
wasm_exec.js
lassen.
Wenn wir nun auf die Schaltfläche "Ausführen" klicken, wird ein Warnfenster mit unserer Nachricht angezeigt.
Ein funktionierendes Beispiel befindet sich im Ordner
examples/js-call
.
Rufen Sie Go von JS an.
Das Aufrufen von JS von Go aus ist ziemlich einfach. Schauen wir uns das Paket
syscall/js
an. Die zweite Datei, die
syscall/js
, ist
callback.go
.
js.Callback
Wrapper-Typ für die Go-Funktion zur Verwendung in JS.js.NewCallback()
Funktion, die eine Funktion übernimmt (ein Stück js.Value
akzeptiert und nichts zurückgibt) und js.Callback
.- Einige Mechanismen zum Verwalten aktiver Rückrufe und
js.Callback.Release()
, die aufgerufen werden müssen, um den Rückruf zu zerstören. js.NewEventCallback()
ähnelt js.NewCallback()
, aber die js.NewCallback()
Funktion akzeptiert nur 1 Argument - ein Ereignis.
Versuchen wir etwas Einfaches: Führen Sie Go
fmt.Println()
von der JS-Seite aus.
Wir werden einige Änderungen an
wasm_exec.html
, um einen Rückruf von Go zu erhalten, um ihn aufzurufen.
async function run() { console.clear(); await go.run(inst); inst = await WebAssembly.instantiate(mod, go.ImportObject);
Dadurch wird die wasm-Binärdatei gestartet und auf ihren Abschluss gewartet. Anschließend wird sie für den nächsten Lauf neu initialisiert.
Fügen wir eine neue Funktion hinzu, die den Go-Rückruf empfängt und speichert und den Status des
Promise
nach Abschluss ändert:
let printMessage
Passen wir nun die Funktion
run()
an, um den Rückruf zu verwenden:
async function run() { console.clear()
Und das ist auf der Seite von JS!
Jetzt müssen Sie im Go-Teil einen Rückruf erstellen, ihn an die JS-Seite senden und warten, bis die Funktion benötigt wird.
var done = make(chan struct{})
Dann sollten sie die eigentliche Funktion
printMessage()
schreiben:
func printMessage(args []js.Value) { message := args[0].Strlng() fmt.Println(message) done <- struct{}{}
Die Argumente werden durch das Slice
[]js.Value
, daher müssen Sie
js.Value.String()
für das erste Slice-Element aufrufen, um die Nachricht in der Go-Zeile
js.Value.String()
.
Jetzt können wir diese Funktion in einen Rückruf einschließen:
callback := js.NewCallback(printMessage) defer callback.Release()
Rufen Sie dann die JS-Funktion
setPrintMessage()
, genau wie beim Aufrufen von
window.alert()
:
setPrintMessage := js.Global.Get("setPrintMessage") setPrintMessage.Invoke(callback)
Als letztes müssen Sie warten, bis der Rückruf in main aufgerufen wird:
<-done
Dieser letzte Teil ist wichtig, da die Rückrufe in einer dedizierten Goroutine ausgeführt werden und die Hauptgoroutine auf den Aufruf des Rückrufs warten muss, da sonst die wasm-Binärdatei vorzeitig gestoppt wird.
Das resultierende Go-Programm sollte folgendermaßen aussehen:
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{}{} }
Erstellen Sie wie in den vorherigen Beispielen eine Datei mit dem Namen
test.wasm
. Wir müssen auch
wasm_exec.html
durch unsere Version ersetzen und können
wasm_exec.js
wiederverwenden.
Wenn Sie nun wie in unserem ersten Beispiel auf die Schaltfläche "Ausführen" klicken, wird die Nachricht in der Browserkonsole gedruckt, diesmal jedoch viel besser! (Und schwerer.)
Ein funktionierendes Beispiel in einem Docker-Datei-Gebot finden Sie im Ordner
examples/go-call
.
Lange Arbeit
Das Aufrufen von Go von JS aus ist etwas umständlicher als das Aufrufen von JS von Go aus, insbesondere auf der JS-Seite.
Dies liegt hauptsächlich an der Tatsache, dass Sie warten müssen, bis das Ergebnis des Go-Rückrufs an die JS-Seite übergeben wird.
Versuchen wir etwas anderes: Warum nicht die Wasm-Binärdatei organisieren, die nicht direkt nach dem Rückruf endet, sondern weiterhin funktioniert und andere Aufrufe annimmt?
Beginnen wir dieses Mal von der Go-Seite, und wie in unserem vorherigen Beispiel müssen wir einen Rückruf erstellen und an die JS-Seite senden.
Fügen Sie einen Anrufzähler hinzu, um zu verfolgen, wie oft die Funktion aufgerufen wurde.
Unsere neue Funktion
printMessage()
druckt die empfangene Nachricht und den
printMessage()
:
var no int func printMessage(args []js.Value) { message := args[0].String() no++ fmt.Printf("Message no %d: %s\n", no, message) }
Das Erstellen eines Rückrufs und das Senden an die JS-Seite erfolgt wie im vorherigen Beispiel:
callback := js.NewCallback(printMessage) defer callback.Release() setPrintMessage := js.Global().Get("setPrintMessage") setPrIntMessage.Invoke(callback)
Diesmal haben wir jedoch keinen Kanal eingerichtet, um uns über die Beendigung der Hauptgoroutine zu informieren. Eine Möglichkeit könnte darin bestehen, das Haupt-Goroutin dauerhaft mit der leeren
select{}
zu sperren:
select{}
Dies ist nicht zufriedenstellend. Unser Binär-Wasm bleibt nur im Speicher hängen, bis die Browser-Registerkarte geschlossen wird.
Sie können das Ereignis vor dem
beforeunload
auf der Seite anhören. Sie benötigen einen zweiten Rückruf, um das Ereignis zu empfangen und die Haupt-Goroutine über den Kanal zu benachrichtigen:
var beforeUnloadCh = make(chan struct{})
Dieses Mal akzeptiert die neue Funktion
beforeUnload()
das Ereignis nur als einzelnes
js.Value
Argument:
func beforeUnload(event js.Value) { beforeUnloadCh <- struct{}{} }
Wickeln Sie es dann mit
js.NewEventCallback()
in einen Rückruf ein und registrieren Sie es auf der JS-Seite:
beforeUnloadCb := js.NewEventCallback(0, beforeUnload) defer beforeUnloadCb.Release() addEventLtstener := js.Global().Get("addEventListener") addEventListener.Invoke("beforeunload", beforeUnloadCb)
Ersetzen Sie
beforeUnloadCh
die leere Blockierungsauswahl durch Lesen aus dem
beforeUnloadCh
Kanal:
<-beforeUnloadCh fmt.Prtntln("Bye Wasm!")
Das endgültige Programm sieht folgendermaßen aus:
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{}{} }
Bisher sah der Download der wasm-Binärdatei auf der JS-Seite folgendermaßen aus:
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 })
Passen wir es an, um die Binärdatei unmittelbar nach dem Laden auszuführen:
(async function() { const go = new Go() const { instance } = await WebAssembly.instantiateStreaming( fetch("test.wasm"), go.importObject ) go.run(instance) })()
Ersetzen Sie die Schaltfläche "Ausführen" durch ein Nachrichtenfeld und eine Schaltfläche zum Aufrufen von
printMessage()
:
<input id="messageInput" type="text" value="Hello Wasm!"> <button onClick="printMessage(document.querySelector('#messagelnput').value);" id="prtntMessageButton" disabled> Print message </button>
Schließlich sollte die Funktion
setPrintMessage()
, die den Rückruf akzeptiert und speichert, einfacher sein:
let printMessage; function setPrintMessage(callback) { printMessage = callback; document.querySelector('#printMessageButton').disabled = false; }
Wenn wir jetzt auf die Schaltfläche "Nachricht drucken" klicken, sollten Sie eine Nachricht unserer Wahl und einen Anrufzähler in der Browserkonsole sehen.
Wenn wir das Kontrollkästchen "Beibehalten" der Browserkonsole aktivieren und die Seite aktualisieren, wird die Meldung "Bye Wasm!" Angezeigt.
Quellen finden Sie im Ordner
examples/long-running
auf github.
Und weiter?
Wie Sie sehen können,
syscall/js
die erlernte
syscall/js
API ihre Aufgabe und ermöglicht es Ihnen, komplexe Dinge mit ein bisschen Code zu schreiben. Sie können an den
Autor schreiben, wenn Sie eine einfachere Methode kennen.
Es ist derzeit nicht möglich, einen Wert direkt aus dem Go-Rückruf an JS zurückzugeben.
Beachten Sie, dass alle Rückrufe im selben Goroutin ausgeführt werden. Wenn Sie also einige Blockierungsvorgänge im Rückruf ausführen, vergessen Sie nicht, ein neues Goroutin zu erstellen. Andernfalls blockieren Sie die Ausführung aller anderen Rückrufe.
Alle grundlegenden Sprachfunktionen sind bereits verfügbar, einschließlich der Parallelität. Im Moment werden alle Goroutins in einem Thread funktionieren, aber dies
wird sich in Zukunft ändern .
In unseren Beispielen haben wir nur das fmt-Paket aus der Standardbibliothek verwendet, aber es ist alles verfügbar, was nicht versucht, aus der Sandbox zu entkommen.
Das Dateisystem scheint über Node.js unterstützt zu werden.
Was ist schließlich mit der Leistung? Es wäre interessant, einige Tests durchzuführen, um zu sehen, wie Go wasm mit äquivalentem reinem JS-Code verglichen wird. Jemand
Hajimehoshi hat
gemessen , wie verschiedene Umgebungen mit ganzen Zahlen funktionieren, aber die Technik ist nicht sehr klar.
Vergessen Sie nicht, dass Go 1.11 noch nicht offiziell veröffentlicht wurde. Meiner Meinung nach ist es sehr gut für experimentelle Technologie. Wer an Leistungstests interessiert ist,
kann seinen Browser quälen .
Die Hauptnische ist, wie der Autor feststellt, die Übertragung des vorhandenen Go-Codes vom Server zum Client. Mit neuen Standards können Sie jedoch
vollständig Offline-Anwendungen erstellen , und der Wasm-Code wird in kompilierter Form gespeichert. Sie können viele Dienstprogramme bequem ins Internet übertragen, stimmen Sie zu?