Binding Metode

Binding Go-JavaScript Type-Safe

Wails secara otomatis menghasilkan binding JavaScript/TypeScript type-safe untuk metode Go Anda. Tulis kode Go, jalankan satu perintah, dan dapatkan fungsi frontend bertipe penuh tanpa overhead HTTP, tanpa pekerjaan manual, dan tanpa boilerplate.

Memulai Cepat

1. Tulis service Go:

type GreetService struct{}

func (g *GreetService) Greet(name string) string {
    return "Hello, " + name + "!"
}

2. Daftarkan service:

app := application.New(application.Options{
    Services: []application.Service{
        application.NewService(&GreetService{}),
    },
})

3. Hasilkan binding:

bash
wails3 generate bindings

4. Gunakan di JavaScript:

import { Greet } from './bindings/changeme/greetservice'

const message = await Greet("World")
console.log(message)  // "Hello, World!"

Itu saja! Panggilan Go-ke-JavaScript type-safe.

Membuat Service

Service Dasar

package main

import "github.com/wailsapp/wails/v3/pkg/application"

type CalculatorService struct{}

func (c *CalculatorService) Add(a, b int) int {
    return a + b
}

func (c *CalculatorService) Subtract(a, b int) int {
    return a - b
}

func (c *CalculatorService) Multiply(a, b int) int {
    return a * b
}

func (c *CalculatorService) Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

Daftarkan:

app := application.New(application.Options{
    Services: []application.Service{
        application.NewService(&CalculatorService{}),
    },
})

Poin penting:

  • Hanya metode yang diekspor (PascalCase) yang di-bind
  • Metode dapat mengembalikan nilai atau (value, error)
  • Service adalah singleton (satu instance per aplikasi)

Service dengan State

type CounterService struct {
    count int
    mu    sync.Mutex
}

func (c *CounterService) Increment() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
    return c.count
}

func (c *CounterService) Decrement() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count--
    return c.count
}

func (c *CounterService) GetCount() int {
    c.mu.RLock()
    defer c.mu.RUnlock()
    return c.count
}

func (c *CounterService) Reset() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count = 0
}

Penting: Service dibagikan ke semua window. Gunakan mutex untuk thread safety.

Service dengan Dependensi

type DatabaseService struct {
    db *sql.DB
}

func NewDatabaseService(db *sql.DB) *DatabaseService {
    return &DatabaseService{db: db}
}

func (d *DatabaseService) GetUser(id int) (*User, error) {
    var user User
    err := d.db.QueryRow("SELECT * FROM users WHERE id = ?", id).Scan(&user)
    return &user, err
}

Daftarkan dengan dependensi:

db, _ := sql.Open("sqlite3", "app.db")

app := application.New(application.Options{
    Services: []application.Service{
        application.NewService(NewDatabaseService(db)),
    },
})

Menghasilkan Binding

Generasi Dasar

bash
wails3 generate bindings

Output:

INFO  347 Packages, 3 Services, 12 Methods, 0 Enums, 0 Models in 1.98s
INFO  Output directory: /myproject/frontend/bindings

Struktur yang dihasilkan:

  • frontend/bindings
    • myapp
      • calculatorservice.js
      • counterservice.js
      • databaseservice.js
      • index.js

Generasi TypeScript

bash
wails3 generate bindings -ts

Menghasilkan file .ts dengan tipe TypeScript penuh.

Direktori Output Kustom

bash
wails3 generate bindings -d ./src/bindings

Mode Watch (Development)

bash
wails3 dev

Secara otomatis meregenerasi binding saat kode Go berubah.

Menggunakan Binding

JavaScript

Binding yang dihasilkan:

// frontend/bindings/<full-go-import-path>/calculatorservice.js
// (Real generated output — imports $Call from /wails/runtime.js and calls $Call.ByID
// with a numeric method ID. Generate with `wails3 generate bindings -names` to get
// $Call.ByName("<package>.<Struct>.<Method>", ...) instead.)
import { Call as $Call, Create as $Create } from "/wails/runtime.js";

/**
 * @param {number} $0
 * @param {number} $1
 * @returns {Promise<number>}
 */
export function Add($0, $1) {
    return $Call.ByID(1234567890, $0, $1); // numeric ID assigned by the generator
}

Penggunaan:

import { Add, Subtract, Multiply, Divide } from './bindings/changeme/calculatorservice'

// Simple calls
const sum = await Add(5, 3)        // 8
const diff = await Subtract(10, 4)  // 6
const product = await Multiply(7, 6) // 42

// Error handling
try {
    const result = await Divide(10, 0)
} catch (error) {
    console.error("Error:", error)  // "division by zero"
}

TypeScript

Binding yang dihasilkan:

// frontend/bindings/changeme/calculatorservice.ts

export function Add(a: number, b: number): Promise<number>
export function Subtract(a: number, b: number): Promise<number>
export function Multiply(a: number, b: number): Promise<number>
export function Divide(a: number, b: number): Promise<number>

Penggunaan:

import { Add, Divide } from './bindings/changeme/calculatorservice'

const sum: number = await Add(5, 3)

try {
    const result = await Divide(10, 0)
} catch (error: unknown) {
    if (error instanceof Error) {
        console.error(error.message)
    }
}

Manfaat:

  • Pemeriksaan tipe penuh
  • Autocomplete IDE
  • Error compile-time
  • Refactoring lebih baik

File Index

Index yang dihasilkan:

// frontend/bindings/changeme/index.js

export * as CalculatorService from './calculatorservice.js'
export * as CounterService from './counterservice.js'
export * as DatabaseService from './databaseservice.js'

Import yang disederhanakan:

import { CalculatorService } from './bindings/myapp'

const sum = await CalculatorService.Add(5, 3)

Pemetaan Tipe

Tipe Primitif

Tipe Go JavaScript/TypeScript
string string
bool boolean
int, int8, int16, int32, int64 number
uint, uint8, uint16, uint32, uint64 number
float32, float64 number
byte number
rune number

Tipe Kompleks

Tipe Go JavaScript/TypeScript Catatan
[]T T[] -
[N]T T[] -
map[string]T { [_: string]: T } map dengan key string
map[K]V { [_ in K]?: V } K non-string dirender sebagai mapped type, bukan JS Map
[]byte string base64-encoded
struct class / interface dengan field
time.Time any diserialisasi sebagai string RFC3339Nano di runtime
*T T | null pointer berarti nullable
any / interface{} any -
error any / Exception Exception jika sebagai return value, selain itu any

Tipe yang Tidak Didukung

Tipe berikut tidak dapat dilewati jembatan:

  • chan T (channel)
  • func() (fungsi)
  • Interface kompleks (kecuali interface{})
  • Field tidak diekspor (lowercase)

Solusi: Gunakan ID atau handle:

// ❌ Can't return file handle
func OpenFile(path string) (*os.File, error)

// ✅ 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()
}

Penanganan Error

Sisi Go

func (d *DatabaseService) GetUser(id int) (*User, error) {
    if id <= 0 {
        return nil, errors.New("invalid user ID")
    }
    
    var user User
    err := d.db.QueryRow("SELECT * FROM users WHERE id = ?", id).Scan(&user)
    if err == sql.ErrNoRows {
        return nil, fmt.Errorf("user %d not found", id)
    }
    if err != nil {
        return nil, fmt.Errorf("database error: %w", err)
    }
    
    return &user, nil
}

Sisi JavaScript

import { GetUser } from './bindings/changeme/databaseservice'

try {
    const user = await GetUser(123)
    console.log("User:", user)
} catch (error) {
    console.error("Error:", error)
    // Error: "user 123 not found"
}

Tipe error:

  • Go error → JavaScript exception
  • Pesan error dipertahankan
  • Stack trace tersedia

Performa

Overhead Panggilan

Panggilan tipikal: <1ms

JavaScript → Bridge → Go → Bridge → JavaScript
    ↓           ↓       ↓       ↓           ↓
  &lt;0.1ms    &lt;0.1ms  [varies] &lt;0.1ms    &lt;0.1ms

Dibandingkan alternatif:

  • HTTP/REST: 5-50ms
  • IPC: 1-10ms
  • Wails: <1ms

Tips Optimasi

✅ Operasi batch:

// ❌ Slow: N calls
for (const item of items) {
    await ProcessItem(item)
}

// ✅ Fast: 1 call
await ProcessItems(items)

✅ Cache hasil:

// ❌ Repeated calls
const config1 = await GetConfig()
const config2 = await GetConfig()

// ✅ Cache
const config = await GetConfig()
// Use config multiple times

✅ Gunakan event untuk streaming:

func ProcessLargeFile(path string) error {
    // Emit progress events
    for line := range lines {
        app.Event.Emit("progress", line)
    }
    return nil
}

Contoh Lengkap

Go:

package main

import (
    "fmt"
    "github.com/wailsapp/wails/v3/pkg/application"
)

type TodoService struct {
    todos []Todo
}

type Todo struct {
    ID        int    `json:"id"`
    Title     string `json:"title"`
    Completed bool   `json:"completed"`
}

func (t *TodoService) GetAll() []Todo {
    return t.todos
}

func (t *TodoService) Add(title string) Todo {
    todo := Todo{
        ID:        len(t.todos) + 1,
        Title:     title,
        Completed: false,
    }
    t.todos = append(t.todos, todo)
    return todo
}

func (t *TodoService) Toggle(id int) error {
    for i := range t.todos {
        if t.todos[i].ID == id {
            t.todos[i].Completed = !t.todos[i].Completed
            return nil
        }
    }
    return fmt.Errorf("todo %d not found", id)
}

func (t *TodoService) Delete(id int) error {
    for i := range t.todos {
        if t.todos[i].ID == id {
            t.todos = append(t.todos[:i], t.todos[i+1:]...)
            return nil
        }
    }
    return fmt.Errorf("todo %d not found", id)
}

func main() {
    app := application.New(application.Options{
        Services: []application.Service{
            application.NewService(&TodoService{}),
        },
    })
    
    app.Window.New()
    app.Run()
}

JavaScript:

import { GetAll, Add, Toggle, Delete } from './bindings/changeme/todoservice'

class TodoApp {
    async loadTodos() {
        const todos = await GetAll()
        this.renderTodos(todos)
    }
    
    async addTodo(title) {
        try {
            const todo = await Add(title)
            this.loadTodos()
        } catch (error) {
            console.error("Failed to add todo:", error)
        }
    }
    
    async toggleTodo(id) {
        try {
            await Toggle(id)
            this.loadTodos()
        } catch (error) {
            console.error("Failed to toggle todo:", error)
        }
    }
    
    async deleteTodo(id) {
        try {
            await Delete(id)
            this.loadTodos()
        } catch (error) {
            console.error("Failed to delete todo:", error)
        }
    }
    
    renderTodos(todos) {
        const list = document.getElementById('todo-list')
        list.innerHTML = todos.map(todo => `
            <div class="todo ${todo.Completed ? 'completed' : ''}">
                <input type="checkbox" 
                       ${todo.Completed ? 'checked' : ''}
                       onchange="app.toggleTodo(${todo.ID})">
                <span>${todo.Title}</span>
                <button onclick="app.deleteTodo(${todo.ID})">Delete</button>
            </div>
        `).join('')
    }
}

const app = new TodoApp()
app.loadTodos()

Praktik Terbaik

✅ Lakukan

  • Jaga metode tetap sederhana - Single responsibility
  • Kembalikan error - Jangan panic
  • Gunakan state thread-safe - Mutex untuk data bersama
  • Operasi batch - Kurangi panggilan jembatan
  • Cache di sisi Go - Hindari pekerjaan berulang
  • Dokumentasikan metode - Komentar menjadi JSDoc

❌ Jangan

  • Jangan blokir - Gunakan goroutine untuk operasi panjang
  • Jangan kembalikan channel - Gunakan event sebagai gantinya
  • Jangan kembalikan fungsi - Tidak didukung
  • Jangan abaikan error - Selalu tangani
  • Jangan gunakan field tidak diekspor - Tidak akan di-bind

Langkah Selanjutnya

Service

Pelajari mendalam sistem service.

Pelajari Selengkapnya →

Model

Bind struktur data kompleks.

Pelajari Selengkapnya →

Jembatan Go-Frontend

Pahami mekanisme jembatan.

Pelajari Selengkapnya →

Event

Gunakan event untuk komunikasi pub/sub.

Pelajari Selengkapnya →


Pertanyaan? Tanyakan di Discord atau lihat contoh binding.

Edit page

Last updated: