使用 Go 编译 WebAssembly

使用 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.org/

WebAssembly 是一项新兴的标准,其目标是定义一个安全、可移植、大小和加载时间高效的二进制编译器目标,提供接近本地代码的性能 --------- 为 Web 提供了一种虚拟 CPU。WebAssembly 正在由 W3C 社区组(CG)开发,其中的成员包括 Mozilla、Microsoft、Google 和 Apple。

Wasm 发展历程

WebAssembly 真正的发展可以追溯到 2015 年,当时由多家主流浏览器厂商(包括 Google、Mozilla、微软和苹果)共同推动其标准化。自那时以来,WebAssembly 已经取得了显著的发展和广泛应用。

目前,几乎所有的主流现代浏览器都支持 WebAssembly。在短短几年内取得了巨大的进展,成为在浏览器中运行高性能的编程语言代码的关键技术。随着更多开发者采用 WebAssembly,预计将会有更多的工具、库和框架出现,进一步推动其发展。

gantt title WebAssembly 的前世今生 dateFormat YYYY section 时间轴 Alon Zakai想把C++游戏运行在浏览器上,但又懒于重新手写JavaScript: t-3, 2010, 30d Emscripten发布,Emscripten利用LLVM将C++编译为字节码,再将字节码编译为JavaScript: t-2, 2011, 30d asm.js发布,成功将游戏移植到了浏览器: t-1, 2013, 30d WebAssembly项目启动: t1, 2015, 30d WebAssembly的第一个MVP版本发布: t2, 2017-03, 30d WebAssembly在现代浏览器中得到广泛支持: t3, 2017-11, 30d WASI被提出,赋予了Wasm访问网络系统的能力,服务端的Wasm走进人们的视野: t4, 2019-03, 30d WebAssembly被正式纳入W3C推荐标准,标志着一种允许代码在浏览器中运行的新Web语言的到来: t5, 2019-12, 30d CNCF接受了三个Wasn项目(WasmEdge/WasmCloud/Krustlet): t6, 2021, 30d

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

  1. 安装 wasmtime 命令:
Bash 复制代码
➜ brew install wasmtime
  1. 编写 Go 代码:
Go 复制代码
package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello Wasm!")
}
  1. 使用 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
  1. 前置准备
Bash 复制代码
# 新建目录
mkdir rookie && cd rookie/

# 初始化工程
go mod init rookie

# 以更简洁的方式创建多层级文件夹
mkdir -p assets cmd/{server,wasm}
  1. 新建 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>
  1. 新建 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;
  });
  1. 新建 cmd/wasm/main.go(这里是主要的业务逻辑)
Go 复制代码
package main

import (
    "fmt"
)

func main() {
    // 使用 ANSI 转义序列来设置颜色和字体样式(蓝色加粗)
    fmt.Println("\033[1;34mHello Web Assembly from Go!\033[0m")
}
  1. 新建 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))
}
  1. 编写 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
  1. 编译 Wasm
Bash 复制代码
# 执行action目标
make action
  1. 测试运行
Bash 复制代码
# 执行serve目标
make serve

打开 http://localhost:8888 将会看到:

示例二 | 使用 Go 编写自定义 Wasm 函数

还是复用上述的目录结构,部分内容稍作改动:

  1. 更新 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>
  1. 更新 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;
  });
  1. 需要注意,如果使用 JetBrains Goland IDE 请按如下进行修改,否则无法正常导入 syscall/js 包
  1. 更新 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()) // 带参函数
}
  1. 重新编译,测试运行
Bash 复制代码
make action
make serve

结果点击 button 后却是报错:

  1. 修改 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
}
  1. 再次编译运行

小结

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...

medium.com/@guglielmin...

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 了。

Docker without containers!

wasmlabs.dev/articles/do...

相关推荐
溪语流沙9 分钟前
【每天一个CSS | Day11】赛博朋克故障风文字:红青分离 + 切片抖动
前端·css
liangshanbo12151 小时前
React Fiber:从“解决卡顿”到完整更新链路
前端·javascript·react.js
Timmy1 小时前
前端转全栈笔记:讲框架之前,先把 TypeScript 这关过了
前端·人工智能·node.js
计算机魔术师1 小时前
年收入650亿、亏420亿,Anthropic凭什么敢叫价万亿?
前端
Frag0ut1 小时前
Chrome Canary 最新实验功能:AI Mode、按设备筛选历史记录与 Gemini 自动改密
前端·人工智能·chrome·dev·beta·canary·最新实验功能
anew___1 小时前
《从零手写操作系统 (19):Ext2文件系统实战——从内存到磁盘的跨越》
java·服务器·前端·数据库·算法
吴声子夜歌2 小时前
Nginx应用与运维——Nginx Web服务应用实战(HTTPS安全服务器的搭建)
运维·前端·nginx
liangshanbo12152 小时前
Monorepo 工程化面试题:如何设计 Web + Node.js 中间层 + Shared + Scripts 的 Monorepo?
前端·node.js
IT_陈寒2 小时前
Redis集群切换主节点时,服务竟然全员掉线
前端·人工智能·后端
wangbing11252 小时前
开发指南147-WebSocket-前端
前端·websocket·网络协议