首页 / Wails 入门教程 / 核心架构:Go 如何驱动前端

Wails 入门教程

核心架构:Go 如何驱动前端

本教程共 42 篇 · 第 2 篇 · 更新于 2026-08-03

Wails架构GoWebViewIPC

2. 核心架构:Go 如何驱动前端

本节目标

  • 说清 Wails 运行时的三个组成部分各自管什么
  • 理解 Go 和前端之间那座”桥”是怎么传数据的
  • 知道最终编译产物是什么形态、为什么能单文件分发
  • 客观看懂 Wails 和 Electron、Tauri 的架构差别

2-1 三层结构:主进程、渲染层、桥

Wails 跑起来后,整台机器上其实有三块东西在协作:

Go 主进程。这是应用的”大脑”,也是操作系统的正式进程。窗口的创建、事件循环、你的业务逻辑、对系统的调用,全在这里。它是真正的原生程序,能起 goroutine、能读文件、能开网络连。

前端渲染层。界面不是浏览器画的,而是操作系统自带的 WebView 组件:Windows 上是 WebView2(基于 Chromium 内核,随系统或应用分发),macOS 上是 Cocoa 里的 WebKit,Linux 上是 GTK 绑定的 WebKit。你在里面写 HTML/CSS/JS,体验跟写网页几乎一模一样。

中间的桥(Bridge)。这是 Wails 的精髓,也是它跟”用浏览器打开一个本地网页”的本质区别。桥负责两件事:把前端发来的调用,序列化后路由到正确的 Go 方法,再把返回值原路送回;以及让 Go 主动往前端的事件总线推消息。前端完全感知不到这是跨进程调用,它只看到一个个返回 Promise 的函数。

Note

这套桥在 Wails v2 里走的是各平台原生的 IPC 通道(Windows 上借助 WebView2 的 WebView 环境、macOS 走 WebView 的脚本注入),不是起一个本地 HTTP 服务来转发。所以调用延迟很低,也不占用端口。

2-2 一段代码对应三层结构

把第 1 章那个最小示例的 main.go 展开,你能直接看到三层是怎么接上的:

package main

import (
	"embed"
	"log"

	"github.com/wailsapp/wails/v2"
	"github.com/wailsapp/wails/v2/pkg/options"
	"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)

//go:embed all:frontend/dist
var assets embed.FS

func main() {
	app := NewApp()

	err := wails.Run(&options.App{
		Title:  "架构示例",
		Width:  1024,
		Height: 768,
		AssetServer: &assetserver.Options{
			Assets: assets,
		},
		OnStartup: app.startup,
		OnShutdown: app.shutdown,
		Bind: []interface{}{
			app,
		},
	})
	if err != nil {
		log.Fatal(err)
	}
}

逐行对号入座:

//go:embed all:frontend/distassets embed.FS —— 这是”渲染层”的资源来源。前端 npm run build 产出的 dist 目录,被编译进二进制。运行时 AssetServer 就从这个 embed.FS 里把 index.html 和静态资源喂给 WebView。这就是为什么最终能单文件分发。

wails.Run(&options.App{...}) —— 这是 Go 主进程的起点,启动事件循环、创建窗口、拉起桥。

Bind: []interface{}{app} —— 这一行把 App 的公开方法交给桥,前端才能调用。第 12 章会专门讲绑定的细节。

OnStartup / OnShutdown —— 生命周期钩子,窗口起来前、退出前各给你一个插手的机会。

2-3 一次调用的完整旅程

前端敲下 await Greet("码上学"),背后发生了什么?把时序拆开:

  1. 前端的 Greet 函数(来自自动生成的 wailsjs/go/main/App.js)把参数 "码上学" 序列化,通过桥发给 Go 主进程。
  2. 桥根据方法名路由,找到 App 实例上绑定的 Greet 方法,用反射把参数填进去调用。
  3. Go 返回字符串,桥把它序列化回 JSON,再送回前端。
  4. 前端的 Promise resolve,拿到 "你好,码上学!"

整个来回对用户来说是”等了一个异步函数”。因为跨进程,所以必然异步——这也是为什么生成的函数一律返回 Promise。

反过来,Go 也能主动找前端:调 runtime.EventsEmit(ctx, "进度", 50),前端用 window.runtime.EventsOn("进度", (data) => ...) 收到。这条反向通道是做后台任务进度条、日志推送的关键,第 14 章细讲。

Tip

调试时直接开浏览器访问 wails dev 内置的 dev server(默认 localhost:34115),在控制台敲 window.go.main.App.Greet("测试") 就能验证桥是否通。能收到返回值,说明 Go→桥→前端整条链路是好的。

2-4 编译产物到底是什么样

跑完 wails build,产物长这样(以 Windows 为例):

build/
└── bin/
    └── 架构示例.exe

就一个 exe。前端 dist 已经被 embed 编进去了,运行时不依赖任何外部资源目录。用户拿到这个 exe 双击就跑,前提是他机器上有 WebView2 运行时——Windows 10/11 大部分已经自带,没有的话 Wails 的 Windows 安装包可以把它一并打进去(后面打包章会讲)。

macOS 产物是一个 .app 包,Linux 是一个 ELF 可执行文件。三者都不需要 Node、不需要浏览器安装包。这就是”轻量”最直接的体现:你的安装包体积基本等于二进制本身,通常十几到几十兆。

Warning

单文件不代表”零依赖”。WebView 运行时是系统级的。Windows 上 WebView2 基本自带;Linux 上用户必须装 libwebkit2gtk 之类的库,否则程序起不来。打包分发时要考虑这个前提,别以为一个二进制丢过去就万事大吉。

2-5 和 Electron、Tauri 比一比

把三个常被拿来比的框架摆一起看:

Electron:内嵌完整 Chromium + Node.js。前端和后端的界限跟 Wails 类似,但”后端”是 Node,运行时要带一整个浏览器。好处是生态全、行为一致;代价是包大(轻松 100MB+)、内存占用高。Wails 用系统 WebView,包小、启动快,但不同系统的渲染会有细微差异。

Tauri:思路和 Wails 几乎一致——系统 WebView 渲染前端、 Rust 写后端逻辑、单文件分发。区别在后端语言是 Rust 而不是 Go。如果你团队是 Go 栈,Wails 上手成本更低;如果是 Rust 栈,Tauri 更自然。两者都不内嵌浏览器,都轻量,这是它们跟 Electron 最大的分野。

原生开发(Qt / Win32 / Cocoa):完全自己控制窗口和渲染,性能上限最高,但开发效率低、跨平台要写多套代码。Wails 牺牲了一点极致性能,换来了”写网页的速度 + Go 的简洁”。

Note

选框架看团队基因。会 Go、想快速出桌面工具,Wails 是顺手的选择;前端为主、不碰后端语言,Electron 仍是省心方案;看重极致包体和 Rust 生态,看 Tauri。没有谁碾压谁,只有合不合适。

常见误区

以为 Wails 在本地起了个 HTTP 服务给前端用。v2 的桥走原生 IPC,不是本地服务器,不占端口。但资产服务(AssetServer)确实会以类 HTTP 的方式响应前端对静态资源的请求,二者不要混为一谈。

以为前端和 Go 在同一个 JS 运行时。不是,它们是不同的进程/运行时,数据靠序列化来回传。所以大对象频繁传递会有性能开销,别拿它当进程内函数调用使。

看到”单文件”就以为不用管系统依赖。WebView 是绕不开的,尤其是 Linux 发行版,打包时要把 webkit 依赖交代清楚。

小结

Wails 的架构就三块:Go 主进程管逻辑、系统 WebView 管界面、中间的原生桥管双向通信。前端调用 Go 方法是序列化→路由→反射调用→回传,天然异步;Go 推消息给前端走事件系统。产物是单文件二进制,但真实运行依赖系统 WebView。和 Electron 比它轻量,和 Tauri 比它用 Go——理解这三层,后面所有配置和 API 都能对号入座。