方法绑定 Bind
本教程共 42 篇 · 第 12 篇 · 更新于 2026-08-03
12. 方法绑定 Bind
本节目标
- 理解 Wails 的「绑定」到底把什么东西暴露给了前端
- 会在
wails.Run()的Bind字段里挂上自己的结构体 - 看懂
wailsjs/go/目录下自动生成的绑定文件 - 在 React 组件里正确调用一个 Go 方法并拿到返回值
- 知道哪些方法会被绑定、哪些会被 Wails 直接忽略
12-1 绑定是把 Go 方法搬到 JS 里
前面几章我们一直在聊窗口、资产、配置,都是「壳」。从这一章开始进入 Wails 真正的核心:Go 和前端怎么通信。
Wails 给的答案很直接——绑定(Binding)。你在 Go 里写一个普通的方法,Wails 在应用启动时用反射扫描它,然后在前端生成一个同名的 JavaScript 函数。前端调用这个函数,就等于调用了 Go 那边的方法。
Note反射(reflection)是 Go 在运行期检查类型和方法的能力。你不需要懂它的细节,只要知道 Wails 靠它「看见」了你的方法签名。
这件事的价值在于:前端开发者不用关心 IPC(进程间通信)、不用手写 HTTP 接口、不用起本地服务器。在前端眼里,Go 方法就是一个返回 Promise 的普通异步函数。
和传统 Web 开发对照一下会更好理解。你可以把绑定的结构体想象成后端的「控制器」,把它的公开方法想象成一条条接口。区别是这里没有 URL,也没有网络请求,调用直接走 WebView 和 Go 进程之间的内部通道。
12-2 写一个可以被绑定的方法
用 react-ts 模板生成的项目里,业务逻辑默认放在 app.go。打开它,结构大概是这样:
package main
import (
"context"
"fmt"
)
// App 保存应用级状态,ctx 是调用运行时 API 的钥匙
type App struct {
ctx context.Context
}
func NewApp() *App {
return &App{}
}
// startup 在前端创建完成、index.html 加载之前被调用
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
}
// Greet 是一个会被绑定到前端的公开方法
func (a *App) Greet(name string) string {
return fmt.Sprintf("你好,%s!", name)
}
三个要点:
结构体 App 里存了一个 context.Context。它由 Wails 在启动时交给你,后面调用运行时 API(事件、对话框、窗口控制)全都要用到它,所以标准做法是在 startup 里存下来。
Greet 的首字母是大写。在 Go 里,首字母大写代表「导出/公开」,Wails 只绑定公开方法。
startup 首字母小写,是私有方法。它同时也是生命周期回调,Wails 永远不会把它绑定到前端。
再加一个稍微复杂点的方法,让它返回错误:
import "errors"
// Divide 演示带 error 返回值的方法
func (a *App) Divide(a1 float64, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("除数不能为 0")
}
return a1 / b, nil
}
Go 里「返回值 + error」是惯用写法,Wails 对这种签名做了专门处理,第 15 章会详细讲前端怎么接住这个 error。
12-3 在 wails.Run 里挂上 Bind
方法写好了,还得告诉 Wails「我要暴露它」。这一步在 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,
Bind: []interface{}{
app,
},
})
if err != nil {
log.Fatal(err)
}
}
Bind 的类型是 []interface{},也就是一个「任意类型的切片」。你往里塞的每一项,Wails 都会扫描它的公开方法。
Warning
Bind里必须放结构体的实例,不能放类型。写app(*App类型的变量)是对的,写App{}这种字面量虽然也是实例但拿不到指针接收者的方法,写类型名更是直接编译不过。老老实实用NewApp()返回的指针。
想绑多个结构体?往切片里继续加就行:
Bind: []interface{}{
app,
&FileService{},
&ConfigService{},
},
每个结构体在前端会生成独立的命名空间,互不干扰。多结构体绑定的 context 传递有个小坑,第 13 章专门讲。
Warning如果你在别的教程里看到
wails.Run(&wails.AppConfig{...})或者wails.Init(...),那是 Wails v1 的写法,v2 已经彻底移除。v2 只有wails.Run(&options.App{...})这一条路。
12-4 生成的绑定文件长什么样
运行 wails dev 之后,Wails 会在前端目录下生成一个 wailsjs 文件夹。对上面的 App 结构体,生成结果是:
frontend/
└── wailsjs/
├── go/
│ ├── main/
│ │ ├── App.d.ts
│ │ └── App.js
│ └── models.ts
└── runtime/
├── runtime.d.ts
└── runtime.js
main 这一层对应 Go 的包名。因为我们的 App 在 package main 里,所以路径是 go/main/App。
App.js 是实际的调用胶水代码,App.d.ts 是 TypeScript 类型声明。打开 App.d.ts 会看到:
export function Greet(arg1: string): Promise<string>;
export function Divide(arg1: number, arg2: number): Promise<number>;
注意两点:Go 的 string 变成了 TS 的 string,Go 的 float64 变成了 number;返回值统统被包成 Promise。跨进程调用天然是异步的,这一点绕不过去。
models.ts 里放的是方法签名中用到的 Go 结构体对应的 TS 类型。只用基础类型的话这个文件可能是空的。
Tip
wailsjs目录是自动生成的产物,不要手改,也建议在.gitignore里忽略掉。每次wails dev或wails build都会重新生成。历史上 v2 早期需要手动跑wails generate module,现在开发和构建流程已经自动带上了,日常基本用不到。
12-5 在 React 组件里调用
有了类型声明,React 侧写起来就跟调用普通异步函数没区别:
import { useState } from "react";
import { Greet } from "../wailsjs/go/main/App";
export default function GreetBox() {
const [name, setName] = useState("");
const [result, setResult] = useState("");
const handleGreet = async () => {
const text = await Greet(name);
setResult(text);
};
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<button onClick={handleGreet}>打招呼</button>
<p>{result}</p>
</div>
);
}
import { Greet } from "../wailsjs/go/main/App" 这一行的相对路径要根据你组件的实际位置调整。模板里 App.tsx 在 frontend/src/ 下,所以是 ../wailsjs/...;如果组件放在 frontend/src/components/ 里,就得写成 ../../wailsjs/...。
除了 import 的方式,Wails 还会把所有绑定挂到全局对象上。在浏览器 devtools 的控制台里直接敲:
window.go.main.App.Greet("码上学").then(console.log);
一样能跑。这个全局路径的规律是 window.go.<包名>.<结构体名>.<方法名>。调试的时候不用改代码就能验证绑定是否成功,很方便。
Tip排查「前端调不通 Go」的问题时,第一步就是在控制台敲
window.go,看看你的结构体和方法在不在里面。在,说明绑定成功,问题在前端调用;不在,说明 Go 侧的绑定没生效。
12-6 哪些方法会被绑定
Wails 的筛选规则比想象中简单,但每一条都容易踩:
必须是公开方法。首字母小写的方法一律跳过。func (a *App) getData() 前端看不到,改成 GetData 才行。
必须挂在被绑定的结构体上。包级别的普通函数 func Helper() {} 不会被绑定,Wails 只扫描 Bind 里那些实例的方法集。
生命周期回调会被排除。即使你把 startup 改成大写的 Startup 并挂进 OnStartup,Wails 也不会把它当成普通业务方法暴露出去。
返回值最多两个。Wails 支持三种签名形态:没有返回值、只有一个返回值、一个返回值加一个 error。写成三个返回值绑定会失败。
参数和返回值的类型要能 JSON 序列化。基础类型、切片、map、结构体都没问题。channel、函数类型、sync.Mutex 这类东西不行——它们没法变成 JSON 传给 JavaScript。
结构体作为参数或返回值时还有个额外要求:字段必须带合法的 json 标签,否则不会出现在生成的 TypeScript 类型里。
type User struct {
Name string `json:"name"`
Email string `json:"email"`
// 没有 json 标签的字段不会出现在生成的 TS 类型中
internal string
}
匿名嵌套结构体目前还不支持,需要拆成具名类型。
常见误区
改了 Go 方法名,前端报 undefined。绑定文件是构建期生成的。wails dev 检测到 .go 文件变化会自动重新编译并重新生成,但如果你的编辑器没保存、或者 dev 进程挂了,前端拿到的还是旧文件。重启 wails dev 一般就好。
方法签名里用了指针接收者却传了值实例。func (a *App) Greet() 是指针接收者,Bind 里必须放 app(*App),放 *app 解引用后的值会丢方法。
以为绑定是同步的。所有绑定方法在前端都返回 Promise,忘了 await 或 .then() 拿到的就是一个 Promise 对象而不是结果值。TypeScript 会提示类型不匹配,纯 JS 项目里就得自己盯着。
在方法里长时间阻塞。绑定调用会占用 Wails 的调用通道,一个跑十秒的方法会让界面看起来像卡死了。这种场景应该改用第 14 章的事件系统,让 Go 主动推进度给前端。
小结
绑定是 Wails 的地基。整条链路是:Go 写公开方法 → Bind 里放结构体实例 → Wails 反射扫描 → 生成 wailsjs/go/ 下的 JS 和 TS 文件 → 前端 import 后当普通异步函数调用。
记住四个硬约束就能少踩九成的坑:方法首字母大写、Bind 放实例、返回值最多「值 + error」、结构体字段要带 json 标签。
下一章讲绑定的进阶用法:多结构体如何共享 context、生命周期钩子怎么配合、以及返回复杂结构体时前端拿到的是什么。