如何通过 JavaScript 运行用 Go 编写的 WebAssembly 模块? 上

简介: 如何通过 JavaScript 运行用 Go 编写的 WebAssembly 模块?

最近在着手重构 presence.js 2.0,其中编解码模块通过 WebAssembly 实现。但代码是一年多之前写的,期间一直没有碰过,现在已经比较陌生了,借着这次机会,我打算重新梳理一下相关的知识,带大家完成一个 Go 的 WebAssembly 程序。


什么是 WebAssembly?


JavaScript 一直是浏览器能够直接运行的唯一编程语言。

JavaScript 经受住了时间的考验,它能够提供大多数 Web 应用所需的性能。但是当涉及到 3D 游戏、VR、AR 和图像编辑之类的应用时,JavaScript 并不完全提供很好的性能,因为它是一门解释性语言。尽管 Gecko 和 V8 等 JavaScript 引擎具有即时编译(JIT)功能,但 JavaScript 仍然无法提供现代 Web 应用所需要的更高性能。

WebAssembly 简称 wasm,它的目标是解决性能这个问题。WebAssembly 是一种浏览器可以运行的虚拟汇编语言。当我们说到虚拟的时候,就意味着它不能在底层硬件上本地运行。

由于浏览器可以在任何架构上运行,所以浏览器不可能直接在底层硬件上运行 WebAssembly。但是这种高度优化的虚拟程序集格式在现代浏览器中的处理速度比普通 JavaScript 快得多,因为它经过编译并且比 JavaScript 更接近硬件架构。

下图显示了与 Javascript 相比,WebAssembly 在堆栈中的位置。它比 JavaScript 更接近硬件。

image.png

现在所有的 JavaScript 引擎都支持运行 WebAssembly 的虚拟程序集代码。


WebAssembly 会取代 JavaScript 吗?


WebAssembly 并不是要取代 JavaScript。它会和 JavaScript 一起运行,并且专注处理 Web 应用的性能关键组件。可以从 JavaScript 调用 WebAssembly,反之亦然。

WebAssembly 通常不是人去写出来的,而是从其他高级编程语言交叉编译得到的。例如可以将 Go、C、C++ 和 Rust 代码交叉编译为 WebAssembly 代码。因此,已经用其他编程语言编码的模块可以交叉编译成 WebAssembly 并直接在浏览器中使用。


我们要开发什么应用?


在本文中,我将交叉编译一个 Go 应用程序到 WebAssembly 并在浏览器上运行它。

我们会创建一个用于格式化 JSON 的简单应用程序。我们把合规的 JSON 字符串作为参数传入,它会将 JSON 格式化并打印出来。

例如,如果输入 JSON 是这样的:


{"user_id": "571401777717031","user_name": "代码与野兽","description": "关注real-time web、低代码、数据可视化、web3等领域。"}

它会被格式化,并且显示在浏览器中。


{
  "user_id": "571401777717031",
  "user_name": "代码与野兽",
  "description": "关注real-time web、低代码、数据可视化、web3等领域。"
}

这个程序的名字我们叫做 prettyJson。

这个程序需要确保 Go Versions >= 1.13。

image.png


Hello World WebAssembly 程序


在开始编写 prettyJson 之前,我们先实现一个用 Go 编写的 hello world 程序,通过交叉编译为 WebAssembly,然后在浏览器上运行。

然后基于这个简单的程序,逐渐转换成我们的 prettyJson 程序。

我们首选在 gopath/bin 目录中创建以下目录结构。


bin
└── webassembly
    ├── assets
    └── cmd
        ├── server
        └── wasm

然后运行 go mod init v2 创建模块。


go mod init v2

在 /webassembly/cmd/wasm 中创建 main.go 文件,并打印一句话。


package main
import (
  "fmt"
)
func main() {
  fmt.Println("Go Web Assembly")
}

将上面的 Go 程序交叉编译成 WebAssembly。

下面的命令可以交叉编译这个 Go 程序并将输出二进制文件到 assets 文件夹中。


cd /go/bin/webassembly/cmd/wasm/  
GOOS=js GOARCH=wasm go build -o  ../../assets/json.wasm

上面的命令中 wasm 是 WebAssembly 架构的缩写形式。

运行上述命令会在 assets 目录中创建 WebAssembly 模块,文件名是 json.wasm。

现在我们已经成功地把第一个 Go 程序交叉编译到 WebAssembly。

如果你尝试在终端中运行编译后的二进制文件,你会得到错误:exec format error: ./json.wasm。

这是因为这个二进制文件是一个 wasm 二进制文件,应该在浏览器沙箱中运行。Linux/Mac 操作系统不理解这个二进制文件的格式。

image.png


Javascript 胶水代码


我之前提到,WebAssembly 是和 JavaScript 并存的。所以我们要想运行这段代码,需要一些 JavaScript 代码来导入我们刚刚创建的 WebAssembly 模块,这种 JavaScript 代码的作用我们可以称为 JavaScript 胶水。

这个 JavaScript 代码已在安装 Go 时下载到了本地,我们只需要把它复制到我们的 assets 目录就可以了。


cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" ~/go/bin/webassembly/assets/

上面的命令会把包含运行 WebAssembly 的胶水代码 wasm_exec.js 复制到 assets 目录中。

assets 文件夹将会包含所有 HTML、JavaScript 和 wasm 代码,最后我们会使用 Web 服务器来运行这些代码。


index.html


现在我们已经准备好 wasm 二进制文件和胶水代码。下一步是创建 index.html 文件并导入我们的 wasm 二进制文件。

在 assets 目录中创建一个 index.html 文件,内容如下。


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>hello wasm</title>
    <script src="wasm_exec.js"></script>
    <script>
      const go = new Go();
      WebAssembly.instantiateStreaming(
        fetch("json.wasm"),
        go.importObject
      ).then((result) => {
        go.run(result.instance);
      });
    </script>
  </head>
  <body></body>
</html>

文件中运行 WebAssembly 模块的代码可以在 WebAssembly Wiki 中找到。

创建 index.html 后的当前目录结构如下所示。


bin
└── webassembly
    ├── assets
    │   ├── index.html
    │   ├── json.wasm
    │   └── wasm_exec.js
    └── cmd
        ├── server
        └── wasm
            └── main.go

虽然 index.html 中的代码是模板代码,但我们可以稍微理解一下。

instantiateStreaming 函数用来初始化 json.wasm 模块。这个函数返回一个 WebAssembly 实例,其中包含了可以从 JavaScript 调用的 WebAssembly 函数列表。这是从 JavaScript 调用 wasm 函数所必需的列表。


Server


现在我们已经准备好了 JavaScript 胶水代码、index.html 和 wasm 二进制文件。唯一缺少的部分就是一个 Web 服务器来托管 assets 文件夹中的内容。

在 server 目录中创建一个 main.go文件。

创建 main.go 之后的目录结构如下。


bin  
└── webassembly
    ├── assets
    │   ├── index.html
    │   ├── json.wasm
    │   └── wasm_exec.js
    └── cmd
        ├── server
        |   └── main.go
        └── wasm
            └── main.go

然后在 /webassembly/cmd/server/main.go 中实现一个 Web 服务器。


package main
import (
  "fmt"
  "net/http"
)
func main() {
  fs := http.FileServer(http.Dir("assets"))
  err := http.ListenAndServe(":9090", http.StripPrefix("/", fs))
  if err != nil {
    fmt.Println("Failed to start server", err)
    return
  }
}

上面的代码创建了一个文件服务器。它监听 9090 端口,根目录是 assets 文件夹。

现在启动服务器,看看第一个 WebAssembly 程序能否正常运行。


cd ~/go/bin/webassembly/cmd/server/  
go run main.go

现在服务器正在监听 9090 端口。

我们在浏览器并输入 http://localhost:9090/,可以看到页面是空的。

我们现在来查看 JavaScript 控制台。

image.png

我们可以看到,在控制台中看到打印了 Go Web Assembly。

现在我们已经成功运行了第一个使用 Go 编写的 Web Assembly 程序。我们从 Go 交叉编译的 Web Assembly 程序已经由服务器提供给了浏览器,并且被浏览器的 Javascript 引擎成功执行。



相关文章
|
12天前
|
数据挖掘 API Go
《Go 简易速速上手小册》第7章:包管理与模块(2024 最新版)(下)
《Go 简易速速上手小册》第7章:包管理与模块(2024 最新版)
38 1
|
1天前
|
数据可视化 算法 Java
了解go语言运行时工具的作用
【5月更文挑战第16天】本文简介`runtime`库提供系统调用包装、执行跟踪、内存分配统计、运行时指标和剖析支持。`internal/syscall`封装系统调用,保证uintptr参数有效。`trace`用于执行跟踪,捕获各种事件,如goroutine活动、系统调用和GC事件。`ReadMemStats`提供内存分配器统计。`metrics`接口访问运行时定义的度量,包括CPU使用、GC和内存信息。`coverage`支持代码覆盖率分析,`cgo`处理C语言交互,`pprof`提供性能剖析工具集成。这些功能帮助优化和理解Go程序的运行行为。
31 6
|
12天前
|
数据采集 JavaScript 前端开发
使用Go和JavaScript爬取股吧动态信息的完整指南
本文介绍了如何使用Go和JavaScript构建网络爬虫,从股吧网站抓取实时股市信息。通过设置代理服务器以应对反爬策略,利用`got`库执行JavaScript提取动态数据,如用户讨论和市场分析。示例代码展示了爬虫的实现过程,包括浏览器实例创建、代理配置、JavaScript执行及数据打印。此方法有助于投资者及时获取市场资讯,为决策提供支持。
使用Go和JavaScript爬取股吧动态信息的完整指南
|
12天前
|
移动开发 资源调度 前端开发
nbcio-vue下载安装后运行报错,diagram-js没有安装
nbcio-vue下载安装后运行报错,diagram-js没有安装
15 0
|
12天前
|
存储 Java Linux
聊聊Go程序是如何运行的
本文作者 **sharkChili** 是一名 Java 和 Go 语言开发者,同时也是 CSDN 博客专家和 JavaGuide 维护者。文章探讨了 Go 语言的执行过程,从汇编角度出发,解释了如何从 `main.go` 文件开始,经过入口跳转、参数拷贝、启动协程、运行 `g0` 的 `main` 方法等步骤,最终执行到用户定义的 `main` 函数。文章还展示了相关汇编代码片段,并提供了运行时检查、系统初始化和调度器初始化的细节。结尾提到,有兴趣的读者可以加入作者创建的交流群进行深入讨论。
16 0
|
12天前
|
JavaScript 前端开发 测试技术
编写JavaScript模块化代码主要涉及将代码分割成不同的文件或模块,每个模块负责处理特定的功能或任务
【5月更文挑战第10天】编写JavaScript模块化代码最佳实践:使用ES6模块或CommonJS(Node.js),组织逻辑相关模块,避免全局变量,封装细节。利用命名空间和目录结构,借助Webpack处理浏览器环境的模块。编写文档和注释,编写单元测试以确保代码质量。通过这些方法提升代码的可读性和可维护性。
21 3
|
12天前
|
消息中间件 监控 JavaScript
Node.js中的进程管理:child_process模块与进程管理
【4月更文挑战第30天】Node.js的`child_process`模块用于创建子进程,支持执行系统命令、运行脚本和进程间通信。主要方法包括:`exec`(执行命令,适合简单任务)、`execFile`(安全执行文件)、`spawn`(实时通信,处理大量数据)和`fork`(创建Node.js子进程,支持IPC)。有效的进程管理策略涉及限制并发进程、处理错误和退出事件、使用流通信、谨慎使用IPC以及监控和日志记录,以确保应用的稳定性和性能。
|
12天前
|
安全 Go API
【Go 语言专栏】Go 语言的模块版本控制与管理
【4月更文挑战第30天】Go 语言模块版本控制始于 1.11 版本,提供了一种替代 GOPATH 的更灵活的依赖管理方式。语义化版本号(主、次、修订版本号)用于标识模块变化和兼容性。开发中可采取固定、范围或最新版本策略。`go mod`工具用于管理模块,升级时注意兼容性、测试和文档更新。实践案例展示如何有效控制与管理模块版本,确保项目稳定、兼容和可维护。随着 Go 语言的发展,模块版本管理将持续优化。
|
12天前
|
编解码 JavaScript 前端开发
【专栏】介绍了字符串Base64编解码的基本原理和在Java、Python、C++、JavaScript及Go等编程语言中的实现示例
【4月更文挑战第29天】本文介绍了字符串Base64编解码的基本原理和在Java、Python、C++、JavaScript及Go等编程语言中的实现示例。Base64编码将24位二进制数据转换为32位可打印字符,用“=”作填充。文中展示了各语言的编码解码代码,帮助开发者理解并应用于实际项目。
|
12天前
|
缓存 JavaScript 前端开发
Node.js的模块系统:CommonJS模块系统的使用
【4月更文挑战第29天】Node.js采用CommonJS作为模块系统,每个文件视为独立模块,通过`module.exports`导出和`require`引入实现依赖。模块有独立作用域,保证封装性,防止命名冲突。引入的模块会被缓存,提高加载效率并确保一致性。利用CommonJS,开发者能编写更模块化、可维护的代码。