不安分的 Go 语言开始入侵 Web 前端领域了!, WebAssembly

参考:https://blog.csdn.net/csdnnews/article/details/84038848

从 Go 语言诞生以来,它就开始不断侵蚀 Java 、C、C++ 语言的领地。今年下半年 Go 语言发布了 1.11 版本,引入了 WebAssembly 技术,浏览器端 Javascript 的垄断地位也开始遭遇 Go 语言的攻击。这次不同以往,它意味着 Go 语言从后端渗透进了前端,进入了一个全新的世界。

WebAssembly 是一项比较新的技术,只有比较现代的浏览器才支持 WebAssembly,例如 Chrome、FireFox浏览器。

Go 编译器可以将代码编译成 WebAssembly 二进制字节码,被浏览器以静态资源的形式加载进来后转换成 Javascript 模块。有了这个模块,浏览器可以直接操纵 Go 语言生成的二进制字节码逻辑。同时在 Go 语言编写的代码中可以直接读写浏览器里面 Javascript 运行时对象,这样就完成了 Javascript 和 Go 代码的双向交互。

Go 语言直到 1.11 版本之后才开启了对 WebAssembly 的支持。如需体验,必须升级。

第一步

使用 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 二进制字节码。(我的是windows)

set GOARCH=wasmset 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">
    <meta http-equiv="expires" content="0"> 
    <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"  oninput="callFib()"/>
    <input type="text"  />
</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))
}

现在在项目目录下有四个文件:main.go、index.html、fib.wasm、wasm_exec.js

用以下命令编译运行:

go run main.go

第五步

打开浏览器,访问 http://localhost:8000,现在就可以体验它的运行效果了。

Javascript 真的需要担心 Go WebAssembly 的威胁么?

其实根本不用担心,WebAssembly 的目的是替换前端运行比较耗时的逻辑,不是用来替换前端框架的,它也替换不了。虽然开源社区冒出了一个 https://github.com/elliotforbes/oak 的 Go WebAssembly 框架,可以让你使用 Go 语言编写前端应用程序。但是我仔细看了一下它的的源码,发现它原来只是一个玩具,实现上没几行代码,离真实的应用程序差距太远。

如果 Go WebAssembly 对 Javascript 是个威胁,那么威胁 Javascript 的可不止 Go 语言了,能够将代码编译成 WebAssembly 字节码的语言多达几十种。

希望将当前 Javascript 项目的部分代码替换成 Go 语言,成本也是显而易见的。技术栈的切换成本,字节码的加载成本,框架项目持续集成的成本都是需要考虑的点。除非能获得巨大的性能提升,否则使用纯粹的 Javascript 来完成项目依然是最佳选择。

原作者简介:老钱,著有《Redis 深度历险》《深入理解 RPC》《快学 Go 语言》。

熟练使用 Java、Python、Golang 等多种计算机语言,开发过游戏,制作过网站,写过消息推送系统和 MySQL 中间件,实现过开源的 ORM 框架、Web 框架、RPC 框架等,目前任职掌阅服务端技术专家。