Ice Go SDK Integration Guide

Go SDK for Ice rule engine, fully compatible with Java ice-core functionality.

Overview

Ice Go SDK provides the same core features as the Java SDK:

  • âś… Rule execution engine (sync/async)
  • âś… 5 serial relation nodes + 5 parallel relation nodes
  • âś… 9 leaf node interfaces (auto-adaptation)
  • âś… File client (config loading, hot reload)
  • âś… Time control, debug tracing
  • âś… Thread-safe Roam data container
  • âś… Full context.Context support (trace, timeout control)

Installation

Requirements

  • Go 1.21 or later

Add Dependency

go get github.com/zjn-zjn/ice/sdks/go

Or add to go.mod:

require github.com/zjn-zjn/ice/sdks/go v1.0.1

Quick Start

1. Register Leaf Nodes

package main

import (
    "context"
    
    ice "github.com/zjn-zjn/ice/sdks/go"
    icecontext "github.com/zjn-zjn/ice/sdks/go/context"
)

// Define leaf node (use ice tag for field descriptions)
type ScoreFlow struct {
    Score float64 `json:"score" ice:"name:Score Threshold,desc:Threshold for score comparison"`
    Key   string  `json:"key" ice:"name:Key,desc:Key to get value from roam"`
}

// Implement RoamFlow interface (Note: first parameter is context.Context)
func (s *ScoreFlow) DoRoamFlow(ctx context.Context, roam *icecontext.Roam) bool {
    value := roam.GetMulti(s.Key)
    if value == nil {
        return false
    }
    switch v := value.(type) {
    case float64:
        return v >= s.Score
    case int:
        return float64(v) >= s.Score
    }
    return false
}

func init() {
    // Register leaf node (supports alias for multi-language compatibility)
    ice.RegisterLeaf("com.example.ScoreFlow",
        &ice.LeafMeta{
            Name:  "Score Check",
            Desc:  "Check if score meets threshold",
            Alias: []string{"score_flow"},  // Alias to respond to other language configs
        },
        func() any { return &ScoreFlow{} })
}

2. Start Client

func main() {
    // Create file client
    client, err := ice.NewClient(1, "./ice-data")
    if err != nil {
        log.Fatal(err)
    }

    // Start (load config)
    if err := client.Start(); err != nil {
        log.Fatal(err)
    }
    defer client.Destroy()

    // Execute rules (first parameter is context.Context)
    ctx := context.Background()
    pack := ice.NewPack().SetIceId(1)
    pack.Roam.Put("score", 85)
    
    ctxList := ice.SyncProcess(ctx, pack)
    
    // Process results
    for _, iceCtx := range ctxList {
        fmt.Println("Result:", iceCtx.Pack.Roam.Data())
    }
}

Leaf Node Development

Node Types

Ice Go SDK supports 9 leaf node interfaces, auto-detected during registration. All interfaces have context.Context as the first parameter:

TypeInterface MethodReturnDescription
FlowDoRoamFlow(ctx, roam)boolCondition check
DoPackFlow(ctx, pack)boolAccess full Pack
DoFlow(ctx, iceCtx)boolAccess full Context
ResultDoRoamResult(ctx, roam)boolBusiness operation
DoPackResult(ctx, pack)boolAccess full Pack
DoResult(ctx, iceCtx)boolAccess full Context
NoneDoRoamNone(ctx, roam)voidData query/logging
DoPackNone(ctx, pack)voidAccess full Pack
DoNone(ctx, iceCtx)voidAccess full Context

Flow Node Example

// Condition check node
type AgeCheck struct {
    MinAge int `json:"minAge"`
    MaxAge int `json:"maxAge"`
}

func (a *AgeCheck) DoRoamFlow(ctx context.Context, roam *icecontext.Roam) bool {
    age := roam.GetInt("age", 0)
    return age >= a.MinAge && age <= a.MaxAge
}

func init() {
    ice.RegisterLeaf("com.example.AgeCheck", nil, func() any {
        return &AgeCheck{}
    })
}

Result Node Example

// Business operation node
type PointGrant struct {
    Key    string  `json:"key"`
    Points float64 `json:"points"`
}

func (p *PointGrant) DoRoamResult(ctx context.Context, roam *icecontext.Roam) bool {
    uid := roam.GetInt("uid", 0)
    if uid <= 0 || p.Points <= 0 {
        return false
    }
    // Call business API (can pass ctx for trace, timeout control)
    success := pointService.GrantWithContext(ctx, uid, p.Points)
    roam.Put("GRANT_RESULT", success)
    return success
}

func init() {
    ice.RegisterLeaf("com.example.PointGrant", 
        &ice.LeafMeta{Name: "Point Grant", Desc: "Grant points to user"},
        func() any { return &PointGrant{} })
}

None Node Example

// Data query node
type UserInfoLoader struct {
    UidKey string `json:"uidKey"`
}

func (u *UserInfoLoader) DoRoamNone(ctx context.Context, roam *icecontext.Roam) {
    uid := roam.GetInt(u.UidKey, 0)
    if uid > 0 {
        userInfo := userService.GetUserInfoWithContext(ctx, uid)
        roam.Put("userInfo", userInfo)
    }
}

func init() {
    ice.RegisterLeaf("com.example.UserInfoLoader", nil, func() any {
        return &UserInfoLoader{}
    })
}

Rule Execution

All execution methods have context.Context as the first parameter for trace, timeout control, etc.

Sync Execution

ctx := context.Background()

// Execute by IceId
pack := ice.NewPack().SetIceId(1)
pack.Roam.Put("uid", 12345)
ctxList := ice.SyncProcess(ctx, pack)

// Execute by Scene
pack := ice.NewPack().SetScene("recharge")
pack.Roam.Put("amount", 100)
ctxList := ice.SyncProcess(ctx, pack)

// Execute by ConfId (execute specific node directly)
pack := ice.NewPack().SetConfId(123)
ctxList := ice.SyncProcess(ctx, pack)

Async Execution

ctx := context.Background()
pack := ice.NewPack().SetIceId(1)
channels := ice.AsyncProcess(ctx, pack)

// Wait for async results
for _, ch := range channels {
    iceCtx := <-ch
    fmt.Println("Async result:", iceCtx.Pack.Roam.Data())
}

Convenience Methods

ctx := context.Background()

// Get single Roam result
roam := ice.ProcessSingleRoam(ctx, pack)

// Get multiple Roam results
roams := ice.ProcessRoam(ctx, pack)

// Get single Context
iceCtx := ice.ProcessSingleCtx(ctx, pack)

// Get multiple Contexts
ctxList := ice.ProcessCtx(ctx, pack)

Context Passing Example (HTTP Scenario)

func handleRequest(w http.ResponseWriter, r *http.Request) {
    // Get context from HTTP request and add traceId
    ctx := ice.WithTraceId(r.Context(), r.Header.Get("X-Trace-Id"))
    
    pack := ice.NewPack().SetScene("api")
    pack.Roam.Put("userId", getUserId(r))
    
    // Context propagates to all leaf nodes, logs auto-include traceId
    ctxList := ice.SyncProcess(ctx, pack)
    
    // ...
}

Roam Data Operations

Roam is a thread-safe data container with multi-level key access:

roam := ice.NewRoam()

// Basic operations
roam.Put("name", "Alice")
roam.Put("age", 25)

// Get values
name := roam.GetString("name")           // "Alice"
age := roam.GetInt("age", 0)             // 25
score := roam.GetFloat64("score", 0.0)   // default value

// Multi-level key operations
roam.PutMulti("user.profile.level", 5)
level := roam.GetMulti("user.profile.level")  // 5

// Reference syntax (used in config)
// "@key" references another key's value
value := roam.GetUnion("@user.profile.level")  // 5

// Print JSON format
fmt.Println(roam.String())  // {"name":"Alice","age":25,...}

Client Configuration

Basic Configuration

// Simplest way (recommended)
client, err := ice.NewClient(1, "./ice-data")

Full Configuration

import "time"

client, err := ice.NewClientWithOptions(
    1,                      // app ID
    "./ice-data",           // storage path
    -1,                     // parallelism (-1 for default)
    5*time.Second,          // poll interval
    10*time.Second,         // heartbeat interval
)

Lifecycle

// Start
client.Start()

// Wait for start completion (optional)
client.WaitStarted()

// Check status
if client.IsStarted() {
    // ...
}

// Get loaded version
version := client.GetLoadedVersion()

// Destroy
client.Destroy()

Logging & Trace Configuration

Using Default Logger (slog)

import "log/slog"

// Set log level
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelDebug,
})))

TraceId Support

SDK has built-in traceId/spanId support, auto-extracted from context and added to logs:

// Add traceId to context
ctx := ice.WithTraceId(context.Background(), "trace-123")
ctx = ice.WithSpanId(ctx, "span-456")

// Pass ctx when executing, all logs auto-include traceId
ice.SyncProcess(ctx, pack)
// Log output: time=xxx level=INFO msg="handle in" traceId=trace-123 spanId=span-456 ...

Custom Logger

import (
    "context"
    icelog "github.com/zjn-zjn/ice/sdks/go/log"
)

// Implement Logger interface (Note: first parameter is context.Context)
type MyLogger struct{}

func (l *MyLogger) Debug(ctx context.Context, msg string, args ...any) { /* ... */ }
func (l *MyLogger) Info(ctx context.Context, msg string, args ...any)  { /* ... */ }
func (l *MyLogger) Warn(ctx context.Context, msg string, args ...any)  { /* ... */ }
func (l *MyLogger) Error(ctx context.Context, msg string, args ...any) { /* ... */ }

// Set custom logger
ice.SetLogger(&MyLogger{})

Field Description & Ignore

Field Description (ice tag)

Use ice struct tag to add descriptions for friendly UI display:

type MyNode struct {
    // iceField - Show name and description in UI
    Score float64 `json:"score" ice:"name:Score Threshold,desc:Threshold for comparison"`
    Key   string  `json:"key" ice:"name:Key,desc:Key to get value from roam"`
    
    // hideField - No ice tag, configurable but hidden
    Internal string `json:"internal"`
}

Field Ignore

Fields that should not be configurable can be ignored:

type MyNode struct {
    // Method 1: json:"-" - Not serialized, naturally not configurable
    Service *http.Client `json:"-"`
    
    // Method 2: ice:"-" - Serialized but not in config list
    Cache map[string]any `json:"cache" ice:"-"`
    
    // Method 3: Unexported fields are automatically ignored
    internal string
}

Alias

Support multi-language compatible configuration:

ice.RegisterLeaf("com.example.ScoreFlow",
    &ice.LeafMeta{
        Name:  "Score Check",
        Alias: []string{"score_flow", "ScoreFlow"},
    },
    func() any { return &ScoreFlow{} })

Complete Example

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    ice "github.com/zjn-zjn/ice/sdks/go"
    icecontext "github.com/zjn-zjn/ice/sdks/go/context"
)

// Point grant node
type PointResult struct {
    Key   string  `json:"key"`
    Value float64 `json:"value"`
}

func (p *PointResult) DoRoamResult(ctx context.Context, roam *icecontext.Roam) bool {
    uid := roam.GetInt(p.Key, 0)
    if uid <= 0 || p.Value <= 0 {
        return false
    }
    fmt.Printf("Grant points: uid=%d, points=%.2f\n", uid, p.Value)
    roam.Put("SEND_POINT", true)
    return true
}

// Score check node
type ScoreFlow struct {
    Threshold float64 `json:"threshold"`
    Key       string  `json:"key"`
}

func (s *ScoreFlow) DoRoamFlow(ctx context.Context, roam *icecontext.Roam) bool {
    score := roam.GetFloat64(s.Key, 0)
    return score >= s.Threshold
}

func init() {
    ice.RegisterLeaf("com.example.PointResult", 
        &ice.LeafMeta{Name: "Point Grant", Desc: "Grant points reward"},
        func() any { return &PointResult{} })
    
    ice.RegisterLeaf("com.example.ScoreFlow",
        &ice.LeafMeta{Name: "Score Check", Desc: "Check if score meets threshold"},
        func() any { return &ScoreFlow{} })
}

func main() {
    // Initialize executor
    ice.InitExecutor(10)

    // Create and start client
    client, err := ice.NewClientWithOptions(
        1, "./ice-data", -1,
        5*time.Second, 10*time.Second,
    )
    if err != nil {
        log.Fatal(err)
    }

    if err := client.Start(); err != nil {
        log.Fatal(err)
    }
    defer client.Destroy()

    // Create context with traceId
    ctx := ice.WithTraceId(context.Background(), "trace-001")

    // Execute rules
    pack := ice.NewPack().SetScene("reward")
    pack.Roam.Put("uid", 12345)
    pack.Roam.Put("score", 85.0)

    ctxList := ice.SyncProcess(ctx, pack)

    for _, iceCtx := range ctxList {
        fmt.Println("Result:", iceCtx.Pack.Roam.Data())
    }
}

Next Steps

  • đź“– Node Details - Learn more about node types
  • 🏗️ Architecture - Understand Ice architecture
  • âť“ FAQ - Resolve integration issues