Files
gobridge/example/main.go
T
what ee5e5b96af feat: 添加 WithStreamErrors 查询流式调用执行过程中的异常
流式输出/双向流的 handler(Python 生成器)如果执行过程中抛异常,
Invoke[chan T] 本身的 err 只描述"调用有没有发起成功",跟这个异常
无关(永远是 nil),channel 只会静默提前关闭,调用方原本完全无法
感知。新增 WithStreamErrors(ctx) 返回一个包过的 ctx 和一个查询函数
streamErr,opt-in 之后可以查到具体错误。

错误记录挂在 WithStreamErrors 返回的 ctx 的对象图里(context.WithValue),
不是全局表——调用方不再引用 ctx/channel 时会被 GC 自然回收,不需要
任何显式清理逻辑,也不依赖 ctx.Done(),即使用 context.Background()
也能正常释放;ctx 之后被别的 context.With*(包括 StickyCtx)再包一层
也不影响查询。

同时补充完整的自动化测试覆盖 example/main.go 里演示过的所有功能:
四种调用模式 × int/struct/slice/[]byte 的组合(client_test.go)、
WithHandlers/call_go 全双工(handlers_test.go)、NewSession 隔离性
与 StickyCtx 路由(session_test.go),之前这些只能靠人肉跑 go run
看输出,现在都有真实断言。
2026-07-23 16:24:05 +08:00

454 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"context"
"fmt"
"log"
"path/filepath"
"runtime"
"sync"
"time"
"git.fsdpf.net/go/gobridge"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Score float64 `json:"score"`
Level string `json:"level,omitempty"`
}
func main() {
_, file, _, _ := runtime.Caller(0)
script := filepath.Join(filepath.Dir(file), "worker.py")
pool, err := gobridge.NewPool(script,
gobridge.WithWorkers(2),
gobridge.WithMaxConns(4),
)
if err != nil {
log.Fatal(err)
}
defer pool.Close()
ctx := context.Background()
demoPool(ctx, pool)
demoServer(ctx, script)
demoSession(ctx, script)
demoTimeout(script)
demoBlocking(script)
}
func demoPool(ctx context.Context, pool gobridge.Pool) {
// ── 普通调用 ──────────────────────────────────────────────────────────
sum, err := gobridge.Invoke[int](ctx, pool, "add", 3, 4)
if err != nil {
log.Fatal(err)
}
fmt.Println("add(3, 4) =", sum) // 7
// ── 流式输出:Python yield → Go channel ──────────────────────────────
ch, err := gobridge.Invoke[chan int](ctx, pool, "range_gen", 1, 6)
if err != nil {
log.Fatal(err)
}
fmt.Print("range_gen(1, 6) =")
for v := range ch {
fmt.Print(" ", v)
}
fmt.Println() // 1 2 3 4 5
// ── 流式输入:Go channel → Python Iterator ───────────────────────────
inputCh := make(chan int, 10)
go func() {
for i := 1; i <= 5; i++ {
inputCh <- i
}
close(inputCh)
}()
total, err := gobridge.Invoke[int](ctx, pool, "sum_stream", inputCh)
if err != nil {
log.Fatal(err)
}
fmt.Println("sum_stream(1..5) =", total) // 15
// ── 双向流:Go channel 输入 + Go channel 输出 ────────────────────────
inputCh2 := make(chan int, 10)
go func() {
for i := 1; i <= 5; i++ {
inputCh2 <- i
}
close(inputCh2)
}()
outCh, err := gobridge.Invoke[chan int](ctx, pool, "double_stream", inputCh2)
if err != nil {
log.Fatal(err)
}
fmt.Print("double_stream(1..5) =")
for v := range outCh {
fmt.Print(" ", v)
}
fmt.Println() // 1 4 9 16 25
// ── struct 普通调用 ───────────────────────────────────────────────────
user, err := gobridge.Invoke[User](ctx, pool, "get_user", 42)
if err != nil {
log.Fatal(err)
}
fmt.Printf("get_user(42) = %+v\n", user)
// ── slice 输入,返回标量 ───────────────────────────────────────────────
users := []User{
{ID: 1, Name: "alice", Score: 5.0},
{ID: 2, Name: "bob", Score: 8.0},
{ID: 3, Name: "carol", Score: 12.0},
}
scoreSum, err := gobridge.Invoke[float64](ctx, pool, "total_score", users)
if err != nil {
log.Fatal(err)
}
fmt.Printf("total_score([alice,bob,carol]) = %.1f\n", scoreSum)
// ── slice 输入输出 ─────────────────────────────────────────────────────
enriched, err := gobridge.Invoke[[]User](ctx, pool, "enrich_users", users)
if err != nil {
log.Fatal(err)
}
fmt.Println("enrich_users:")
for _, u := range enriched {
fmt.Printf(" %+v\n", u)
}
// ── 流式输出 structPython yield User → Go chan User ─────────────────
userCh, err := gobridge.Invoke[chan User](ctx, pool, "gen_users", 3)
if err != nil {
log.Fatal(err)
}
fmt.Print("gen_users(3) =")
for u := range userCh {
fmt.Printf(" {%d %s %.0f}", u.ID, u.Name, u.Score)
}
fmt.Println()
// ── 双向流 structGo chan User 输入 → Python 处理 → Go chan User 输出 ─
inCh := make(chan User, 5)
go func() {
for _, u := range users {
inCh <- u
}
close(inCh)
}()
procCh, err := gobridge.Invoke[chan User](ctx, pool, "process_users", inCh)
if err != nil {
log.Fatal(err)
}
fmt.Println("process_users:")
for u := range procCh {
fmt.Printf(" %+v\n", u)
}
// ── []byte 输入输出 ───────────────────────────────────────────────────────
rev, err := gobridge.Invoke[[]byte](ctx, pool, "bytes_reverse", []byte("hello"))
if err != nil {
log.Fatal(err)
}
fmt.Printf("bytes_reverse(hello) = %s\n", rev) // olleh
cat, err := gobridge.Invoke[[]byte](ctx, pool, "bytes_concat", []byte("foo"), []byte("bar"))
if err != nil {
log.Fatal(err)
}
fmt.Printf("bytes_concat(foo, bar) = %s\n", cat) // foobar
// ── []byte 流式输出:Python yield bytes → Go chan []byte ──────────────────
bCh, err := gobridge.Invoke[chan []byte](ctx, pool, "bytes_chunks", []byte("abcdefgh"), 3)
if err != nil {
log.Fatal(err)
}
fmt.Print("bytes_chunks(abcdefgh, 3) =")
for chunk := range bCh {
fmt.Printf(" %s", chunk)
}
fmt.Println() // abc def gh
}
// goService 实现 Handler 接口,公开方法自动暴露给 Python 通过 call_go() 调用
type goService struct {
pool gobridge.Pool // 用于 EnrichName 内部再调 Python
}
func (s *goService) Multiply(ctx context.Context, a, b int) (int, error) {
return a * b, nil
}
func (s *goService) Log(msg string) {
fmt.Println("[Go Log]", msg)
}
// EnrichName 内部通过 Invoke 调用 Python 的 to_upper,演示 Go→Python→Go→Python 四层链路
func (s *goService) EnrichName(ctx context.Context, name string) (string, error) {
upper, err := gobridge.Invoke[string](ctx, s.pool, "to_upper", name)
if err != nil {
return "", err
}
return "Hello, " + upper + "!", nil
}
func (s *goService) MakeUser(ctx context.Context, uid int) (User, error) {
return User{ID: uid, Name: fmt.Sprintf("user_%d", uid), Score: float64(uid) * 1.5}, nil
}
func demoSession(ctx context.Context, script string) {
fmt.Println("\n── Session 亲和示例(workers=2)─────────────────────────────────")
pool, err := gobridge.NewPool(script, gobridge.WithWorkers(2))
if err != nil {
log.Fatal(err)
}
defer pool.Close()
// ── NewSession:三个 sessionC 与 A 落在同一 worker,验证互不干扰 ───────
sessA := gobridge.NewSession(pool) // worker 1
sessB := gobridge.NewSession(pool) // worker 0
sessC := gobridge.NewSession(pool) // worker 1(与 A 同进程,不同 session_id
msgA, err := gobridge.Invoke[string](ctx, sessA, "session_init", "A", 100)
if err != nil {
log.Fatal(err)
}
fmt.Println("A init:", msgA)
msgB, err := gobridge.Invoke[string](ctx, sessB, "session_init", "B", 200)
if err != nil {
log.Fatal(err)
}
fmt.Println("B init:", msgB)
msgC, err := gobridge.Invoke[string](ctx, sessC, "session_init", "C", 300)
if err != nil {
log.Fatal(err)
}
fmt.Println("C init:", msgC) // 应与 A 在同一 worker
// 对 A/B/C 各自做 step,验证三者状态完全独立
v, _ := gobridge.Invoke[int](ctx, sessA, "session_step", "A", 10)
fmt.Println("A step(+10) =", v) // 110
v, _ = gobridge.Invoke[int](ctx, sessA, "session_step", "A", 5)
fmt.Println("A step(+5) =", v) // 115
v, _ = gobridge.Invoke[int](ctx, sessB, "session_step", "B", 50)
fmt.Println("B step(+50) =", v) // 250
v, _ = gobridge.Invoke[int](ctx, sessC, "session_step", "C", 99)
fmt.Println("C step(+99) =", v) // 399(与 A 同 worker 但不受影响)
rA, _ := gobridge.Invoke[map[string]any](ctx, sessA, "session_result", "A")
rB, _ := gobridge.Invoke[map[string]any](ctx, sessB, "session_result", "B")
rC, _ := gobridge.Invoke[map[string]any](ctx, sessC, "session_result", "C")
fmt.Printf("A result = %v\n", rA) // map[steps:[10 5] value:115]
fmt.Printf("B result = %v\n", rB) // map[steps:[50] value:250]
fmt.Printf("C result = %v\n", rC) // map[steps:[99] value:399]
// ── 全局变量测试:sessA 和 sessC 同 worker,共享进程级 counter ──────────
fmt.Println()
r, _ := gobridge.Invoke[string](ctx, sessA, "global_increment", 10)
fmt.Println("sessA +10:", r) // worker 1 counter = 10
r, _ = gobridge.Invoke[string](ctx, sessC, "global_increment", 5)
fmt.Println("sessC +5 :", r) // worker 1 counter = 15(与 sessA 共享)
r, _ = gobridge.Invoke[string](ctx, sessB, "global_increment", 99)
fmt.Println("sessB +99:", r) // worker 0 counter = 99(独立进程,从 0 开始)
r, _ = gobridge.Invoke[string](ctx, sessA, "global_get")
fmt.Println("sessA get:", r) // worker 1 counter = 15(不受 sessB 影响)
// ── StickyCtx:相同 key 跨调用始终路由同一 worker ────────────────────
fmt.Println()
for i := range 4 {
affinityCtx := gobridge.StickyCtx(ctx, "sticky-key")
msg, _ := gobridge.Invoke[string](affinityCtx, pool, "session_init",
fmt.Sprintf("aff-%d", i), i)
fmt.Printf("StickyCtx(sticky-key) #%d → %s\n", i, msg)
}
// ── 对照组:不带亲和,轮询分配给两个 worker ─────────────────────────────
fmt.Println()
for i := range 4 {
msg, _ := gobridge.Invoke[string](ctx, pool, "session_init",
fmt.Sprintf("rr-%d", i), i)
fmt.Printf("round-robin #%d → %s\n", i, msg)
}
}
func demoTimeout(script string) {
fmt.Println("\n── 默认超时示例(WithDefaultTimeout)─────────────────────────────")
// workers=1, maxConns=1:整个池子只有一个连接槽位,便于演示"占满后阻塞/超时"
pool, err := gobridge.NewPool(script,
gobridge.WithWorkers(1),
gobridge.WithMaxConns(1),
gobridge.WithDefaultTimeout(500*time.Millisecond),
)
if err != nil {
log.Fatal(err)
}
defer pool.Close()
// ── 示例1ctx 未设置 deadline,池的默认超时自动生效 ─────────────────────
// sleep_seconds(2) 耗时 2s,远超 500ms 默认超时,Invoke 会在 500ms 后返回
// context.DeadlineExceeded,而不是永久阻塞。
_, err = gobridge.Invoke[string](context.Background(), pool, "sleep_seconds", 2.0)
fmt.Println("sleep_seconds(2.0)(无 ctx deadline,池默认超时 500ms)→ err =", err)
// ── 示例2:调用方显式传入的 deadline 优先级更高,不会被默认超时覆盖 ───────
ctx2, cancel2 := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel2()
msg, err := gobridge.Invoke[string](ctx2, pool, "sleep_seconds", 1.0)
fmt.Println("sleep_seconds(1.0)(显式 3s ctx)→", msg, err)
// ── 示例3:流式输出模式下,超时只会静默关闭 channel,不返回 error
// 需要调用方自行在 range 结束后检查 ctx.Err() 才能区分"正常结束"还是"被打断"。
ctx3, cancel3 := context.WithTimeout(context.Background(), 350*time.Millisecond)
defer cancel3()
ch, err := gobridge.Invoke[chan int](ctx3, pool, "slow_range_gen", 1, 10, 100)
if err != nil {
log.Fatal(err)
}
fmt.Print("slow_range_gen(1, 10, 100ms)350ms ctx 超时)=")
for v := range ch {
fmt.Print(" ", v)
}
fmt.Println()
if ctx3.Err() != nil {
fmt.Println("channel 提前关闭,ctx.Err() =", ctx3.Err())
}
// ── 示例4WithDefaultTimeout 对流式输出完全不生效——即使不设 ctx deadline
// 池子的默认超时(这里是 500ms)也不会把一个耗时 900ms 的正常流式输出腰斩。
// 想要超时保护,流式调用必须自己显式传 deadline(见示例3)。
ch2, err := gobridge.Invoke[chan int](context.Background(), pool, "slow_range_gen", 1, 10, 100)
if err != nil {
log.Fatal(err)
}
fmt.Print("slow_range_gen(1, 10, 100ms)(无 ctx deadline,池默认超时 500ms 但流式输出不受影响)=")
for v := range ch2 {
fmt.Print(" ", v)
}
fmt.Println() // 应该完整输出 1~9,不会被 500ms 默认超时打断
// ── 示例5WithStreamErrors——流式 handler 执行过程中抛异常,channel 会静默
// 提前关闭,Invoke 本身的 err 只描述"调用有没有发起成功",跟这个异常无关
// (永远是 nil)。想知道流是不是因为 Python 侧异常提前结束,需要先用
// WithStreamErrors 包一层 ctxopt-in),拿到的 streamErr 函数不用再传 ctx。
//
// 错误记录挂在 WithStreamErrors 返回的这个 ctx 的对象图里,调用方不再引用
// ctx5/ch3 时会被 GC 自然回收,不需要任何显式清理,即使用 context.Background()
// 也一样能正常释放(不依赖 ctx.Done())。
ctx5, streamErr := gobridge.WithStreamErrors(context.Background())
ch3, err := gobridge.Invoke[chan int](ctx5, pool, "stream_then_raise", 3)
if err != nil {
log.Fatal(err) // 这里的 err 只可能是"发起调用失败",不会是 stream_then_raise 里的异常
}
fmt.Print("stream_then_raise(3)(执行过程中会抛异常)=")
for v := range ch3 {
fmt.Print(" ", v)
}
fmt.Println()
if err := streamErr(ch3); err != nil {
fmt.Println("streamErr 查到执行过程中的异常:", err)
}
}
func demoBlocking(script string) {
fmt.Println("\n── 连接池占满后阻塞示例(不设超时)───────────────────────────")
// workers=2, maxConns=2:总容量只有 2*2=4 个连接槽位,便于演示"占满后新调用会怎样"
pool, err := gobridge.NewPool(script,
gobridge.WithWorkers(2),
gobridge.WithMaxConns(2),
)
if err != nil {
log.Fatal(err)
}
defer pool.Close()
const capacity = 4 // 2 workers * 2 maxConns
const taskSeconds = 2.0 // 每个占位任务耗时 2s
// 并发发起 4 个耗时调用,占满全部连接槽位
var wg sync.WaitGroup
wg.Add(capacity)
for i := 0; i < capacity; i++ {
go func(n int) {
defer wg.Done()
if _, err := gobridge.Invoke[string](context.Background(), pool, "sleep_seconds", taskSeconds); err != nil {
log.Printf("occupy task %d failed: %v", n, err)
}
}(i)
}
// 留出时间让 4 个占位任务都真正进入 Python 侧 time.sleep(),占满全部槽位
time.Sleep(200 * time.Millisecond)
fmt.Printf("池子已被 %d 个任务占满(各耗时 %.0fs),此时发起第 5 个调用(ctx 不设超时)……\n", capacity, taskSeconds)
// 池子被占满时,新调用既不会立刻失败,也不会被跳过,而是排队阻塞等待连接释放。
// 这里故意不设超时,用等待耗时证明它确实在阻塞排队,而不是瞬间返回。
callStart := time.Now()
sum, err := gobridge.Invoke[int](context.Background(), pool, "add", 1, 2)
fmt.Printf("第 5 个调用等待 %v 后返回:add(1,2)=%d, err=%v\n", time.Since(callStart).Round(time.Millisecond), sum, err)
// 预期:等待时长接近"占满起 ~taskSeconds 秒后有槽位释放"的剩余时间,
// 说明调用被真正阻塞排队,直到某个占位任务结束、连接被释放后才继续执行。
wg.Wait()
}
func demoServer(ctx context.Context, script string) {
fmt.Println("\n── Server 全双工示例 ─────────────────────────────────────────────")
svc := &goService{}
serv, err := gobridge.NewPool(script,
gobridge.WithWorkers(1),
gobridge.WithHandlers(svc),
)
if err != nil {
log.Fatal(err)
}
defer serv.Close()
svc.pool = serv // 注入 pool 供 EnrichName 内部调用
// ── 示例1Python 调用 Go Multiply ───────────────────────────────────────
result, err := gobridge.Invoke[int](ctx, serv, "compute_with_go_mul", 6, 7)
if err != nil {
log.Fatal(err)
}
fmt.Println("compute_with_go_mul(6, 7) =", result) // 42
// ── 示例2:流式输出 + Go Log 回调 ────────────────────────────────────────
ch, err := gobridge.Invoke[chan int](ctx, serv, "squared_with_log", 4)
if err != nil {
log.Fatal(err)
}
fmt.Print("squared_with_log(4) =")
for v := range ch {
fmt.Print(" ", v)
}
fmt.Println() // 1 4 9 16
// ── 示例3Go→Python→Go→Python 四层全双工链路 ───────────────────────────
// full_chain("world") → call_go[str]("EnrichName","world") → Invoke to_upper("world") → "WORLD"
// ← "Hello, WORLD!" ← "Hello, WORLD!"
greeting, err := gobridge.Invoke[string](ctx, serv, "full_chain", "world")
if err != nil {
log.Fatal(err)
}
fmt.Println("full_chain(world) =", greeting) // Hello, WORLD!
// ── 示例4call_go[User] 将 Go 返回的 dict 自动构造为 dataclass ──────────
enriched, err := gobridge.Invoke[User](ctx, serv, "get_user_via_go", 12)
if err != nil {
log.Fatal(err)
}
fmt.Printf("get_user_via_go(12) = %+v\n", enriched) // {ID:12 Name:user_12 Score:18 Level:gold}
}