今年八月底Go語言釋出了1.11,引起話題的一大賣點是支援WebAssembly,由於四大瀏覽器廠商一致支援WebAssembly,這讓Go在取代JavaScript方面,似乎多了一份籌碼。

那麼,目前的Go怎麼做?只要會Go,就夠了嗎?

Hello, WebAssembly!

在Go的Wiki上有份〈WebAssembly〉(https://goo.gl/dmrNGT),示範了執行println("Hello, WebAssembly!")的Go程式,如何能編譯為.wasm並在網頁上執行。編譯前,必須設定環境變數GOOS=js、GOARCH=wasm,然後使用go build -o main.wasm編譯出一個WebAssembly模組。

根據〈Go 1.11 Release Notes〉(https://goo.gl/YfaETG),所編譯出來的WebAssembly模組,包含了goroutine、垃圾收集、maps等功能的執行環境,最小約在2MB,壓縮後可減至500KB。如果想在瀏覽器中執行編譯出來的模組,須使用WebAssembly API,進一步編譯、實例化載入的模組檔案。

然而,正如我在先前專欄〈Wasm儲存空間〉中所談到的,高階語言應該會提供程式庫,讓編譯出來的WebAssembly與JavaScript間,在高階資料結構的方面可以便於轉換。

在JavaScript這方面,Go 1.11安裝目錄misc\wasm中,有個wasm_exec.js,可以擔任此角色,在網頁中含括wasm_exec.js之後,使用底下的程式,就可以在主控臺顯示文字:

const go = new Go();
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject)
.then(result => go.run(result.instance));

實際上,wasm_exec.js也整合了Node.js API,若已安裝Node.js,可以執行node wasm_exec.js main.wasm,就能夠看到文字輸出;如果在Linux下,還可以使用misc\wasm中的go_js_wasm_exec指令稿,讓go run、go test可以直接搭配Node.js來執行。

操作DOM與JavaScript函式

Go程式編譯為WebAssembly模組後,標準輸出相關函式也直接對應至主控臺(console.log)。既然社群直言,Go支援WebAssembly就是要取代JavaScript,支援DOM操作自是Go份內之事,為此,Go 1.11提供實驗性syscall/js套件來負責。

JavaScript與Go畢竟是兩個不同的語言,各擁有不同的資料型態與結構,因而必須先知道,兩個語言間的型態如何對應,關於這部分,主要定義在syscall/js套件的js.go中。

例如,js.Value代表JavaScript值,擁有Get與Set方法可以對物件上的特性存取;若想存取的對象,實際上是陣列,可以使用Index、SetIndex並指定索引;若對象是個函式,可以使用Invoke指定引數來呼叫;若想呼叫的是物件上的方法,可以使用Call指定方法名稱與呼叫時的引數等。

在js.go中預先定義了一些js.Value的實例,可以透過公開的Undefined、Null、Global等函式呼叫取得,因此,如果網頁上有個<div id="span1">Hello, WebAssembly!</div>,想撰寫Go來取得對應的DOM物件,並在主控臺顯示innerHTML特性值,可以如下撰寫:

doc := js.Global().Get("document")
doc.Call("getElementById", "span1").Get("innerHTML").String()

這是因為,在瀏覽器中,document是全域物件的特性,而上述程式,相當於在JavaScript撰寫document.getElementById("span1").innerHTML;類似地,若想在Go呼叫瀏覽器提供的alert全域函式,可如下撰寫,這會令瀏覽器出現alert對話方塊:

alert := js.Global().Get("alert")
alert.Invoke("Hello, WebAssembly!")

匯出與註冊Go函式

在Go中要呼叫JavaScript函式,非常簡單,因為在JavaScript環境中,函式必須是某個物件上的特性,全域函式也不過就是全域物件上的特性,因此,方才的作法,也等同於示範了如何呼叫其他物件上的函式或方法。而在JavaScript中怎麼做,在Go中就是轉換為對應的Get、Invoke或Call。

若要定義一個Go函式,可以被JavaScript環境呼叫,就麻煩了一些。一個可以被JavaScript環境呼叫的Go函式,必須被包裝為js.Callback型態(內嵌js.Value),這能夠透過js.NewCallback(定義在callback.go)指定Go函式來建立,然而,必須注意的是,Go函式的參數型態是[]js.Value,也就是js.Value的slice,當中元素會是呼叫函式時傳入的引數,例如,使用加總函式時,簽署上可以定義為func sum(args []js.Value)。

在Go中,並沒有直接匯出函式給JavaScript環境的API,然而從上述來看,js.Value有Set方法可以對物件設定特性,因此只要取得某個JavaScript物件,就可以使用Set方法,將js.Callback設定給該物件,這麼一來,JavaScript中對應的物件,就可以呼叫Go中定義的函式了,例如,若有個方才提及的sum函式,我們就能如下指定為JavaScript中的全域函式:

js.Global().Set("sum", js.NewCallback(sum))

如果是要註冊給DOM作為事件處理器的函式,也是將Go函式包裝為js.Callback,然後呼叫相關的註冊函式,例如,若cb是個js.Callback實例,想要註冊按鈕的click事件,此時,可以撰寫如下:

doc := js.Global().Get("document")
doc.Call("getElementById", "btn1").Call("addEventListener", "click", cb)

必須注意的是,程式流程一旦執行完Go的main,該模組就跟著結束,被註冊的函式也就無效了。目前的作法,是建立一個channel,利用存取它來阻斷流程。如果想要實際看看程式的撰寫,可以參考〈WebAssembly support in Go〉(https://goo.gl/FPt1mo)、〈Go WebAssembly Tutorial〉(https://goo.gl/xXpako)或〈WebAssembly for the gophers〉(https://goo.gl/FpSf41)等。

探究syscall/js與wasm-exec.js

想要進一步認識Go對WebAssembly的支援,最好的方式是察看syscall/js與wasm-exec.js原始碼,能得到許多目前可見文件中沒有談到的細節;然而,這也表示想要掌握Go對WebAssembly的支援,須對Go、JavaScript都有深入的認識,甚至於還必須知道WebAssembly的運作方式,特別是在記憶體這塊。

若未來Go因為對WebAssembly的支援而獲得肯定,瀏覽器相關環境也成熟,理想上,要有專門的人來負責Go、WebAssembly、JavaScript,實作出介面。而介面的一邊,有人專心寫Go的程式庫,另一邊則有人寫JavaScript的操作,兩者不用(太過)關心另一語言的細節。也就是說,Go對WebAssembly的支援,有利於Go語言生態系的開發者與前端開發者合作,然而,取代JavaScript還言之過早。

畢竟在Go對WebAssembly及瀏覽器的支援成熟之前,別忘了,JavaScript生態系也還是持續演化中,只要還必須與JavaScript生態系打交道,對JavaScript的認識與使用就無可避免。

這也就是為何Brendan Eich說過:「WebAssembly不是為了取代JavaScript」既然如此,對於可編譯為WebAssembly的Go或其他語言來說,取代JavaScript這件事,就姑且當成是個偉大的目標就好!

作者簡介


Advertisement

更多 iThome相關內容