Go-Frontend Bridge
Direct Go-JavaScript Communication
Section titled “Direct Go-JavaScript Communication”Wails provides a direct, in-memory bridge between Go and JavaScript, enabling seamless communication without HTTP overhead, process boundaries, or serialisation bottlenecks.
The Big Picture
Section titled “The Big Picture”Key insight: No HTTP, no IPC, no process boundaries. Just direct function calls with type safety.
How It Works: Step by Step
Section titled “How It Works: Step by Step”1. Service Registration (Startup)
Section titled “1. Service Registration (Startup)”When your application starts, Wails scans your services:
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 serviceapp := application.New(application.Options{ Services: []application.Service{ application.NewService(&GreetService{prefix: "Hello, "}), },})What Wails does:
- Scans the struct for exported methods
- Extracts type information (parameters, return types)
- Builds a registry mapping method names to functions
- Generates TypeScript bindings with full type definitions
2. Binding Generation (Build Time)
Section titled “2. Binding Generation (Build Time)”Wails generates TypeScript bindings automatically:
export function Greet(name: string): Promise<string>export function Add(a: number, b: number): Promise<number>Type mapping:
| Go Type | TypeScript Type |
|---|---|
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 | Exception (thrown) |
3. Frontend Call (Runtime)
Section titled “3. Frontend Call (Runtime)”Developer calls the Go method from JavaScript:
import { Greet, Add } from './bindings/GreetService'
// Call Go from JavaScriptconst greeting = await Greet("World")console.log(greeting) // "Hello, World!"
const sum = await Add(5, 3)console.log(sum) // 8What happens:
- Binding function called -
Greet("World") - Message created -
{ service: "GreetService", method: "Greet", args: ["World"] } - Sent to bridge - Via WebView’s JavaScript bridge
- Promise returned - Awaits response
4. Bridge Processing (Runtime)
Section titled “4. Bridge Processing (Runtime)”The bridge receives the message and processes it:
Security: Only registered services and exported methods are callable.
5. Go Execution (Runtime)
Section titled “5. Go Execution (Runtime)”The Go method executes:
func (g *GreetService) Greet(name string) string { // This runs in Go return g.prefix + name + "!"}Execution context:
- Runs in a goroutine (non-blocking)
- Has access to all Go features (file system, network, databases)
- Can call other Go code freely
- Returns result or error
6. Response (Runtime)
Section titled “6. Response (Runtime)”Result is sent back to JavaScript:
// Promise resolves with resultconst greeting = await Greet("World")// greeting = "Hello, World!"Error handling:
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"}Performance Characteristics
Section titled “Performance Characteristics”Typical call overhead: <1ms
Frontend Call → Bridge → Go Execution → Bridge → Frontend Response ↓ ↓ ↓ ↓ ↓ <0.1ms <0.1ms [varies] <0.1ms <0.1msCompared to alternatives:
- HTTP/REST: 5-50ms (network stack, serialisation)
- IPC: 1-10ms (process boundaries, marshalling)
- Wails Bridge: <1ms (in-memory, direct call)
Memory
Section titled “Memory”Per-call overhead: ~1KB (message buffer)
Zero-copy optimisation: Large data (>1MB) uses shared memory where possible.
Concurrency
Section titled “Concurrency”Calls are concurrent:
- Each call runs in its own goroutine
- Multiple calls can execute simultaneously
- No blocking between calls
// These run concurrentlyconst [result1, result2, result3] = await Promise.all([ SlowOperation1(), SlowOperation2(), SlowOperation3(),])Type System
Section titled “Type System”Supported Types
Section titled “Supported Types”Primitives
Section titled “Primitives”// Gofunc 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]>Slices and Arrays
Section titled “Slices and Arrays”// Gofunc Sum(numbers []int) int { total := 0 for _, n := range numbers { total += n } return total}// TypeScriptfunction Sum(numbers: number[]): Promise<number>
// Usageconst total = await Sum([1, 2, 3, 4, 5]) // 15// Gofunc GetConfig() map[string]interface{} { return map[string]interface{}{ "theme": "dark", "fontSize": 14, "enabled": true, }}// TypeScriptfunction GetConfig(): Promise<Record<string, any>>
// Usageconst config = await GetConfig()console.log(config.theme) // "dark"Structs
Section titled “Structs”// Gotype 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>
// Usageconst user = await GetUser(1)console.log(user.name) // "Alice"JSON tags: Use json: tags to control field names in TypeScript.
// Gofunc GetTimestamp() time.Time { return time.Now()}// TypeScriptfunction GetTimestamp(): Promise<Date>
// Usageconst timestamp = await GetTimestamp()console.log(timestamp.toISOString())Errors
Section titled “Errors”// Gofunc Validate(input string) error { if input == "" { return errors.New("input cannot be empty") } return nil}// TypeScriptfunction Validate(input: string): Promise<void>
// Usagetry { await Validate("")} catch (error) { console.error(error) // "input cannot be empty"}Unsupported Types
Section titled “Unsupported Types”These types cannot be passed across the bridge:
- Channels (
chan T) - Functions (
func()) - Interfaces (except
interface{}/any) - Pointers (except to structs)
- Unexported fields (lowercase)
Workaround: Use IDs or handles:
// ❌ Can't pass file handlefunc OpenFile(path string) (*os.File, error) { return os.Open(path)}
// ✅ Return file ID insteadvar 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()}Advanced Patterns
Section titled “Advanced Patterns”Context Passing
Section titled “Context Passing”Services can access the call context:
type UserService struct{}
func (s *UserService) GetCurrentUser(ctx context.Context) (*User, error) { // Access window that made the call window := application.ContextWindow(ctx)
// Access application app := application.ContextApplication(ctx)
// Your logic return getCurrentUser(), nil}Context provides:
- Window that made the call
- Application instance
- Request metadata
Streaming Data
Section titled “Streaming Data”For large data, use events instead of return values:
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 progressEvents.On('file-progress', (data) => { console.log(`Line ${data.line}: ${data.text}`)})
// Start processingawait ProcessLargeFile('/path/to/large/file.txt')Cancellation
Section titled “Cancellation”Use context for cancellable operations:
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}Note: Context cancellation on frontend disconnect is automatic.
Batch Operations
Section titled “Batch Operations”Reduce bridge overhead by batching:
// ❌ Inefficient: N bridge callsfor _, item := range items { await ProcessItem(item)}
// ✅ Efficient: 1 bridge callawait 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}Debugging the Bridge
Section titled “Debugging the Bridge”Enable Debug Logging
Section titled “Enable Debug Logging”app := application.New(application.Options{ Name: "My App", Logger: application.NewDefaultLogger(), LogLevel: logger.DEBUG,})Output shows:
- Method calls
- Parameters
- Return values
- Errors
- Timing information
Inspect Generated Bindings
Section titled “Inspect Generated Bindings”Check frontend/bindings/ to see generated TypeScript:
export function MyMethod(arg: string): Promise<number> { return window.wails.Call('MyService.MyMethod', arg)}Test Services Directly
Section titled “Test Services Directly”Test Go services without the frontend:
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) }}Performance Tips
Section titled “Performance Tips”- Batch operations - Reduce bridge calls
- Use events for streaming - Don’t return large arrays
- Keep methods fast - <100ms ideal
- Use goroutines - For long operations
- Cache on Go side - Avoid repeated calculations
❌ Don’t
Section titled “❌ Don’t”- Don’t make excessive calls - Batch when possible
- Don’t return huge data - Use pagination or streaming
- Don’t block - Use goroutines for long operations
- Don’t pass complex types - Keep it simple
- Don’t ignore errors - Always handle them
Security
Section titled “Security”The bridge is secure by default:
- Whitelist only - Only registered services callable
- Type validation - Arguments checked against Go types
- No eval() - Frontend can’t execute arbitrary Go code
- No reflection abuse - Only exported methods accessible
Best practices:
- Validate input in Go (don’t trust frontend)
- Use context for authentication/authorisation
- Rate limit expensive operations
- Sanitise file paths and user input
Next Steps
Section titled “Next Steps”Build System - Learn how Wails builds and bundles your application
Learn More →
Services - Deep dive into the service system
Learn More →
Events - Use events for pub/sub communication
Learn More →
Questions about the bridge? Ask in Discord or check the binding examples.