Wasm on Go

本篇内容,是对极客兔兔:Go WebAssembly (Wasm) 简明教程[1]的实践与记录,主体内容来自这篇博客,推荐阅读原文。


是否需要搭建wasm环境?


WebAssembly 上手[2]

如果是C/C++,需要借助emcc,将C和C++代码编译到WebAssembly和JavaScript。

在Mac上,

brew install emscripten

然后就可以使用 emcc 命令了

通过git clone https://github.com/emscripten-core/emsdk.git的方式编译安装,可能有一堆坑(可能和Python有关,这个项目是用Python写的,WebAssembly开发环境搭建-MAC[3],直接绕道使用brew)

emcc 是 Emscripten 的 C/C++ 到 WebAssembly 编译器。

Emscripten是一个项目,它可以将C和C++代码编译到WebAssembly和JavaScript,从而能在浏览器和Node.js中运行本来需要本地编译的C/C++代码。

emcc 的主要作用和功能如下:

  • 将C/C++源代码编译成WebAssembly二进制格式(.wasm文件)

  • 生成JavaScript源代码用来加载和支持WebAssembly模块

  • 为C/C++代码连接必要的JavaScript运行时支持(如文件I/O、多线程等)

  • 将C/C++标准库封装成JavaScript接口方便调用

  • 支持C++标准特性(如RTTI、异常等)的编译

  • 优化编译配置以减小文件体积

  • 嵌入编译进一步混淆代码以提升性能

emcc实际上是一个非常强大的交叉编译器,可以将大多数C/C++代码通过几次编译转化成浏览器和Node.js可以理解和运行的WebAssembly与JavaScript组合。以实现在web环境中运行原本需要本地编译的代码。


但如果用Go或者Rust,就不需要这东西,这些新语言原生支持wasm



Go对wasm的支持


Go在2018年8月24号发布的1.11版本[4]中,增加了实验性的js/wasm,算是对Wasm进行了原生的支持(当然这个版本更重大的更新是go module这种依赖管理方式)。可以使用go build命令将Go程序编译为WebAssembly字节码。

自那以后便可以说,Go语言原生支持WebAssembly的编译,可以将Go语言编写的程序编译成wasm格式,并在浏览器或其他支持wasm的环境中运行。

此外,Go语言还提供了一些标准库和工具,如syscall/js包和wasm_exec.js库,用于与JavaScript交互和加载WebAssembly模块。


而在此之前,如果想用 Go 开发前端,需用 GopherJS[5],这是一个可将Go转换成能在浏览器中运行的 JavaScript 代码的编译器。

而Go1.11 之后可以直接将 Go 代码编译为 wasm 二进制文件,不再需要转为 JavaScript 代码。(实现 GopherJS 和在 Go 语言中内建支持 WebAssembly 的是同一批人,包括后面会提到的dmitshur[6]大佬)

Go 语言实现的函数可以直接导出供 JavaScript 代码调用,同时,Go 语言内置了 syscall/js 包,可以在 Go 语言中直接调用 JavaScript 函数,包括对 DOM 树的操作


初入门径--使用Go,在网页上弹出 Hello World


(1).新建main.go:

package main

import "syscall/js"

func main() {
 alert := js.Global().Get("alert")
 alert.Invoke("Hello World!")
}

IDE一直飘红, 这是因为需要修改构建标记中的OS为js,Arch为wasm

alt

(2).执行GOOS=js GOARCH=wasm go build -o static/main.wasm, 将 main.go 编译为 static/main.wasm (如果按上面设置了GOOS和GOARCH,则可以直接go build -o static/main.wasm)

此时会新生成一个文件夹static,里面有一个main.wasm文件


(3).执行 cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" static, 将 wasm_exec.js (JavaScript 支持文件,加载 wasm 文件时需要) 拷贝到 static 文件夹

misc是Go源码中的一个文件,其目录结构如下:

alt

在Go语言源码中的misc目录下,包含了一些与特定平台或用途相关的杂项文件。以下是其中的各个目录和文件的作用:

  1. cgo/gmp:

    • fib.go: 包含一个使用GMP库计算斐波那契数列的示例程序。
    • gmp.go: 提供对GMP(GNU Multiple Precision Arithmetic Library)库的Go绑定。
    • pi.go: 包含一个使用GMP库计算圆周率的示例程序。
  2. chrome/gophertool:

    • README.txt: 有关Chrome扩展的说明文档。
    • background.html: Chrome扩展的后台页面HTML。
    • background.js: Chrome扩展的后台页面JavaScript。
    • gopher.js: Chrome扩展的Gopher图标的JavaScript代码。
    • gopher.png: Chrome扩展中使用的Gopher图标。
    • manifest.json: Chrome扩展的清单文件。
    • popup.html: Chrome扩展的弹出页面HTML。
    • popup.js: Chrome扩展的弹出页面JavaScript。
  3. go_android_exec:

    • README: 有关在Android上执行Go程序的说明文档。
    • exitcode_test.go: 包含与退出代码相关的测试。
    • main.go: 包含一个在Android上执行的示例Go程序。
  4. ios:

    • README: 有关在iOS上执行Go程序的说明文档。
    • clangwrap.sh: 提供用于iOS的Clang包装脚本。
    • detect.go: 包含检测iOS环境的Go代码。
    • go_ios_exec.go: 包含在iOS上执行Go程序的Go代码。
  5. linkcheck:

    • linkcheck.go: 包含一个用于检查链接的工具。
  6. wasm:

    • go_js_wasm_exec: 提供Go与JavaScript之间通信的支持。
    • go_wasip1_wasm_exec: 提供Go与JavaScript之间通信的支持(估计与IP地址相关)。
    • wasm_exec.html: 用于在浏览器中运行WebAssembly程序的HTML文件。
    • wasm_exec.js: WebAssembly的JavaScript执行器。
    • wasm_exec_node.js: 用于在Node.js中运行WebAssembly程序的JavaScript文件。

这些文件和目录主要包含了与Go语言在不同平台、环境下的一些特殊需求或功能相关的实用工具和示例。


(4). 在与main.go同级目录下,新建index.html,引用 static/main.wasm 和 static/wasm_exec.js

<html>
<script src="static/wasm_exec.js"></script>
<script>
    const go = new Go();
    WebAssembly.instantiateStreaming(fetch("static/main.wasm"), go.importObject)
        .then((result) => go.run(result.instance));
</script>

</html>

(5). 使用 goexec或者npx http-server 启动一个本地 Web 服务

其中 shurcooL/goexec[7]是Go生态的, 是前面提到的社区中非常活跃的Go项目多次核心贡献者dmitshur大佬写的。 (GOTIME[8]有采访他的谈话节目)

http-party/http-server[9]则是一个简单零配置的命令行 http服务器,nodejs开发.

此处使用后者,执行 npx http-server

alt

在浏览器中打开http://127.0.0.1:8080 (服务器默认使用8080端口,可以通过参数进行配置)

能看到如下弹框

alt

另外可以看到请求的日志

alt


再上层楼--注册(自定义)函数(Register Functions)


上面是在Go中调用js的函数, 但wasm最大的价值之一,是能在浏览器中执行一些对于js来说压力太大的计算密集型操作.

在此用Go实现计算斐波那契数列的函数,并注册到js中,可以让其他js代码调用

新建一个目录,创建一个main.go文件:

package main

import "syscall/js"

// fib 函数计算斐波那契数列的值
func fib(i int) int {
 if i == 0 || i == 1 {
  return 1
 }
 return fib(i-1) + fib(i-2)
}

// fibFunc 是一个JS回调函数,用于在JS中调用fib函数
func fibFunc(this js.Value, args []js.Value) interface{} {
 return js.ValueOf(fib(args[0].Int()))
}

func main() {
 done := make(chan int0)

 // 在全局对象上设置一个名为 "fibFunc" 的JS函数,该函数调用fibFunc回调
 js.Global().Set("fibFunc", js.FuncOf(fibFunc))

 // 通过无限循环,使Wasm程序保持运行状态;fibFunc 如果在 JavaScript 中被调用,会开启一个新的子协程执行。
 <-done
}

以上这段程序演示如何在WebAssembly中使用Go语言编写函数,并通过JavaScript调用这些函数。在这个例子中,fibFunc函数充当了Go和JavaScript之间的桥梁,允许JavaScript代码调用Go中定义的斐波那契数列计算函数。


<font size=1 color="#4682B4"

关于js.Valuejs.ValueOf,在 Go 的 WebAssembly(Wasm)和 JavaScript 交互中,js.Valuejs.ValueOf 是两个相关但不同的概念。

  1. js.Value:

    • js.Value 是 Go 语言中用于表示 JavaScript 值的类型。
    • 它是一个接口,表示可以与 JavaScript 交互的值。
    • js.Value 接口提供了一系列方法,例如 GetSetCall,用于在 Go 中操作 JavaScript 对象和函数。
  2. js.ValueOf:

    • js.ValueOf 是一个函数,用于将 Go 中的基本类型或其他类型转换为 js.Value
    • 当需要将 Go 的值传递给 JavaScript 时,通常使用 js.ValueOf 进行转换。

下面是一个简单的例子,说明了它们的使用:

package main

import (
 "fmt"
 "syscall/js"
)

func main() {
 // 创建一个 js.Value 对象,表示 JavaScript 中的数字 42
 jsNumber := js.ValueOf(42)

 // 在 Go 中调用 JavaScript 的 alert 函数,并传递一个字符串
 js.Global().Get("alert").Invoke(js.ValueOf("Hello from Go!"))

 // 在 Go 中调用 JavaScript 函数,传递和获取参数
 sum := js.Global().Get("add").Call(jsNumber, js.ValueOf(8))
 fmt.Println("Sum:", sum.Int())

 // 在 Go 中定义一个 JavaScript 回调函数,并传递给 JavaScript
 js.Global().Set("goCallback", js.FuncOf(goCallback))
 js.Global().Call("callJsFunction", js.Global().Get("goCallback"))

 // 保持程序运行,以便在浏览器中查看结果
 select {}
}

// goCallback 是一个在 JavaScript 中调用的 Go 回调函数
func goCallback(this js.Value, p []js.Value) interface{} {
 fmt.Println("Callback called from JavaScript!")
 return nil
}

在这个例子中:

  • 使用 js.ValueOf 将 Go 的值转换为 js.Value
  • 使用 js.Global().Get("alert").Invoke 调用 JavaScript 的 alert 函数。
  • 使用 js.Global().Get("add").Call 调用 JavaScript 的自定义函数,并传递参数。
  • 使用 js.FuncOf 创建一个 JavaScript 可调用的 Go 回调函数,然后通过 js.Global().Set 注册到全局对象。

新建index.html:


<html>

<body>
<input id="num" type="number" />
<button id="btn" onclick="ans.innerHTML=fibFunc(num.value * 1)">Click</button>
<p id="ans">1</p>
</body>

<script src="static/wasm_exec.js"></script>
<script>
    const go = new Go();
    WebAssembly.instantiateStreaming(fetch("static/main.wasm"), go.importObject)
        .then((result) => go.run(result.instance));
</script>

</html>

相比于之前的页面,新增了一段块,增加一个输入框,按钮,文本框. 并给按钮添加一个点击事件,将计算结果显示在文本框中

执行GOOS=js GOARCH=wasm go build -o static/main.wasm, 将 main.go 编译为 static/main.wasm

执行 npx http-server

输入任意数字,能正确计算出结果

alt

如果输入的数字较大,浏览器能直接把CPU跑满..

alt

牛刀再试---操作 DOM


上面例子中index.html中DOM元素的操作,是靠嵌入在 HTML 中的 JavaScript 代码。

<input id="num" type="number" />
<button id="btn" onclick="ans.innerHTML=fibFunc(num.value * 1)">Click</button>
<p id="ans">1</p>
  • <input> 元素的 id="num" 用于输入数字的输入字段。

  • <button> 元素的 id="btn" 具有一个 onclick 属性,其中包含 JavaScript 代码。

  • onclick 属性中的 JavaScript 代码是 ans.innerHTML=fibFunc(num.value * 1)。它将具有 id="ans" 的元素的 innerHTML 设置为调用名为 fibFunc 的函数的结果,该函数使用 num 输入字段中输入的值。

这段 JavaScript 代码负责在按钮点击时更新具有 id="ans" 的段落(<p>)元素的内容。fibFunc 函数是一个斐波那契函数,接收来自 num 输入字段的输入值,计算斐波那契值,并在具有 id="ans" 的段落中显示它。


希望能够通过Go而不是js来操作DOM元素

新建一个项目,名叫dom,

新建index.html,去除操作DOM部分的js代码:


<html>

<body>
<input id="num" type="number" />
<button id="btn" onclick="ans.innerHTML=fibFunc(num.value * 1)">Click</button>
<p id="ans">1</p>
</body>

<script src="static/wasm_exec.js"></script>
<script>
    const go = new Go();
    WebAssembly.instantiateStreaming(fetch("static/main.wasm"), go.importObject)
        .then((result) => go.run(result.instance));
</script>



</html>

新建main.go:

package main

import (
 "strconv"
 "syscall/js"
)

func fib(i int) int {
 if i == 0 || i == 1 {
  return 1
 }
 return fib(i-1) + fib(i-2)
}

var (
 document = js.Global().Get("document")
 numEle   = document.Call("getElementById""num")
 ansEle   = document.Call("getElementById""ans")
 btnEle   = js.Global().Get("btn")
)

func fibFunc(this js.Value, args []js.Value) interface{} {
 v := numEle.Get("value")
 if num, err := strconv.Atoi(v.String()); err == nil {
  ansEle.Set("innerHTML", js.ValueOf(fib(num)))
 }
 return nil
}

func main() {
 done := make(chan int0)
 btnEle.Call("addEventListener""click", js.FuncOf(fibFunc))
 <-done
}

这是一个使用 Go 语言和 WebAssembly(Wasm)的简单示例程序,它通过网页上的按钮触发斐波那契数列的计算。

  1. fib 函数定义了一个递归的斐波那契数列计算方法。

  2. main 函数中,通过 js.Global().Get("document") 获取全局文档对象,然后使用 Call 方法获取 HTML 文档中的元素,包括输入框 (num)、段落 (ans) 和按钮 (btn)。

  3. fibFunc 函数是一个回调函数,它被注册到按钮的点击事件上。当按钮被点击时,这个函数会读取输入框的值,将其转换为整数,然后调用斐波那契函数计算结果,并将结果更新到段落中。

  4. main 函数中,通过 js.FuncOf(fibFunc) 将 Go 函数转换为 JavaScript 函数,然后通过 Call 方法将这个 JavaScript 函数注册到按钮的点击事件上。

  5. done := make(chan int, 0)<-done 是为了保持程序运行,以便持续监听事件。

该程序利用 Go 和 JavaScript 的互操作性,通过 WebAssembly 在浏览器中执行 Go 代码,实现了一个简单的交互式网页。

cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" static

GOOS=js GOARCH=wasm go build -o static/main.wasm

npx http-server

alt

紫禁之巅---使用回调函数(Callback Functions)


在 Js 中,异步+回调很常见,如请求一个 Restful API,注册一个回调函数,待数据获取到,再执行回调函数的逻辑. 这期间程序可以继续做其他事。Go 语言可通过协程实现异步。

假设 fib 的计算非常耗时,那么可以启动注册一个回调函数,待 fib 计算完成后,再把计算结果显示出来。

先修改 main.go,使得 fibFunc 支持传入回调函数。

新建一个目录称为callback

main.go:

修改fibFunc,使其支持传入回调函数

package main

import (
 "syscall/js"
 "time"
)

func fib(i int) int {
 if i == 0 || i == 1 {
  return 1
 }
 return fib(i-1) + fib(i-2)
}

func fibFunc(this js.Value, args []js.Value) interface{} {
 callback := args[len(args)-1]
 go func() {
  time.Sleep(3 * time.Second)
  v := fib(args[0].Int())
  callback.Invoke(v)
 }()

 js.Global().Get("ans").Set("innerHTML""Waiting 3s...")
 return nil
}

func main() {
 done := make(chan int0)
 js.Global().Set("fibFunc", js.FuncOf(fibFunc))
 <-done
}

这是一个使用 Go 语言和 WebAssembly(Wasm)的示例程序,演示了在计算斐波那契数列时如何通过 Go 异步处理,并在等待期间更新网页。

  1. fib 函数定义了一个递归的斐波那契数列计算方法。

  2. fibFunc 函数是一个回调函数,它被注册到 JavaScript 中的 fibFunc 函数。在计算斐波那契数列时,它通过 JavaScript 的回调方式异步执行,模拟了一个耗时的操作。在计算完成后,通过 callback.Invoke(v) 将结果传递给 JavaScript 回调函数。

  3. main 函数中,通过 js.Global().Set("fibFunc", js.FuncOf(fibFunc)) 将 Go 中的 fibFunc 函数注册到全局,以便 JavaScript 可以调用它。

  4. time.Sleep(3 * time.Second) 模拟一个耗时的操作,延迟3秒。

  5. 在等待期间,通过 js.Global().Get("ans").Set("innerHTML", "Waiting 3s...") 将网页上显示的信息更新为 "Waiting 3s..."。

通过这个示例,展示了如何在 WebAssembly 中使用 Go 处理异步操作,并在等待时更新网页内容。


  • 假设调用 fibFunc 时,回调函数作为最后一个参数,那么通过 args[len(args)-1] 便可获取到该函数。这与其他类型参数的传递并无区别。

  • 使用 go func() 启动子协程,调用 fib 计算结果,计算结束后,调用回调函数 callback,并将计算结果传递给回调函数,使用 time.Sleep() 模拟 3s 的耗时操作。

  • 计算结果出来前,先在界面上显示 Waiting 3s...

新建index.html,为按钮添加点击事件,调用 fibFunc


<html>

<body>
<input id="num" type="number" />
<button id="btn" onclick="fibFunc(num.value * 1, (v)=> ans.innerHTML=v)">Click</button>
<p id="ans"></p>
</body>

<script src="static/wasm_exec.js"></script>
<script>
    const go = new Go();
    WebAssembly.instantiateStreaming(fetch("static/main.wasm"), go.importObject)
        .then((result) => go.run(result.instance));
</script>



</html>
  • 为 btn 注册了点击事件,第一个参数是待计算的数字,从 num 输入框获取。
  • 第二个参数是一个回调函数,将参数 v 显示在 ans 文本框中。

cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" static

GOOS=js GOARCH=wasm go build -o static/main.wasm

npx http-server

alt
alt

会先显示 Waiting 3s...,3s过后显示计算结果


更多推荐阅读

go编译wasm与调用[10]

Go 中的 WASM 很棒:全网最全示例教程[11]

【Go】【WebAssembly】【wasm】基于go打包的网页wasm[12]

可能是世界上最简单的用 Go 来写 WebAssembly 的教程[13]

如何在 Go 中使用 Wasm:浅聊 WebAssembly[14]

参考资料
[1]

极客兔兔:Go WebAssembly (Wasm) 简明教程: https://geektutu.com/post/quick-go-wasm.html

[2]

WebAssembly 上手: https://www.cnblogs.com/Wayou/p/webassembly_quick_start.html

[3]

WebAssembly开发环境搭建-MAC: https://blog.youkuaiyun.com/daill894/article/details/103815099

[4]

1.11版本: https://tip.golang.org/doc/go1.11

[5]

GopherJS: https://github.com/gopherjs/gopherjs

[6]

dmitshur: https://github.com/dmitshur

[7]

shurcooL/goexec: https://github.com/shurcooL/goexec

[8]

GOTIME: https://changelog.com/gotime

[9]

http-party/http-server: https://github.com/http-party/http-server

[10]

go编译wasm与调用: https://www.jianshu.com/p/69645c8bf57c

[11]

Go 中的 WASM 很棒:全网最全示例教程: https://www.qinglite.cn/doc/66216476ed6fe796d

[12]

【Go】【WebAssembly】【wasm】基于go打包的网页wasm: https://blog.youkuaiyun.com/sky529063865/article/details/126005525

[13]

可能是世界上最简单的用 Go 来写 WebAssembly 的教程: https://www.jiqizhixin.com/articles/2020-06-30-10

[14]

如何在 Go 中使用 Wasm:浅聊 WebAssembly: https://juejin.cn/post/7195217413091262523

本文由 mdnice 多平台发布

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值