從 Go 語言誕生以來,它就開始不斷侵蝕 Java 、C、C++ 語言的領地。今年下半年 Go 語言釋出了 1.11 版本,引入了 WebAssembly 技術,瀏覽器端 Javascript 的壟斷地位也開始遭遇 Go 語言的攻擊。這次不同以往,它意味著 Go 語言從後端滲透進了前端,進入了一個全新的世界。
WebAssembly 執行原理
WebAssembly 這個名字翻譯過來就是 「Web 彙編」,也就是 Web 端的組合語言。它是一段二進位制位元組碼程式,Javascript 可以將這段二進位制程式編譯成模組,然後再例項化這個模組就可以呼叫位元組碼邏輯了。WebAssembly 程式碼執行的速度很快,比 Javascript 要快很多,Javascript 可以通過 WebAssembly 技術將關鍵性耗費效能的邏輯交給 WebAssembly 來做就可以明顯提升瀏覽器端的效能。
對比顯示,使用 WebAssembly 執行斐波那契數列相比使用原生 Javascript 來實現,執行效率上能帶來 3.5 倍的提升。
WebAssembly 是一項比較新的技術,只有比較現代的瀏覽器才支援 WebAssembly,例如 Chrome、FireFox瀏覽器。
Go WebAssembly 執行原理
Go 編譯器可以將程式碼編譯成 WebAssembly 二進位制位元組碼,被瀏覽器以靜態資源的形式載入進來後轉換成 Javascript 模組。有了這個模組,瀏覽器可以直接操縱 Go 語言生成的二進位制位元組碼邏輯。同時在 Go 語言編寫的程式碼中可以直接讀寫瀏覽器裡面 Javascript 執行時物件,這樣就完成了 Javascript 和 Go 程式碼的雙向互動。
Go 語言直到 1.11 版本之後才開啟了對 WebAssembly 的支援。如需體驗,必須升級。
Go WebAssembly 初體驗
下面我們就開始體驗一下 Chrome 瀏覽器與 Go 程式碼是如何互動的。我們要實現一個功能,在瀏覽器的輸入框裡輸入一個正整數,然後呼叫 Go 程式碼的斐波那契數列,再將結果再呈現在頁面上。涉及到 4 個檔案,分別是 fib.go、main.go、index.html、wasm_exec.js。
第一步
使用 Go 程式碼編寫 WebAssembly 模組檔案 fib.go,將 Go 語言實現的斐波那契函式註冊到 Javascript 全域性環境。這需要使用內建的 syscall/js 模組,它提供了和 Javascript 引擎互動的介面。
// fib.go
package main
import "syscall/js"
func main() {
f_fib := func(params []js.Value) {
var n = params[0].Int() // 輸入引數
var callback = params[1] // 回撥引數
var result = fib(n)
// 呼叫回撥函式,傳入計算結果
callback.Invoke(result)
}
// 註冊全域性函式
js.Global().Set("fib", js.NewCallback(f_fib))
// 保持 main 函式持續執行
select {}
}
// 計算斐波那契數
func fib(n int) int {
if n <= 0 {
return 0
}
var result = make([]int, n+1)
result[0] = 0
result[1] = 1
if n <= 1 {
return result[n]
}
for i:=2;i<=n;i++ {
result[i] = result[i-2] + result[i-1]
}
return result[n]
}
複製程式碼
Go 語言註冊到 Javascript 引擎的函式在執行時是非同步的,所以這個函式沒有返回值,在完成計算後需要通過呼叫「傳進來的回撥函式」將結果傳遞到 Javascript 引擎。注意 main 函式要保持執行狀態不要退出,不然註冊進去的 fib 函式體就銷燬了。
第二步
下面將 Go 程式碼編譯成 WebAssembly 二進位制位元組碼。
$ GOARCH=wasm GOOS=js go build -o fib.wasm fib.go
複製程式碼
執行完成後可以看到目錄下多了一個 fib.wasm,這個就是位元組碼檔案。它的大小是 1.3M,作為靜態檔案傳遞到瀏覽器似乎有點大,不過靜態檔案伺服器一般有 gzip 壓縮,壓縮後的大小隻有幾百K,這差不多也可以接受了。
第三步
編寫網頁檔案 index.html,這個網頁包含兩個輸入框,第一個輸入框用來輸入整數引數,第二個輸入框用來呈現計算結果。當第一個輸入框內容發生改變時,呼叫 javascript 程式碼,執行通過 WebAssembly 註冊的 fib 函式。需要傳入引數 n 和回撥的函式。
<html>
<head>
<meta charset="utf-8">
<title>Go wasm</title>
</head>
<style>
body {
text-align: center
}
input {
height: 50px;
font-size: 20px;
}
#result {
margin-left: 20px;
}
</style>
<body>
<script src="wasm_exec.js"></script>
<script>
// 容納 WebAssembly 模組的容器
var go = new Go();
// 下載 WebAssembly 模組並執行模組
// 也就是執行 Go 程式碼裡面的 main 函式
// 這樣 fib 函式就註冊進了 Javascript 全域性環境
WebAssembly.instantiateStreaming(fetch("fib.wasm"), go.importObject).then((result) => {
go.run(result.instance);
});
function callFib() {
let paramInput = document.getElementById("param")
let n = parseInt(paramInput.value || "0")
// 傳入輸入引數和回撥函式
// 回撥函式負責呈現結果
fib(n, function(result) {
var resultDom = document.getElementById("result")
resultDom.value = result
})
}
</script>
// 輸入發生變化時,呼叫 WebAssembly 的 fib 函式
<input type="number" id="param" oninput="callFib()"/>
<input type="text" id="result" />
</body>
</html>
複製程式碼
注意程式碼中引入了一個特殊的 js 檔案 wasm_exec.js,這個檔案可以從 Go 安裝目錄的 misc 子目錄裡找到,將它直接拷貝過來。它實現了和 WebAssembly 模組互動的功能。
第四步
執行靜態檔案伺服器,這裡不能使用普通的靜態檔案伺服器,因為瀏覽器要求請求到的 WebAssemly 位元組碼檔案的 Content-Type 必須是 application/wasm,很多靜態檔案伺服器並不會因為副檔名是 wasm 就會自動使用這個 Content-Type。但是 Go 內建的 HTTP 伺服器可以。所以下面我們使用 Go 程式碼簡單編寫一個靜態檔案伺服器。
package main
import (
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.Handle("/", http.FileServer(http.Dir(".")))
log.Fatal(http.ListenAndServe(":8000", mux))
}
複製程式碼
使用下面的命令執行它
$ go run main.go
複製程式碼
第五步
開啟瀏覽器,訪問 http://localhost:8000,現在就可以體驗它的執行效果了。
Javascript 真的需要擔心 Go WebAssembly 的威脅麼?
其實根本不用擔心,WebAssembly 的目的是替換前端執行比較耗時的邏輯,不是用來替換前端框架的,它也替換不了。雖然開源社群冒出了一個 github.com/elliotforbe… 的 Go WebAssembly 框架,可以讓你使用 Go 語言編寫前端應用程式。但是我仔細看了一下它的的原始碼,發現它原來只是一個玩具 ^_^,實現上沒幾行程式碼,離真實的應用程式差距太遠。
如果 Go WebAssembly 對 javascript 是個威脅,那麼威脅 javascript 的可不止 Go 語言了,能夠將程式碼編譯成 WebAssembly 位元組碼的語言多達幾十種。
希望將當前 javascript 專案的部分程式碼替換成 Go 語言,成本也是顯而易見的。技術棧的切換成本,位元組碼的載入成本,框架專案持續整合的成本都是需要考慮的點。除非能獲得巨大的效能提升,否則使用純粹的 javascript 來完成專案依然是最佳選擇。
微信搜尋「codehole」,關注公眾號「碼洞」