Go–フロントエンドブリッジ
Wails が Go と JavaScript の直接通信を実現する仕組みを詳しく解説します
Go と JavaScript の直接通信
Wails は Go と JavaScript の間にメモリ内で動作する直接ブリッジを提供し、HTTP のオーバーヘッド、プロセス境界、シリアライズのボトルネックなしでシームレスな通信を実現します。
全体像
要点:HTTP、IPC、プロセス境界はありません。型安全性を備えた直接的な関数呼び出しだけです。
仕組み:ステップごとの解説
1. サービスの登録(起動時)
アプリケーションの起動時に、Wails がサービスをスキャンします。
type GreetService struct {
prefix string
}
func (g *GreetService) Greet(name string) string {
return g.prefix + name + "!"
}
func (g *GreetService) Add(a, b int) int {
return a + b
}
// Register service
app := application.New(application.Options{
Services: []application.Service{
application.NewService(&GreetService{prefix: "Hello, "}),
},
})Wails が行う処理:
- エクスポートされたメソッドを検出するため、構造体をスキャンします
- 型情報を抽出します(パラメーター、戻り値の型)
- メソッド名を関数に対応付けるレジストリを構築します
- 完全な型定義を含むTypeScript バインディングを生成します
2. バインディングの生成(ビルド時)
Wails は TypeScript バインディングを自動的に生成します。
// Auto-generated: frontend/bindings/GreetService.ts
export function Greet(name: string): Promise<string>
export function Add(a: number, b: number): Promise<number>型のマッピング:
| Go の型 | TypeScript の型 |
|---|---|
string |
string |
int、int32、int64 |
number |
float32、float64 |
number |
bool |
boolean |
[]T |
T[] |
map[string]T |
Record<string, T> |
struct |
interface |
time.Time |
Date |
error |
例外(スローされる) |
3. フロントエンドからの呼び出し(実行時)
開発者が JavaScript から Go メソッドを呼び出します。
import { Greet, Add } from './bindings/GreetService'
// Call Go from JavaScript
const greeting = await Greet("World")
console.log(greeting) // "Hello, World!"
const sum = await Add(5, 3)
console.log(sum) // 8実行される処理:
- バインディング関数が呼び出されます -
Greet("World") - メッセージが作成されます -
{ service: "GreetService", method: "Greet", args: ["World"] } - ブリッジに送信 — WebView の JavaScript ブリッジ経由
- Promise を返す — レスポンスを待機
4. ブリッジでの処理(実行時)
ブリッジはメッセージを受信し、処理します。
セキュリティ:呼び出せるのは、登録済みのサービスとエクスポートされたメソッドだけです。
5. Go での実行(実行時)
Go メソッドが実行されます。
func (g *GreetService) Greet(name string) string {
// This runs in Go
return g.prefix + name + "!"
}実行コンテキスト:
- goroutine で実行(ノンブロッキング)
- Go のすべての機能(ファイルシステム、ネットワーク、データベース)を利用可能
- 他の Go コードを自由に呼び出し可能
- 結果またはエラーを返す
6. レスポンス(実行時)
結果が JavaScript に返されます。
// Promise resolves with result
const greeting = await Greet("World")
// greeting = "Hello, World!"エラー処理:
func (g *GreetService) Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}try {
const result = await Divide(10, 0)
} catch (error) {
console.error("Go error:", error) // "division by zero"
}パフォーマンス特性
速度
一般的な呼び出しオーバーヘッド: <1ms
Frontend Call → Bridge → Go Execution → Bridge → Frontend Response
↓ ↓ ↓ ↓ ↓
<0.1ms <0.1ms [varies] <0.1ms <0.1ms他の方式との比較:
- HTTP/REST: 5-50ms(ネットワークスタック、シリアル化)
- IPC: 1-10ms(プロセス境界、マーシャリング)
- Wails Bridge: <1ms(インメモリ、直接呼び出し)
メモリ
呼び出しごとのオーバーヘッド: 約1KB(メッセージバッファ)
ゼロコピー最適化:大容量データ(>1MB)では、可能な場合に共有メモリを使用します。
並行処理
呼び出しは並行して実行されます:
- 各呼び出しは個別の goroutine で実行される
- 複数の呼び出しを同時に実行可能
- 呼び出し間でブロッキングが発生しない
// These run concurrently
const [result1, result2, result3] = await Promise.all([
SlowOperation1(),
SlowOperation2(),
SlowOperation3(),
])型システム
サポートされる型
プリミティブ型
// Go
func Example(
s string,
i int,
f float64,
b bool,
) (string, int, float64, bool) {
return s, i, f, b
}// TypeScript (auto-generated)
function Example(
s: string,
i: number,
f: number,
b: boolean,
): Promise<[string, number, number, boolean]>スライスと配列
// Go
func Sum(numbers []int) int {
total := 0
for _, n := range numbers {
total += n
}
return total
}// TypeScript
function Sum(numbers: number[]): Promise<number>
// Usage
const total = await Sum([1, 2, 3, 4, 5]) // 15マップ
// Go
func GetConfig() map[string]interface{} {
return map[string]interface{}{
"theme": "dark",
"fontSize": 14,
"enabled": true,
}
}// TypeScript
function GetConfig(): Promise<Record<string, any>>
// Usage
const config = await GetConfig()
console.log(config.theme) // "dark"構造体
// Go
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func GetUser(id int) (*User, error) {
return &User{
ID: id,
Name: "Alice",
Email: "alice@example.com",
}, nil
}// TypeScript (auto-generated)
interface User {
id: number
name: string
email: string
}
function GetUser(id: number): Promise<User>
// Usage
const user = await GetUser(1)
console.log(user.name) // "Alice"JSON タグ:TypeScript でのフィールド名を制御するには、json: タグを使用します。
時刻
// Go
func GetTimestamp() time.Time {
return time.Now()
}// TypeScript
function GetTimestamp(): Promise<Date>
// Usage
const timestamp = await GetTimestamp()
console.log(timestamp.toISOString())エラー
// Go
func Validate(input string) error {
if input == "" {
return errors.New("input cannot be empty")
}
return nil
}// TypeScript
function Validate(input: string): Promise<void>
// Usage
try {
await Validate("")
} catch (error) {
console.error(error) // "input cannot be empty"
}サポートされない型
次の型はブリッジ経由で渡すことができません。
- チャネル(
chan T) - 関数(
func()) - インターフェース(
interface{}/anyを除く) - ポインター(構造体へのポインターを除く)
- 非公開フィールド(小文字で始まるフィールド)
回避策:ID またはハンドルを使用します:
// ❌ Can't pass file handle
func OpenFile(path string) (*os.File, error) {
return os.Open(path)
}
// ✅ Return file ID instead
var files = make(map[string]*os.File)
func OpenFile(path string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", err
}
id := generateID()
files[id] = file
return id, nil
}
func ReadFile(id string) ([]byte, error) {
file := files[id]
return io.ReadAll(file)
}
func CloseFile(id string) error {
file := files[id]
delete(files, id)
return file.Close()
}高度なパターン
コンテキストの受け渡し
サービスは呼び出しコンテキストにアクセスできます:
type UserService struct{}
func (s *UserService) GetCurrentUser(ctx context.Context) (*User, error) {
// Access the calling window via the context value
window, _ := ctx.Value(application.WindowKey).(application.Window)
_ = window
// Access the application
app := application.Get()
_ = app
// Your logic
return getCurrentUser(), nil
}コンテキストから取得できる情報:
- 呼び出し元のウィンドウ
- アプリケーションインスタンス
- リクエストのメタデータ
データのストリーミング
大量のデータには、戻り値ではなくイベントを使用します:
func ProcessLargeFile(path string) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
lineNum := 0
for scanner.Scan() {
lineNum++
// Emit progress events
app.Event.Emit("file-progress", map[string]interface{}{
"line": lineNum,
"text": scanner.Text(),
})
}
return scanner.Err()
}import { Events } from '@wailsio/runtime'
import { ProcessLargeFile } from './bindings/FileService'
// Listen for progress
Events.On('file-progress', (data) => {
console.log(`Line ${data.line}: ${data.text}`)
})
// Start processing
await ProcessLargeFile('/path/to/large/file.txt')キャンセル
キャンセル可能な処理にはコンテキストを使用します:
func LongRunningTask(ctx context.Context) error {
for i := 0; i < 1000; i++ {
// Check if cancelled
select {
case <-ctx.Done():
return ctx.Err()
default:
// Continue work
time.Sleep(100 * time.Millisecond)
}
}
return nil
}注:フロントエンドの切断時には、コンテキストが自動的にキャンセルされます。
バッチ処理
処理をバッチ化してブリッジのオーバーヘッドを削減します:
// ❌ Inefficient: N bridge calls
for _, item := range items {
await ProcessItem(item)
}
// ✅ Efficient: 1 bridge call
await ProcessItems(items)func ProcessItems(items []Item) ([]Result, error) {
results := make([]Result, len(items))
for i, item := range items {
results[i] = processItem(item)
}
return results, nil
}ブリッジのデバッグ
デバッグログの有効化
app := application.New(application.Options{
Name: "My App",
LogLevel: slog.LevelDebug, // requires `import "log/slog"`
// `Logger` is an optional *slog.Logger; the default-logger helper is
// application.DefaultLogger(slog.Leveler) if you want to construct one explicitly.
})出力に表示される情報:
- メソッド呼び出し
- 引数
- 戻り値
- エラー
- タイミング情報
生成されたバインディングの確認
生成された TypeScript を確認するには、frontend/bindings/を調べます:
// frontend/bindings/<full-go-import-path>/myservice.js (real generated shape)
import { Call as $Call } from "/wails/runtime.js";
export function MyMethod($0) {
return $Call.ByID(1234567890, $0); // numeric method ID assigned by the generator
}サービスの直接テスト
フロントエンドを使用せずに Go サービスをテストします:
func TestGreetService(t *testing.T) {
service := &GreetService{prefix: "Hello, "}
result := service.Greet("Test")
if result != "Hello, Test!" {
t.Errorf("Expected 'Hello, Test!', got '%s'", result)
}
}パフォーマンスのヒント
✅ 推奨事項
- 処理をバッチ化する — ブリッジの呼び出し回数を減らす
- ストリーミングにはイベントを使用する — 大きな配列を返さない
- メソッドを高速に保つ — 理想は<100ms
- goroutine を使用する — 長時間の処理に使用する
- Go 側でキャッシュする — 計算の繰り返しを避ける
❌ 禁止事項
- 過剰に呼び出さない — 可能な場合はバッチ化する
- 巨大なデータを返さない — ページネーションまたはストリーミングを使用する
- ブロックしない — 長時間の処理には goroutine を使用する
- 複雑な型を渡さない — 単純な型を使用する
- エラーを無視しない — 必ず処理する
セキュリティ
ブリッジはデフォルトで安全です:
- ホワイトリストのみ — 呼び出せるのは登録済みのサービスだけです
- 型の検証 — 引数が Go の型に照らして検証されます
- eval() 不使用 — フロントエンドから任意の Go コードを実行することはできません
- リフレクションの悪用を防止 — アクセスできるのは公開メソッドだけです
ベストプラクティス:
- Go で入力を検証する(フロントエンドを信頼しない)
- 認証/認可にはコンテキストを使用する
- 高コストな処理をレート制限する
- ファイルパスとユーザー入力をサニタイズする
次のステップ
ビルドシステム — Wails がアプリケーションをビルドしてバンドルする仕組みを学びます 詳細を見る →
サービス — サービスシステムを詳しく掘り下げます 詳細を見る →
イベント - パブリッシュ/サブスクライブ通信にイベントを使用します 詳細を見る →