网站首页 > 技术文章 正文
在当今的软件开发领域,跨平台技术的重要性愈发凸显。WebAssembly(Wasm)作为一项突破性的技术,为在浏览器中运行多种编程语言提供了可能,其中Go语言与WebAssembly的结合尤为引人注目。Go以其高效的并发性能、简洁的语法和强大的标准库闻名,通过编译为WebAssembly,能够在浏览器环境中发挥出独特的优势,拓展了Go语言的应用边界,为Web开发带来了新的活力。
Go与WebAssembly基础交互:从JavaScript调用Go函数
在将Go代码运行于浏览器的过程中,实现从JavaScript调用Go函数是基础且关键的一环。我们先以一个计算调和级数和的函数calcHarmonic为例,该函数运用Go的math/big标准库,能够精确计算调和级数在指定时间内的和,并返回高精度的字符串结果。
// calcHarmonic 计算调和级数,持续约nsecs秒,返回高精度字符串结果
func calcHarmonic(nsecs float64) string {
d := time.Duration(nsecs * float64(time.Second))
start := time.Now()
r1 := big.NewRat(1, 1)
for i := 2; ; i++ {
addend := big.NewRat(1, int64(i))
r1 = r1.Add(r1, addend)
if i%10 == 0 && time.Now().Sub(start) >= d {
break
}
}
return r1.FloatString(40)
}
为了让这个函数能够在浏览器中被JavaScript调用,我们需要进行一系列的设置。在Go代码中,通过js.Global().Set("calcHarmonic", jsCalcHarmonic)将函数名calcHarmonic导出给JavaScript,并定义包装函数jsCalcHarmonic。
func main() {
// 向JS导出函数名为"calcHarmonic",绑定包装函数
js.Global().Set("calcHarmonic", jsCalcHarmonic)
// WASM编译的Go主函数需无限阻塞
select {}
}
// 包装函数,使calcHarmonic可被JS调用
var jsCalcHarmonic = js.FuncOf(func(this js.Value, args []js.Value) any {
if len(args) != 1 {
panic("需要一个参数")
}
s := calcHarmonic(args[0].Float())
return js.ValueOf(s)
})
将Go文件编译为WASM/JS目标的命令为GOOS=js GOARCH=wasm go build -o harmonic.wasm harmonic.go。在JavaScript端,通过
WebAssembly.instantiateStreaming方法加载WASM模块,并实例化Go运行时。
// 实例化Go运行时(由wasm_exec.js定义)
const go = new Go();
WebAssembly.instantiateStreaming(fetch("harmonic.wasm"), go.importObject).then(
(result) => {
go.run(result.instance);
});
之后,在HTML页面中,通过JavaScript代码获取页面元素并调用calcHarmonic函数,实现用户输入与Go函数计算结果的交互展示。
let buttonElement = document.getElementById("submitButton");
document.getElementById("submitButton").addEventListener("click", () => {
let input = document.getElementById("timeInput").value;
let s = calcHarmonic(parseFloat(input));
document.getElementById("outputDiv").innerText = s;
});
最后,不要忘记引入Go发行版中的wasm_exec.js文件,它是Go代码在浏览器环境中运行的重要支持文件。
<script src="wasm_exec.js"></script>
这种基础的调用模式,为后续更复杂的应用场景奠定了坚实的基础,展现了Go与JavaScript在浏览器环境下协作的基本架构。
进阶探索:在Go中直接操作DOM
在传统的Web开发中,DOM操作通常由JavaScript完成。然而,借助WebAssembly,Go语言也能够实现对DOM的直接操作。在前述示例中,虽然Go实现了计算逻辑,但UI相关的逻辑如按钮监听、输出更新等仍依赖JavaScript。若将这些逻辑转移到Go中,可通过修改主函数来实现。
func main() {
doc := js.Global().Get("document")
buttonElement := doc.Call("getElementById", "submitButton")
inputElement := doc.Call("getElementById", "timeInput")
outputElement := doc.Call("getElementById", "outputDiv")
buttonElement.Call("addEventListener", "click", js.FuncOf(
func(this js.Value, args []js.Value) any {
input := inputElement.Get("value")
inputFloat, err := strconv.ParseFloat(input.String(), 64)
if err != nil {
log.Println(err)
return nil
}
s := calcHarmonic(inputFloat)
outputElement.Set("innerText", s)
return nil
}))
select {}
}
通过js.Global()获取JavaScript的上下文环境后,Go代码可以像JavaScript一样调用DOM方法、设置DOM元素属性。例如,通过buttonElement.Call("addEventListener", "click", js.FuncOf(...))为按钮添加点击事件监听器,并且在回调函数中获取输入值、调用计算函数并更新输出元素的文本。这一能力极大地拓展了Go在Web开发中的应用范围,开发者可以将更多业务逻辑集中在Go语言中实现,增强代码的内聚性和可维护性。
优化方案:使用TinyGo作为替代编译器
Go编译器对WebAssembly的支持已经相当成熟,但存在一个不容忽视的问题:完整的Go运行时会被编译到WASM二进制文件中,导致文件体积较大。以示例代码生成的.wasm文件为例,在一些机器上可达约2.5 MiB,这在网络加载时,尤其是慢速网络环境下,会严重影响用户体验。
TinyGo作为一种针对嵌入式设备的Go工具链,为这一问题提供了有效的解决方案。TinyGo运行时更加轻量,生成的二进制文件大小约为传统Go编译结果的1/4。这意味着在浏览器加载时,能够显著缩短加载时间,提升应用的响应速度。然而,TinyGo并非完美无缺。其编译速度相对较慢,这在开发过程中可能会增加编译等待时间;生成的代码执行效率也略低于传统Go编译的代码。此外,TinyGo对依赖反射的标准库(如encoding/json)支持有限,在实际应用中,可能需要寻找替代的JSON库。在dom - in - go示例目录中,详细展示了如何使用TinyGo进行项目编译,并且通过Makefile妥善处理了TinyGo专属的wasm_exec.js依赖问题,为开发者提供了实践参考。
性能提升:在Web Worker中运行WASM
在Web开发中,性能是至关重要的因素。当在浏览器中执行耗时较长的计算任务时,可能会出现页面“冻结”的现象,这是因为JavaScript主线程被阻塞,无法及时响应用户的交互操作。为了解决这一问题,我们可以利用Web Workers技术,将WASM代码放到独立的线程中执行。
在主HTML文件中,通过创建一个新的Web Worker并监听其消息来实现与Worker线程的通信。
const worker = new Worker("worker.js");
worker.onmessage = ({ data }) => {
let { action, payload } = data;
switch (action) {
case "log":
console.log(`worker.log: ${payload}`);
break;
case "result":
resultReady(payload);
break;
default:
console.error(`未知操作: ${action}`);
}
};
在worker.js文件中,加载WASM模块并在接收到计算任务时调用Go函数进行计算,然后将结果返回给主线程。
importScripts("wasm_exec.js");
console.log("Worker运行中");
const go = new Go();
WebAssembly.instantiateStreaming(fetch("harmonic.wasm"), go.importObject).then(
(result) => {
go.run(result.instance);
console.log("Worker加载WASM模块");
}).catch((err) => {
console.error("Worker加载失败: ", err)
});
onmessage = ({ data }) => {
let { action, payload } = data;
postMessage({
action: "log",
payload: `Worker收到消息 ${action}: ${payload}`,
});
switch (action) {
case "calculate":
let result = calcHarmonic(payload);
postMessage({ action: "result", payload: result });
break;
default:
throw (`未知操作 '${action}'`);
}
};
在这个过程中,Go代码无需修改,计算逻辑在Worker线程中独立执行,而JavaScript主线程可以自由处理UI交互,从而避免了页面卡顿现象。示例中添加的加载动画,直观地展示了这种非阻塞效果,提升了用户体验。
通信拓展:用Go实现WebSocket客户端
WebSocket作为一种在单个TCP连接上进行全双工通信的协议,在现代Web应用中被广泛用于实现实时通信功能。以往,Go服务器与JavaScript客户端之间的WebSocket通信较为常见,而现在,我们可以尝试用Go完全替代JavaScript客户端。
以一个简单的应用场景为例,当鼠标在特定区域内移动时,客户端通过WebSocket向服务器发送消息,服务器回显消息后,客户端更新UI。在服务器端,使用
golang.org/x/net/websocket包来搭建WebSocket服务。在客户端,通过Go操作浏览器的WebSocket API来实现通信。
const wsServerAddress = "ws://127.0.0.1:4050"
// 等价于JS中的 new WebSocket(addr)
wsCtor := js.Global().Get("WebSocket")
wsEcho := wsCtor.New(wsServerAddress + "/wsecho")
wsTime := wsCtor.New(wsServerAddress + "/wstime")
通过定义wsSend函数来发送消息,确保在WebSocket连接处于打开状态时,将消息序列化为JSON格式后发送。
// wsSend 向WebSocket发送消息(需确保连接已打开)
func wsSend(sock js.Value, msg any) {
if !sock.IsNull() && sock.Get("readyState").Equal(js.Global().Get("WebSocket").Get("OPEN")) {
b, err := json.Marshal(msg)
if err != nil {
log.Fatal(err)
}
sock.Call("send", string(b))
} else {
log.Println("socket未打开")
}
}
同时,通过wsEcho.Call("addEventListener", "message", js.FuncOf(...))注册消息事件监听器,在接收到消息后进行解析并更新UI。此示例展示了Go对浏览器WebSocket API的直接调用能力,尽管服务器与客户端使用不同的WebSocket实现(Go原生与浏览器API),但通过JSON通信实现了跨环境的交互,为构建全Go栈的实时通信应用提供了技术支持。
本地测试:使用Node.js助力开发
在开发过程中,本地测试是确保代码质量和功能正确性的重要环节。尽管本文主要聚焦于在浏览器中运行Go代码,但借助Node.js加载WASM模块的能力,我们可以在不启动浏览器的情况下,便捷地运行GOOS=js GOARCH=wasm编译的二进制文件。
Go工具链提供了特殊执行器,如go_js_wasm_exec,来支持跨平台二进制文件的运行。以下是一个示例测试用例,通过在Go代码中编写测试函数TestJSArr,利用js.Global()在Node.js环境中模拟浏览器的JavaScript上下文,对相关功能进行测试。
//go:build js && wasm
package main
import (
"log"
"syscall/js"
"testing"
)
func TestJSArr(t *testing.T) {
log.Println("js/wasm测试中")
objs := js.Global().Call("eval", `({arr: [41,42,43]})`)
arr := objs.Get("arr")
if got := arr.Length(); got != 3 {
t.Errorf("长度不符: %#v vs 3", got)
}
if got := arr.Index(1).Int(); got != 42 {
t.Errorf("元素不符: %#v vs 42", got)
}
}
通过GOOS=js GOARCH=wasm go test -exec=
supportfiles/go_js_wasm_exec -v.命令即可运行测试,这种方式使得WASM程序的本地测试流程与普通Go程序的测试流程相似,极大地提高了开发效率,方便开发者快速发现和修复问题。
总结与展望
通过WebAssembly在浏览器中运行Go代码,为跨平台开发带来了诸多优势和创新的应用模式。从基础的函数调用,到复杂的DOM操作、Web Worker多线程运用、WebSocket通信实现,以及TinyGo优化和本地测试方法的介绍,本文全面展示了Go与WebAssembly结合的技术魅力。随着WebAssembly生态系统的不断成熟和完善,Go开发者将拥有更广阔的创作空间,能够更加灵活地将后端逻辑迁移到前端,甚至构建全Go栈应用。这些技术实践不仅适用于当前的浏览器场景,也为WASI等新兴领域提供了宝贵的实践参考。相信在未来,Go与WebAssembly的深度融合将持续推动Web开发技术的创新与发展,为开发者和用户带来更多惊喜。
关注我的《Golang实用技巧》专栏,它将为你揭秘生产环境最佳实践,带你探索高并发编程的实用教程。从分享实用的Golang小技巧到深入剖析实际应用场景,让你成为真正的Golang大师。无论你是初学者还是经验丰富的开发者,这里都有你所需要的灵感和知识。让我们一同探索Golang的无限可能!
猜你喜欢
- 2025-05-02 Node.js+Puppeteer:新一代动态爬虫利器,高效抓取不再难!
- 2025-05-02 现在可以部署到Web Assembly的6种语言
- 2025-05-02 七爪源码:这是在 Go 中进行实时重新加载的好方法
- 2025-05-02 哪种编程语言又快又省电?有人对比了27种语言
- 2025-05-02 阿里云迎来史上最特殊的员工:工号AI001,精通200 多种编程语言!
- 2025-05-02 使用 WebAssembly 和 Go 编写前端 Web 框架
- 2025-05-02 完全用 Go 编写的 JS 引擎(常用的编写javascript的工具有哪些)
- 2025-05-02 使用Docker和Codeship来装运Node.js应用
- 2025-05-02 英特尔 Evo 认证 PC 对决 M1 Macbook:x86 生态加持,Evo 更省心
- 2025-05-02 Playwright 全面教程从入门到精通-Playwright 高级特性与技巧
- 最近发表
- 标签列表
-
- cmd/c (64)
- c++中::是什么意思 (83)
- 标签用于 (65)
- 主键只能有一个吗 (66)
- c#console.writeline不显示 (75)
- pythoncase语句 (81)
- es6includes (73)
- sqlset (64)
- windowsscripthost (67)
- apt-getinstall-y (86)
- node_modules怎么生成 (76)
- chromepost (65)
- c++int转char (75)
- static函数和普通函数 (76)
- el-date-picker开始日期早于结束日期 (70)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- & (66)
- java (73)
- js数组插入 (83)
- linux删除一个文件夹 (65)
- mac安装java (72)
- eacces (67)
- 查看mysql是否启动 (70)
- 无效的列索引 (74)