使用 Go 编译 WebAssembly
概述
如果你希望在浏览器中运行像 C++
、Rust
或 Go
这样的代码,并且对性能要求较高,而不是使用 JavaScript
,那么学习 WebAssembly
绝对是你的最佳选择!
WebAssembly
是一种低级字节码格式,可以在现代浏览器中运行。它允许你将其他编程语言(如 C++
、Rust
或 Go
)的代码编译为 WebAssembly
模块,然后通过 JavaScript
在浏览器中加载和执行这些模块。
实际上,许多编程语言都可以被编译成 .wasm
,而 Go
只是其中之一。而目前几乎所有主流的现代浏览器也都支持了 WebAssembly
,这使得开发者能够利用其高性能特性来在浏览器环境中运行编译语言的代码。
通过使用 WebAssembly
,你可以获得接近原生执行速度的性能,同时仍然能够利用 Web
平台的优势,比如访问 DOM
或处理事件等。这为那些需要超出 JavaScript
能力或 存在性能瓶颈 的应用场景提供了很好的解决方案,例如游戏开发、视频剪辑、3D
渲染、音乐制作等。
应用案例
当谈论到 WebAssembly
这项技术时,可能对一些开发者来说它还相对陌生。然而,事实上,WebAssembly
已经在业界取得了一些非常令人惊艳的成熟应用,尤其是在前端浏览器中的应用。
以下是一些具体例子:
- 2017年,
Figma
使用WebAssembly
,使启动时间快了3倍。 - 2018年,
AutoCAD
被编译为WebAssembly
,完整移植到浏览器中。 - 2019年,
Google Earth
被编译为WebAssembly
,移植到浏览器中,实现了跨浏览器支持。 - 2020年,
Google Meet
借助WebAssembly
实现了视频的实时背景虚化以及背景替代。 - 2022年,
Adobe Photoshop
得益于WebAssembly
,也将其复杂的应用移植到了浏览器中。
Wasm 是什么
WebAssembly 1.0 has shipped in 4 major browser engines: Chrome, Firefox, Edge, or Safari.
WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications.
WebAssembly
是一项新兴的标准,其目标是定义一个安全、可移植、大小和加载时间高效的二进制编译器目标,提供接近本地代码的性能 --------- 为 Web
提供了一种虚拟 CPU
。WebAssembly
正在由 W3C
社区组(CG
)开发,其中的成员包括 Mozilla
、Microsoft
、Google
和 Apple
。
Wasm 发展历程
WebAssembly
真正的发展可以追溯到 2015
年,当时由多家主流浏览器厂商(包括 Google
、Mozilla
、微软和苹果)共同推动其标准化。自那时以来,WebAssembly
已经取得了显著的发展和广泛应用。
目前,几乎所有的主流现代浏览器都支持 WebAssembly
。在短短几年内取得了巨大的进展,成为在浏览器中运行高性能的编程语言代码的关键技术。随着更多开发者采用 WebAssembly
,预计将会有更多的工具、库和框架出现,进一步推动其发展。
Wasm 与 JavaScript
如果说 JavaScript 是前端的传统老大哥,那么 WebAssembly 绝对是前端的当红辣子鸡!
在当前的 Web 2.0/3.0
时代,前端开发早已不再局限于简单的页面渲染和从后端获取数据,而是希望能够参与更多的逻辑处理。
然而,众所周知,JavaScript
在性能方面存在一些限制,因为动态解释和速度性能很难兼得。在大数据时代,如果将所有逻辑处理都放在后端,无疑会给服务器端带来巨大的压力。正因如此,WebAssembly
的出现为前端开发提供了另一种思路与选择。
对于需要进行大量数据处理或计算的任务,或者追求极致性能的场景(例如计算MD5
值、图像处理、密码学操作等),利用 WebAssembly
可能会带来显著的性能优势。通过使用诸如 C/C++
或 Rust
等语言编写高度优化的代码,并将其编译为 WebAssembly
模块,在现代浏览器中运行,可以实现更快的执行速度。
下表展示了 WebAssembly
和 JavaScript
的一些区别:
WebAssembly | JavaScript |
---|---|
使用二进制格式 | 使用文本格式 |
面向底层编程 | 面向高级编程 |
执行效率高 | 解释执行 |
接近机器码 | 动态和灵活 |
跨平台 | 浏览器原生支持 |
Wasm
与传统前端三剑客(html/css/js
)的关系:
wasmtime Runtime
Go 1.21 版本 go.dev/blog/wasi
- 安装
wasmtime
命令:
Bash
➜ brew install wasmtime
- 编写
Go
代码:
Go
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello Wasm!")
}
- 使用
wasmtime
工具来创建和执行WebAssembly
模块:
Bash
# 将Go代码编译为WebAssembly模块
➜ GOOS=wasip1 GOARCH=wasm go build -o main.wasm main.go
# 检查文件类型
➜ file main.wasm
main.wasm: WebAssembly (wasm) binary module version 0x1 (MVP)
# 执行WebAssembly模块
➜ wasmtime main.wasm
Hello Wasm!
简单教程
示例一 | Go + Wasm + Js 实现控制台打印 hello world
目录结构:
Bash
➜ rookie tree
.
├── Makefile # Makefile 文件,用于定义项目的构建和任务
├── assets
│ ├── index.html # HTML 文件,用于构建网页
│ ├── main.wasm # WebAssembly 模块,将在浏览器中执行
│ ├── script.js # JavaScript 文件,包含与 WebAssembly 集成相关的代码
│ └── wasm_exec.js # WebAssembly Go 运行时库,支持在浏览器中运行 Go WebAssembly 模块(该文件通常是由 Go 工具链提供,只需要将其复制到你的项目目录中即可)
├── cmd
│ ├── server # 运行服务器应用的目录
│ │ └── main.go
│ └── wasm # WebAssembly 应用的源代码目录
│ └── main.go
└── go.mod
4 directories, 8 files
- 前置准备
Bash
# 新建目录
mkdir rookie && cd rookie/
# 初始化工程
go mod init rookie
# 以更简洁的方式创建多层级文件夹
mkdir -p assets cmd/{server,wasm}
- 新建
assets/index.html
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello WebAssembly Golang</title>
<link rel="icon" href="https://webassembly.org/favicon.ico" type="image/x-icon">
<!-- 首先导入 wasm_exec.js 是非常重要的,因为它为后续的 WebAssembly 模块提供了必要的运行时支持 -->
<!-- 这意味着在 script.js 中使用 WebAssembly 之前,wasm_exec.js 必须已经加载和执行 -->
<script src="wasm_exec.js" type="text/javascript"></script>
</head>
<body>
<h1>Hello WebAssembly</h1>
<!-- 加载和实例化指定的 WebAssembly 模块 -->
<script src="script.js" type="text/javascript"></script>
</body>
</html>
- 新建
assets/script.js
JavaScript
// 创建一个名为goWasm的新Go实例,用于将Go编程语言与WebAssembly集成
const goWasm = new Go();
// 实例化名为 "main.wasm" 的 WebAssembly 模块,同时传递 importObject 以供导入
WebAssembly.instantiateStreaming(fetch("main.wasm"), goWasm.importObject)
.then((result) => {
// 运行实例化后的模块
goWasm.run(result.instance);
})
.catch((err) => {
// 如果发生错误,将其抛出处理
throw err;
});
- 新建
cmd/wasm/main.go
(这里是主要的业务逻辑)
Go
package main
import (
"fmt"
)
func main() {
// 使用 ANSI 转义序列来设置颜色和字体样式(蓝色加粗)
fmt.Println("\033[1;34mHello Web Assembly from Go!\033[0m")
}
- 新建
cmd/server/main.go
(如果你有其他启动方式,也可忽略此步骤)
Go
package main
import (
"fmt"
"log"
"net/http"
"path/filepath"
"runtime"
"strconv"
)
func main() {
// 监听端口号
const port = 8888
// 构建 assets 目录的路径
assetsDir := func() string {
_, currentFile, _, _ := runtime.Caller(0)
dirPath := filepath.Dir(currentFile)
parentDir := filepath.Dir(filepath.Dir(dirPath))
return filepath.Join(parentDir, "assets")
}()
// 注册 http 请求处理函数
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.FileServer(http.Dir(assetsDir)).ServeHTTP(w, r)
})
// 打印 console log
fmt.Printf("Run the dev server: http://localhost:%d\n", port)
// 启动 web 服务
log.Fatal(http.ListenAndServe(":"+strconv.Itoa(port), nil))
}
- 编写
Makefile
💡 想要对 Makefile 有更全面的了解,可参考另一篇文章:《使用 Makefile 构建你的 Go 项目》
Makefile
.PHONY: action, build, copy, serve
GO := $(HOME)/go/go1.21.0/bin/go # 替换成你的go sdk实际位置
# 用于编译 Go 代码为 WebAssembly 格式,并将结果输出到 assets/main.wasm 文件中
build:
GOOS=js GOARCH=wasm $(GO) build -o assets/main.wasm cmd/wasm/main.go
# 用于将 wasm_exec.js 复制到 assets/ 目录中,以便在浏览器中运行 Go WebAssembly 应用
copy:
cp -a `$(GO) env GOROOT`/misc/wasm/wasm_exec.js assets/
action: build copy
serve:
$(GO) run cmd/server/main.go
- 编译
Wasm
Bash
# 执行action目标
make action
- 测试运行
Bash
# 执行serve目标
make serve
打开 http://localhost:8888 将会看到:
示例二 | 使用 Go 编写自定义 Wasm 函数
还是复用上述的目录结构,部分内容稍作改动:
- 更新
assets/index.html
Html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello WebAssembly Golang</title>
<link rel="icon" href="https://webassembly.org/favicon.ico" type="image/x-icon">
<style>
body {
width: 30%;
min-height: 300px;
position: fixed;
top: 0;
left: 0;
margin: 0;
padding: 20px;
box-sizing: border-box;
border: 1px solid #ccc;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
#get-input {
margin: 10px 0;
}
</style>
<script src="wasm_exec.js" type="text/javascript"></script>
</head>
<body>
<h1>Hello WebAssembly</h1>
<div class="container">
<button id="get-css">Get CSS</button>
<div id="get-input">
<label for="input-text">Input Text:</label>
<input type="text" id="input-text" name="input-text" value="" />
<div id="result"></div>
</div>
</div>
<script src="script.js" type="text/javascript"></script>
</body>
</html>
- 更新
assets/script.js
JavaScript
const goWasm = new Go();
WebAssembly.instantiateStreaming(fetch("main.wasm"), goWasm.importObject)
.then((result) => {
goWasm.run(result.instance);
// 添加事件监听器到 "get-css" 按钮
document.getElementById("get-css").addEventListener("click", () => {
// 创建一个新的 <style> 元素
const cssString = getCss();
// 创建一个新的 <style> 元素
const styleElement = document.createElement("style");
// 将获取的 CSS 字符串设置为 <style> 元素的文本内容
styleElement.textContent = cssString;
// 将 <style> 元素添加到页面的 <head> 部分,从而应用 CSS 样式
document.head.appendChild(styleElement);
});
// 获取必要的 DOM 元素
const inputText = document.getElementById('input-text');
const resultDiv = document.getElementById('input-result');
// 添加事件监听器到输入框
inputText.addEventListener('input', () => {
// 获取输入框中的值
const inputValue = inputText.value;
// 调用 inputHtml 函数以获取 HTML 字符串
const htmlString = window.setHtml(inputValue);
// 渲染 HTML 字符串到结果区域
renderHtml(htmlString);
});
// 将 HTML 字符串渲染到结果区域
const renderHtml = (html) => {
// 清空结果区域
resultDiv.innerHTML = '';
// 创建一个临时容器元素
const tempContainer = document.createElement('div');
tempContainer.innerHTML = html;
// 将临时容器中的子节点添加到结果区域
while (tempContainer.firstChild) {
resultDiv.appendChild(tempContainer.firstChild);
}
};
})
.catch((err) => {
throw err;
});
- 需要注意,如果使用
JetBrains Goland IDE
请按如下进行修改,否则无法正常导入syscall/js
包
- 更新
cmd/wasm/main.go
Go
package main
import (
"fmt"
"strings"
"syscall/js"
)
var (
// HTML 字符串
htmlString = `
<p>hello, I'm an HTML snippet from Go!</p>
<p>{{ placeholder }}</p>
`
// CSS 字符串
cssString = `
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
text-align: center;
padding: 20px;
}
h1 {
color: red;
text-transform: uppercase;
}
p {
color: blue;
font-size: 16px;
}
`
)
// GetCss 函数返回预定义的CSS字符串
func GetCss() js.Func {
return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return cssString
})
}
// SetHtml 函数返回自定义的HTML字符串
func SetHtml() js.Func {
return js.FuncOf(func(this js.Value, args []js.Value) any {
if len(args) > 0 {
text := args[0].String()
return strings.Replace(htmlString, "{{ placeholder }}", text, -1)
}
return js.Undefined()
})
}
func main() {
// 输出将打印在 console log 中
fmt.Println("Hello Web Assembly from Go!")
// 将函数导出到全局JavaScript环境,以便在JavaScript中调用
js.Global().Set("getCss", GetCss()) // 无参函数
js.Global().Set("setHtml", SetHtml()) // 带参函数
}
- 重新编译,测试运行
Bash
make action
make serve
结果点击 button
后却是报错:
- 修改
cmd/wasm/main.go
在 Go WebAssembly
中,一旦 main
函数完成执行,Go
程序就会退出,这可能导致后续的交互或事件处理出现问题。为了避免 "Go program has already exited"
的错误,只需要让 main
这个主协程 "保持程序运行状态" 不退出即可。
Go
// 当 main 函数完成执行时,WebAssembly 应用通常会立即终止。然而,通过在 main 函数中创建一个无缓冲的通道 ch,
// 并在通道上等待(<-ch),可以确保程序保持运行状态,直到某个外部事件或条件导致通道被关闭。这样可以确保
// WebAssembly 应用在浏览器中持续运行,而不会在 main 函数完成后立即退出。
func main() {
// 在 main 函数中创建一个无缓冲的通道并等待,以保持程序处于运行状态
ch := make(chan struct{}, 0)
// 中间省略...
// 从通道 ch 中接收数据。该操作会阻塞,防止主程序退出,直到从通道中接收到数据
<-ch
}
- 再次编译运行
小结
WebAssembly
本身是一种单线程的执行环境。这意味着 WebAssembly
实例在任何给定时间点只能执行一条指令。然而,WebAssembly
可以与 JavaScript
协同工作,利用浏览器的多线程性质来执行多线程任务。
WebAssembly
可以在主线程和 Web Worker
线程中运行。主线程是 JavaScript
的主要执行线程,而 Web Worker
是浏览器中的后台线程,用于在单独的线程中执行计算密集型任务。当 WebAssembly
在 Web Worker
中运行时,它可以利用多线程并行性执行任务,以提高性能。
在浏览器中,WebAssembly
通常与 JavaScript
进行互操作。通过 WebAssembly
的导出功能,可以在JavaScript
中调用 WebAssembly
函数,反之亦然。这种互操作性允许 WebAssembly
与 JavaScript
协同工作,以实现多线程和并行执行任务。
总的来说,WebAssembly
本身是单线程的,但它可以与 JavaScript
协同工作,并在多线程环境中执行任务,以提高性能和响应性。这使得 WebAssembly
成为执行计算密集型任务的强大工具,并且可以利用浏览器的多核处理器和多线程支持。
Github wasm-demo
一个更完整的项目示例:
Calculate Fibonacci Sequence from js and go wasm
项目构建: Vue3 + Vite + Bun / Go wasm
如何运行:
Bash
git clone git@github.com:pokeyaro/wasm-demo.git
cd wasm-demo
bun install
bun run dev
运行截图:
参考
developer.aliyun.com/article/787...
blog.logrocket.com/webassembly...
www.oschina.net/question/53...
相关话题
如果你对以下概念感兴趣,想要做更进一步了解,可以转到这篇文章: 《JavaScript runtime & engine 概念汇总》
- Run-time & Compile Time
- RTE(Runtime environment)
- ECMAScript specification
- JavaScript runtime(Nodejs、Deno、Bun)
- Browser(Rendering engine、JavaScript engine)
WebAssembly 在后端的应用
用 Docker 和 WebAssembly 打造容器的新时代
WebAssembly(Wasm)
是由 Mozilla
、谷歌、微软、苹果等公司合作研发的二进制指令格式语言,最初是为浏览器设计的,具有内存安全、可移植等特性。
2019 年 12 月 5 日,W3C
正式宣布 WebAssembly
成为继 HTML
、CSS
和 Javascipt
之后的第四大标准 Web
语言。这是 WebAssembly 在前端的高光时刻!
同样是 2019 年,Mozilla
、Fastly
、Intel
与 Red Hat
宣布成立联合组织 Bytecode Alliance
(字节码联盟),希望通过协作实施标准和提出新标准,以完善 WebAssembly
在浏览器之外的生态。
随后,WebAssembly
在后端走得越来越远。Docker
联合创始人 Solomon Hykes
在一条著名 twtter
里表示,如果 Wasm + WASI
在 2008 年就存在了,就没有必要创建 Docker
了。