从前端调用 Go
本教程共 42 篇 · 第 15 篇 · 更新于 2026-08-03
15. 从前端调用 Go
本节目标
- 看懂
window.go全局对象的结构,会用它做快速调试 - 理解绑定方法返回的 Promise 什么时候 resolve、什么时候 reject
- 记住 Go 与 TypeScript 之间的类型映射规则
- 在 React 里用 try/catch 或
.catch()稳妥地处理 Go 返回的错误 - 知道
ipc.js和runtime.js是怎么被注入到页面里的
15-1 两种调用写法
第 12 章提过一句,这里展开。前端调用 Go 方法有两个入口,底层是同一套东西。
写法一:import 生成的模块(推荐)
import { Greet } from "../wailsjs/go/main/App";
const text = await Greet("码上学");
好处是有完整的 TypeScript 类型提示,参数写错、返回值用错,编辑器当场标红。
写法二:走全局对象
const text = await window.go.main.App.Greet("码上学");
路径规律是 window.go.<Go 包名>.<结构体名>.<方法名>。没有类型提示,但胜在不用 import,在 devtools 控制台里随手就能敲。
Tip排查问题时,先在控制台敲一句
window.go,展开看看有没有你的结构体。绑定成功的话结构一目了然。这一招比翻半天代码快多了。
日常写业务用第一种,调试用第二种。两者调用的是同一个底层通道,行为完全一致。
15-2 Promise 的两种结局
所有绑定方法在前端都返回 Promise。跨进程通信本质是异步的,Go 那边跑完了才能把结果送回来。
Promise 的 resolve 和 reject 分别对应什么,取决于 Go 方法的签名。
只有一个返回值:永远 resolve
func (a *App) Greet(name string) string {
return "你好," + name
}
export function Greet(arg1: string): Promise<string>;
这个 Promise 不会 reject(除非你传了非法数据导致序列化失败)。写 .catch() 也没什么意义。
返回值 + error:error 非 nil 时 reject
func (a *App) ReadConfig(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("读取配置失败: %w", err)
}
return string(data), nil
}
Go 返回的 error 不为 nil 时,Promise 走 reject 分支,错误信息作为 rejection 的值传过去。
只有 error 没有值
func (a *App) SaveConfig(path, content string) error {
return os.WriteFile(path, []byte(content), 0644)
}
export function SaveConfig(arg1: string, arg2: string): Promise<void>;
成功时 resolve 一个 void,失败时 reject。
什么都不返回
func (a *App) Refresh() {}
依然返回 Promise<void>,只是永远 resolve。
NoteGo 的惯例是「结果在前,error 在后」,Wails 严格遵循这个约定。error 必须是第二个返回值,写在第一位或者返回三个值,绑定都会失败。
15-3 类型映射表
Go 和 JavaScript 中间隔着一层 JSON 序列化,类型对应关系如下:
| Go 类型 | TypeScript 类型 | 说明 |
|---|---|---|
string | string | 直接对应 |
int / int8…int64 | number | 注意 JS number 的精度上限 |
uint / uint8…uint64 | number | 同上 |
float32 / float64 | number | 直接对应 |
bool | boolean | 直接对应 |
[]T | Array<T> | 切片变数组 |
map[string]T | { [key: string]: T } | key 必须是 string |
struct | 生成的 class | 见 models.ts |
*struct | 可选的 class | 可能为 null |
interface{} / any | any | 失去类型信息 |
time.Time | string | 序列化成 RFC3339 字符串 |
WarningGo 的
int64能表示的范围超过 JavaScript 的安全整数范围(约 9 × 10^15)。传大整数 ID 或时间戳纳秒值时会丢精度。稳妥做法是在 Go 侧把它转成string再返回。
map 的 key 只能是 string。map[int]string 序列化成 JSON 后 key 会变成字符串,前端拿到的是 {"1": "a"},和你预期的类型对不上。
chan、func、含有互斥锁的结构体这类无法 JSON 化的类型,不能出现在绑定方法的签名里。
time.Time 这一项也值得单独说一句。它序列化后是一个 RFC3339 格式的字符串,比如 2026-08-03T10:30:00Z。前端拿到后要用 new Date(str) 转换才能做日期运算。如果你只关心「显示」,直接在 Go 侧格式化成中文习惯的字符串返回会更省事,前端一行代码都不用写。
还有一个容易忽略的点:Go 里值为 nil 的切片和 map,序列化后是 null 而不是 [] 或 {}。前端拿到 null 直接 .map() 会报错。要么在 Go 侧初始化成空切片再返回,要么前端做一次空值兜底。
15-4 错误处理的三种写法
async/await + try/catch(最常用)
import { ReadConfig } from "../wailsjs/go/main/App";
async function load() {
try {
const content = await ReadConfig("./config.json");
console.log(content);
} catch (err) {
// err 是 Go 侧 error 的字符串描述
console.error("加载失败:", err);
}
}
Promise 链
ReadConfig("./config.json")
.then((content) => console.log(content))
.catch((err) => console.error("加载失败:", err));
在 React 组件里的完整形态
import { useState } from "react";
import { ReadConfig } from "../wailsjs/go/main/App";
export default function ConfigLoader() {
const [content, setContent] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const handleLoad = async () => {
setLoading(true);
setError("");
try {
const data = await ReadConfig("./config.json");
setContent(data);
} catch (err) {
setError(String(err));
} finally {
setLoading(false);
}
};
return (
<div>
<button onClick={handleLoad} disabled={loading}>
{loading ? "加载中…" : "加载配置"}
</button>
{error && <p style={{ color: "red" }}>{error}</p>}
{content && <pre>{content}</pre>}
</div>
);
}
loading 状态别省。桌面应用的用户对「点了没反应」特别敏感,一个磁盘 IO 慢一点就会让人怀疑程序卡死了。
Tipcatch 到的
err是 Go 侧error.Error()的字符串,不是一个结构化对象。想传错误码、错误详情这些信息,就在 Go 里定义一个结构体作为正常返回值,把成功失败的判断放进业务字段里。
比如这样:
type Result struct {
OK bool `json:"ok"`
Code string `json:"code"`
Message string `json:"message"`
Data string `json:"data"`
}
func (a *App) LoadSafe(path string) Result {
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return Result{OK: false, Code: "NOT_FOUND", Message: "文件不存在"}
}
return Result{OK: false, Code: "IO_ERROR", Message: err.Error()}
}
return Result{OK: true, Data: string(data)}
}
前端就能按 code 分支处理,而不是靠字符串匹配去猜错误类型。
15-5 window.go 是怎么来的
这套调用机制不是魔法。Wails 在给浏览器返回 index.html 时,会往 <body> 里注入两个脚本:
<script src="/wails/ipc.js"></script>
<script src="/wails/runtime.js"></script>
ipc.js 负责建立前端到 Go 的调用通道,也就是 window.go 的来源。runtime.js 提供 window.runtime,下一章会详细讲。
这两个脚本由 Wails 的资产服务器动态提供,不在你的项目目录里,所以别去 frontend/ 里找。
大多数情况下自动注入就够了。但如果你用了自己的开发服务器(比如 Create React App 的 yarn start),前端重建时会覆盖掉 Wails 注入的内容,导致 window.go 不存在。
这时候需要关掉自动注入,手动写进 index.html:
<head>
<meta name="wails-options" content="noautoinject" />
<script src="/wails/ipc.js"></script>
<script src="/wails/runtime.js"></script>
</head>
wails-options 这个 meta 标签支持三个值:
| 值 | 作用 |
|---|---|
noautoinjectruntime | 只禁用 runtime.js 的自动注入 |
noautoinjectipc | 只禁用 ipc.js 的自动注入 |
noautoinject | 全部禁用 |
多个值用逗号分隔。
Warning用 Vite 的
react-ts官方模板不需要做这些配置,Wails 的 dev 流程已经处理好了。只有接入外部开发服务器时才会遇到这个问题。手动注入了却忘记加noautoinject,脚本会被加载两遍。
常见误区
忘了 await。const r = Greet("x") 拿到的是 Promise 对象,打印出来是 [object Promise]。TypeScript 会报类型错误,纯 JS 项目里就只能靠自己发现。
import 路径写错。wailsjs 在 frontend/ 根目录下,不在 src/ 里。组件层级越深,../ 越多。可以在 tsconfig.json 里配 path alias 简化。
在 useEffect 里直接写 async 函数。useEffect(async () => {...}) 是错的,effect 的返回值会被当成清理函数。正确写法是在里面定义一个 async 函数再调用,或者用 .then() 链。
指望前端能捕获 Go 的 panic。Go 方法里 panic 会让整个应用崩溃,不会变成一个 Promise rejection。可能 panic 的地方自己加 defer recover()。
在渲染期间调用绑定方法。组件函数体里直接 Greet(...),每次渲染都会触发一次 IPC 调用。放进 useEffect 或事件处理函数里。
用 int64 传大数。前面说过的精度问题,症状是前端拿到的数字末尾几位对不上。转成字符串传。
小结
前端调 Go 这条链路,本质就三步:Wails 注入 ipc.js 建立通道 → 生成的 wailsjs/go/ 模块包装成普通函数 → 调用返回 Promise。
Promise 的行为由 Go 方法签名决定:单返回值永远 resolve,带 error 的在 error 非 nil 时 reject。
类型映射基本符合直觉,两个例外要记牢:int64 有精度风险,map 的 key 只能是 string。
错误处理上,简单场景用 try/catch 接住 error 字符串;需要错误码和结构化信息时,改成返回一个带业务状态的结构体。
下一章跳出绑定,看看运行时(Runtime)这套 API 集合——窗口、对话框、剪贴板、日志,都在里面。