Jembatan Go-Frontend
Komunikasi Go-JavaScript Langsung
Wails menyediakan jembatan in-memory langsung antara Go dan JavaScript, memungkinkan komunikasi seamless tanpa overhead HTTP, batas proses, atau bottleneck serialisasi.
Gambaran Besar
direction: right
Frontend: "Frontend (JavaScript)" {
UI: "React/Vue/Vanilla" {
shape: rectangle
style.fill: "#8B5CF6"
}
Bindings: "Auto-Generated Bindings" {
shape: rectangle
style.fill: "#A78BFA"
}
}
Bridge: "Wails Bridge" {
Encoder: "JSON Encoder" {
shape: rectangle
style.fill: "#10B981"
}
Router: "Method Router" {
shape: diamond
style.fill: "#10B981"
}
Decoder: "JSON Decoder" {
shape: rectangle
style.fill: "#10B981"
}
TypeGen: "Type Generator" {
shape: rectangle
style.fill: "#10B981"
}
}
Backend: "Backend (Go)" {
Services: "Your Services" {
shape: rectangle
style.fill: "#00ADD8"
}
Registry: "Service Registry" {
shape: rectangle
style.fill: "#00ADD8"
}
}
Frontend.UI -> Frontend.Bindings: "import { Method }"
Frontend.Bindings -> Bridge.Encoder: "Call Method('arg')"
Bridge.Encoder -> Bridge.Router: "Encode to JSON"
Bridge.Router -> Backend.Registry: "Find service"
Backend.Registry -> Backend.Services: "Invoke method"
Backend.Services -> Bridge.Decoder: "Return result"
Bridge.Decoder -> Frontend.Bindings: "Decode to JS"
Frontend.Bindings -> Frontend.UI: "Promise resolves"
Bridge.TypeGen -> Frontend.Bindings: "Generate types"Wawasan kunci: Tanpa HTTP, tanpa IPC, tanpa batas proses. Hanya panggilan fungsi langsung dengan type safety.
Cara Kerjanya: Langkah demi Langkah
1. Registrasi Service (Startup)
Ketika aplikasi Anda start, Wails memindai service Anda:
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, "}),
},
})Yang dilakukan Wails:
- Memindai struct untuk metode yang diekspor
- Mengekstrak informasi tipe (parameter, tipe return)
- Membangun registry yang memetakan nama metode ke fungsi
- Menghasilkan binding TypeScript dengan definisi tipe lengkap
2. Generasi Binding (Waktu Build)
Wails menghasilkan binding TypeScript secara otomatis:
// Auto-generated: frontend/bindings/GreetService.ts
export function Greet(name: string): Promise<string>
export function Add(a: number, b: number): Promise<number>Pemetaan tipe:
| Tipe Go | Tipe 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 |
Exception (thrown) |
3. Panggilan Frontend (Runtime)
Developer memanggil metode Go dari JavaScript:
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) // 8Yang terjadi:
- Fungsi binding dipanggil -
Greet("World") - Pesan dibuat -
{ service: "GreetService", method: "Greet", args: ["World"] } - Dikirim ke bridge - Via JavaScript bridge WebView
- Promise dikembalikan - Menunggu respons
4. Pemrosesan Bridge (Runtime)
Bridge menerima pesan dan memprosesnya:
direction: down
Receive: "Receive Message" {
shape: rectangle
style.fill: "#10B981"
}
Parse: "Parse JSON" {
shape: rectangle
}
Validate: "Validate" {
Check: "Service exists?" {
shape: diamond
}
CheckMethod: "Method exists?" {
shape: diamond
}
CheckTypes: "Types correct?" {
shape: diamond
}
}
Invoke: "Invoke Go Method" {
shape: rectangle
style.fill: "#00ADD8"
}
Encode: "Encode Result" {
shape: rectangle
}
Send: "Send Response" {
shape: rectangle
style.fill: "#10B981"
}
Error: "Send Error" {
shape: rectangle
style.fill: "#EF4444"
}
Receive -> Parse
Parse -> Validate.Check
Validate.Check -> Validate.CheckMethod: "Yes"
Validate.Check -> Error: "No"
Validate.CheckMethod -> Validate.CheckTypes: "Yes"
Validate.CheckMethod -> Error: "No"
Validate.CheckTypes -> Invoke: "Yes"
Validate.CheckTypes -> Error: "No"
Invoke -> Encode: "Success"
Invoke -> Error: "Error"
Encode -> SendKeamanan: Hanya service terdaftar dan metode yang diekspor yang dapat dipanggil.
5. Eksekusi Go (Runtime)
Metode Go dieksekusi:
func (g *GreetService) Greet(name string) string {
// This runs in Go
return g.prefix + name + "!"
}Context eksekusi:
- Berjalan di goroutine (non-blocking)
- Memiliki akses ke semua fitur Go (file system, jaringan, database)
- Dapat memanggil kode Go lain dengan bebas
- Mengembalikan hasil atau error
6. Respons (Runtime)
Hasil dikirim kembali ke JavaScript:
// Promise resolves with result
const greeting = await Greet("World")
// greeting = "Hello, World!"Penanganan error:
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"
}Karakteristik Performa
Kecepatan
Overhead panggilan tipikal: <1ms
Frontend Call → Bridge → Go Execution → Bridge → Frontend Response
↓ ↓ ↓ ↓ ↓
<0.1ms <0.1ms [varies] <0.1ms <0.1msDibanding alternatif:
- HTTP/REST: 5-50ms (network stack, serialisasi)
- IPC: 1-10ms (batas proses, marshalling)
- Wails Bridge: <1ms (in-memory, panggilan langsung)
Memori
Overhead per-panggilan: ~1KB (buffer pesan)
Optimasi zero-copy: Data besar (>1MB) menggunakan shared memory jika memungkinkan.
Concurrency
Panggilan bersifat concurrent:
- Setiap panggilan berjalan di goroutine sendiri
- Beberapa panggilan dapat dieksekusi secara simultan
- Tidak ada blocking antar panggilan
// These run concurrently
const [result1, result2, result3] = await Promise.all([
SlowOperation1(),
SlowOperation2(),
SlowOperation3(),
])Sistem Tipe
Tipe yang Didukung
Primitif
// 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]>Slice dan Array
// 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]) // 15Map
// 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"Struct
// 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"Tag JSON: Gunakan tag json: untuk mengontrol nama field di TypeScript.
Time
// Go
func GetTimestamp() time.Time {
return time.Now()
}// TypeScript
function GetTimestamp(): Promise<Date>
// Usage
const timestamp = await GetTimestamp()
console.log(timestamp.toISOString())Error
// 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"
}Tipe yang Tidak Didukung
Tipe ini tidak dapat diteruskan melintasi bridge:
- Channel (
chan T) - Fungsi (
func()) - Interface (kecuali
interface{}/any) - Pointer (kecuali ke struct)
- Field tidak diekspor (lowercase)
Solusi: Gunakan ID atau handle:
// ❌ 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()
}Pola Lanjutan
Context Passing
Service dapat mengakses context panggilan:
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
}Context menyediakan:
- Window yang melakukan panggilan
- Instance aplikasi
- Metadata request
Streaming Data
Untuk data besar, gunakan event alih-alih nilai return:
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')Pembatalan
Gunakan context untuk operasi yang dapat dibatalkan:
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
}Catatan: Pembatalan context saat frontend disconnect otomatis.
Operasi Batch
Kurangi overhead bridge dengan batching:
// ❌ 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
}Debugging Bridge
Aktifkan Debug Logging
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.
})Output menampilkan:
- Panggilan metode
- Parameter
- Nilai return
- Error
- Informasi timing
Periksa Binding yang Dihasilkan
Periksa frontend/bindings/ untuk melihat TypeScript yang dihasilkan:
// 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
}Uji Service Langsung
Uji service Go tanpa 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)
}
}Tips Performa
✅ Lakukan
- Operasi batch - Kurangi panggilan bridge
- Gunakan event untuk streaming - Jangan return array besar
- Jaga metode tetap cepat - <100ms ideal
- Gunakan goroutine - Untuk operasi panjang
- Cache di sisi Go - Hindari perhitungan berulang
❌ Jangan
- Jangan buat panggilan berlebihan - Batch jika memungkinkan
- Jangan return data besar - Gunakan pagination atau streaming
- Jangan block - Gunakan goroutine untuk operasi panjang
- Jangan kirim tipe kompleks - Jaga tetap sederhana
- Jangan abaikan error - Selalu tangani error
Keamanan
Bridge aman secara default:
- Whitelist saja - Hanya service terdaftar yang dapat dipanggil
- Validasi tipe - Argumen dicek terhadap tipe Go
- Tanpa eval() - Frontend tidak dapat mengeksekusi kode Go sembarangan
- Tanpa penyalahgunaan reflection - Hanya metode yang diekspor yang dapat diakses
Praktik terbaik:
- Validasi input di Go (jangan percaya frontend)
- Gunakan context untuk autentikasi/otorisasi
- Rate limit operasi yang mahal
- Sanitasi path file dan input pengguna
Langkah Selanjutnya
Sistem Build - Pelajari bagaimana Wails mem-build dan membundel aplikasi Anda
Pelajari Selengkapnya →
Service - Pelajari mendalam sistem service
Pelajari Selengkapnya →
Event - Gunakan event untuk komunikasi pub/sub
Pelajari Selengkapnya →
Pertanyaan tentang bridge? Tanyakan di Discord atau lihat contoh binding.