首页 / Wails 入门教程 / 方法绑定 Bind

Wails 入门教程

方法绑定 Bind

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

Wails桌面开发BindGoReact

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 的包名。因为我们的 Apppackage 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 devwails 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.tsxfrontend/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、生命周期钩子怎么配合、以及返回复杂结构体时前端拿到的是什么。