在〈Go 呼叫 JavaScript〉看過如何在 Go 中取得 JavaScript 的函式,然後予以呼叫,若你曾稍微瞭解過〈WebAssembly〉,就會發覺,這跟 WebAssembly 匯入函式至 WebAssembly 的方式不同。
這是 JavaScript 的 wasm_exec.js 以及 Go 的 syscall/js 居中之緣故,在 wasm_exec.html 中你也可以看到載入、編譯、實例化 WebAssembly 的過程:
if (!WebAssembly.instantiateStreaming) { // polyfill
    WebAssembly.instantiateStreaming = async (resp, importObject) => {
        const source = await (await resp).arrayBuffer();
        return await WebAssembly.instantiate(source, importObject);
    };
}
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;
});
async function run() {
    console.clear();
    await go.run(inst);
    inst = await WebAssembly.instantiate(mod, go.importObject); // reset instance
}
Go 有自己的匯入物件,也就是 go.importObject,這個物件主要是 JavaScript 環境與 Go 編譯出來的 WebAssembly 之橋樑,將 JavaScript 的值與 Go 的結構實例作了個對應,因此,不用自己匯入某個函式,只要取得某個作為名稱空間的 JavaScript 物件,取得上頭對應的特性,像是函式,就可以在 Go 中操作。
也就是說,如果想要在 Go 中定義函式,然後在 JavaScript 中呼叫,就是將 Go 中定義的函式,設定給某個對應的 JavaScript 物件,之後就可以在 JavaScript 環境中使用了,只不過在定義時,必須留意 JavaScript 與 Go 的型態對應。
可以被 JavaScript 環境呼叫的 Go 函式,必須被包裝為 js.Callback 型態,這個結構型態內嵌 js.Value,也就是它也是一種值,想要建立 js.Callback 實例,可以透過 js.NewCallback 函式(定義在 callback.go)。
要能被 JavaScript 呼叫的 Go 函式,參數型態是 []js.Value,也就是 js.Value 的 slice,slice 的元素代表著呼叫函式時傳入的引數,你可以想像 JavaScript 函式中 arguments 的對應型態。
例如,顯示加總至某個指定 DOM 物件的函式,可以如下定義:
package main
import "syscall/js"
func main() {
    // 註冊在 JavaScript 全域
    js.Global().Set("printSumTo", js.NewCallback(printSum))
    // 阻斷 main 流程
    select {}   
}
func printSum(args []js.Value) {
    c1 := args[0]         // 結果顯示到這個 div 
    numbers := args[1:]   // 接下來是要加總的數字
    c1.Set("innerHTML", sum(numbers))
}
func sum(numbers []js.Value) int {
    var sum int
    for _, val := range numbers {
        sum += val.Int()
    }
    return sum
}
目前 Go 給 JavaScript 回呼用的函式不支援傳回值,未來也許會進一步支援,如果你想將結果帶回 JavaScript 環境,就是以副作用的方式實現,例如改變某個 JavaScript 物件的狀態,像是這邊是改變某個 DOM 的 innerHTML。
因為 Go 的 main 執行完,模組的程式就結束了,這樣 Go 中定義的函式就沒有了,然而,事件會是在之後才發生,因而要被回呼的函式必須存活著,為了這個目的,範例中使用 select {} 來阻斷流程,視需求而定,你也可以用別的方式來設計某種阻斷。
至於 JavaScript 的部份,來稍微修改一下 wasm_exec.html:
<!doctype html>
<!--
Copyright 2018 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
<html>
<head>
    <meta charset="utf-8">
    <title>Go wasm</title>
</head>
<body>
    <script src="wasm_exec.js"></script>
    <script>
        if (!WebAssembly.instantiateStreaming) { // polyfill
            WebAssembly.instantiateStreaming = async (resp, importObject) => {
                const source = await (await resp).arrayBuffer();
                return await WebAssembly.instantiate(source, importObject);
            };
        }
        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;
        }).then(_ => {   // 實例化模組之後就執行
            console.clear();
            go.run(inst);       
        });
    </script>   
    <script>
        function run() {
            // 呼叫 Go 定義的回呼函式
            printSumTo(document.getElementById('c1'), 
                1, 2, 3, 4, 5);
        }
    </script>
    <button onClick="run();" id="runButton" disabled>Run</button>
    <div id="c1"></div>
</body>
</body>
</html>
按下 Run 之後,會呼叫 runAndPrintSum,這會先執行 run 函式,執行 WebAssembly 模組實例,對應的就是執行 Go 定義的 main,因為 run 是非同步的,接下來就會執行 printSumTo,因此 1 到 5 的加總結果,就會顯示到 id 為 c1 的 div 元素之中。
至於 WebAssembly API 的調整,想要瞭解這部份的話,可以看看〈WebAssembly〉中前三篇的說明。
故且不討論 WebAssembly API 怎麼寫,在自定義的 JavaScript 程式碼中,想要呼叫 Go 中定義的函式,其實感覺就是多了些額外的手續,而且不自然。
如果把一切都帶到 Go 中做,將 Go 中定義的函式,當成是某事件的回呼,會比較單純一些,例如:
package main
import (
    "strconv"
    "syscall/js"
)
func main() {
    // 註冊按鈕事件
    dom("runButton").Call("addEventListener", "click", js.NewCallback(cal))
    select {}
}
// 根據 id 取得 DOM 物件
func dom(id string) js.Value {
    return js.Global().Get("document").Call("getElementById", id)
}
// 按下 Run 的事件處理器
func cal(args []js.Value) {
    n1, _ := inputValue("n1")
    n2, _ := inputValue("n2")
    dom("r").Set("innerHTML", n1+n2)
}
// 取得輸入欄位值
func inputValue(id string) (int, error) {
    return strconv.Atoi(dom(id).Get("value").String())
}
至於 wasm_exec.html 可以如下調整:
<!doctype html>
<!--
Copyright 2018 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
<html>
<head>
    <meta charset="utf-8">
    <title>Go wasm</title>
</head>
<body>
    <input id="n1"> + <input id="n2"> = <span id="r"></span><br>
    <button id="runButton" disabled>Run</button>
    <script src="wasm_exec.js"></script>
    <script>
        if (!WebAssembly.instantiateStreaming) { // polyfill
            WebAssembly.instantiateStreaming = async (resp, importObject) => {
                const source = await (await resp).arrayBuffer();
                return await WebAssembly.instantiate(source, importObject);
            };
        }
        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;
        }).then(_ => {
            console.clear();
            go.run(inst);       
        });
    </script>   
</body>
</html>
這樣就可以進行頁面操作,就是個簡單的加法器:
(這也許才是 Go 希望的,要你把東西都帶入 Go 中來做,JavaScript 環境的事件會呼叫 Go 的函式,然後在 Go 中計算,在 Go 中改變物件狀態、畫面等。)

