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:
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
wails3 generate bindings
Output:
INFO 347 Packages, 3 Services, 12 Methods, 0 Enums, 0 Models in 1.98s
INFO Output directory: /myproject/frontend/bindingsStruktur yang dihasilkan:
- frontend/bindings
- myapp
- calculatorservice.js
- counterservice.js
- databaseservice.js
- index.js
- myapp
Generasi TypeScript
wails3 generate bindings -ts
Menghasilkan file .ts dengan tipe TypeScript penuh.
Direktori Output Kustom
wails3 generate bindings -d ./src/bindings
Mode Watch (Development)
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
↓ ↓ ↓ ↓ ↓
<0.1ms <0.1ms [varies] <0.1ms <0.1msDibandingkan 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
Pelajari mendalam sistem service.
Bind struktur data kompleks.
Pahami mekanisme jembatan.
Gunakan event untuk komunikasi pub/sub.
Pertanyaan? Tanyakan di Discord atau lihat contoh binding.