feat: 添加 Python→Go 全双工回调支持(call_go)

- 新增 WithHandlers 选项,通过反射将 Go 结构体方法暴露给 Python
- 新增 callback/callback_result 消息类型,支持 Python 在处理中回调 Go
- client 侧新增 readResult,内联处理 callback,复用同一连接避免死锁
- Python 侧新增 call_go[T]() 泛型调用,支持 dataclass 自动构造
- 注入 GOBRIDGE_WORKER_ID/WORKER_COUNT 环境变量,支持多 worker 初始化分工
- 新增示例演示 Go→Python→Go→Python 四层全双工链路
- Python 包版本升至 0.1.1
This commit is contained in:
2026-04-14 13:06:50 +08:00
parent 07e9239ac5
commit b390effd8e
8 changed files with 490 additions and 54 deletions

161
pool.go
View File

@@ -2,9 +2,12 @@ package gobridge
import (
"context"
"encoding/json"
"fmt"
"io"
"net"
"reflect"
"sync"
"sync/atomic"
)
@@ -19,6 +22,7 @@ type poolConfig struct {
socketDir string
stdout io.Writer
stderr io.Writer
handler any
}
// Option 是 NewPool 的函数选项
@@ -41,7 +45,6 @@ func WithPythonExe(exe string) Option {
}
// WithScriptArgs 设置脚本路径之后的附加参数
// uv 模式示例WithScriptArgs("run") → 执行 uv run <script>
func WithScriptArgs(args ...string) Option {
return func(c *poolConfig) { c.scriptArgs = args }
}
@@ -52,7 +55,6 @@ func WithWorkDir(workDir string) Option {
}
// WithEnv 设置附加环境变量,格式为 "KEY=VALUE"
// 与当前进程环境合并,同名时以此处为准
func WithEnv(env ...string) Option {
return func(c *poolConfig) { c.env = env }
}
@@ -72,29 +74,55 @@ func WithStderr(w io.Writer) Option {
return func(c *poolConfig) { c.stderr = w }
}
// WithHandlers 注册 Go handler struct其所有公开方法自动暴露给 Python 通过 call_go() 调用。
//
// type MyService struct{}
// func (s *MyService) Multiply(ctx context.Context, a, b int) (int, error) { ... }
//
// pool, err := gobridge.NewPool("worker.py", gobridge.WithHandlers(&MyService{}))
// // Python: call_go("Multiply", 3, 4)
func WithHandlers(h any) Option {
return func(c *poolConfig) { c.handler = h }
}
// ── Pool 接口 ────────────────────────────────────────────────────────────────
// Pool 是 Python worker 进程池的接口
type Pool interface {
// Close 关闭所有 worker 进程和连接
Close()
acquire(ctx context.Context) (net.Conn, *worker, error)
nextReqID() uint64
callbackDispatch(ctx context.Context, msg Message) (any, string)
}
// pool 是 Pool 的具体实现
// ── goHandler ────────────────────────────────────────────────────────────────
type goHandler struct {
fn reflect.Value
hasCtx bool
inTypes []reflect.Type
outType reflect.Type
hasErr bool
}
// ── pool内部实现─────────────────────────────────────────────────────────
type pool struct {
workers []*worker
idx atomic.Uint64
reqID atomic.Uint64
workers []*worker
idx atomic.Uint64
reqID atomic.Uint64
mu sync.RWMutex
handlers map[string]goHandler
}
// NewPool 创建并启动进程池
// NewPool 创建并启动进程池
//
// pool, err := gobridge.NewPool("worker.py")
// pool, err := gobridge.NewPool("worker.py", gobridge.WithWorkers(4))
// pool, err := gobridge.NewPool("run",
// gobridge.WithPythonExe("uv"),
// gobridge.WithScriptArgs("worker.py"),
// gobridge.WithWorkDir("./worker"),
// pool, err := gobridge.NewPool("worker.py",
// gobridge.WithHandlers(&MyService{}),
// gobridge.WithWorkers(2),
// )
func NewPool(script string, opts ...Option) (Pool, error) {
cfg := &poolConfig{
@@ -107,7 +135,7 @@ func NewPool(script string, opts ...Option) (Pool, error) {
o(cfg)
}
if script == "" {
return nil, fmt.Errorf("NewPool: script must not be empty")
return nil, fmt.Errorf("gobridge: script must not be empty")
}
cfg.scriptArgs = append([]string{script}, cfg.scriptArgs...)
@@ -115,17 +143,72 @@ func NewPool(script string, opts ...Option) (Pool, error) {
for i := range workers {
w, err := newWorker(cfg, i)
if err != nil {
for j := 0; j < i; j++ {
for j := range i {
workers[j].stop()
}
return nil, fmt.Errorf("create worker %d: %w", i, err)
}
workers[i] = w
}
return &pool{workers: workers}, nil
p := &pool{workers: workers, handlers: make(map[string]goHandler)}
if cfg.handler != nil {
p.bindHandlers(cfg.handler)
}
return p, nil
}
func (p *pool) bindHandlers(h any) {
rv := reflect.ValueOf(h)
rt := rv.Type()
for i := range rt.NumMethod() {
m := rt.Method(i)
if !m.IsExported() {
continue
}
p.bindHandler(m.Name, rv.Method(i))
}
}
func (p *pool) bindHandler(name string, fn reflect.Value) {
errType := reflect.TypeFor[error]()
ctxType := reflect.TypeFor[context.Context]()
ft := fn.Type()
h := goHandler{fn: fn}
startIdx := 0
if ft.NumIn() > 0 && ft.In(0).Implements(ctxType) {
h.hasCtx = true
startIdx = 1
}
for i := startIdx; i < ft.NumIn(); i++ {
h.inTypes = append(h.inTypes, ft.In(i))
}
switch ft.NumOut() {
case 0:
case 1:
if ft.Out(0).Implements(errType) {
h.hasErr = true
} else {
h.outType = ft.Out(0)
}
case 2:
if !ft.Out(1).Implements(errType) {
panic(fmt.Sprintf("gobridge: handler %s: second return value must implement error", name))
}
h.outType = ft.Out(0)
h.hasErr = true
default:
panic(fmt.Sprintf("gobridge: handler %s: must return at most (value, error)", name))
}
p.handlers[name] = h
}
// acquire 以轮询方式从进程池取出一个可用连接
func (p *pool) acquire(ctx context.Context) (net.Conn, *worker, error) {
idx := p.idx.Add(1) % uint64(len(p.workers))
w := p.workers[idx]
@@ -137,7 +220,51 @@ func (p *pool) nextReqID() uint64 {
return p.reqID.Add(1)
}
// Close 关闭所有 worker 进程和连接
func (p *pool) callbackDispatch(ctx context.Context, msg Message) (any, string) {
p.mu.RLock()
h, ok := p.handlers[msg.Method]
p.mu.RUnlock()
if !ok {
return nil, fmt.Sprintf("unknown go handler: %s", msg.Method)
}
var rawArgs []json.RawMessage
if len(msg.Args) > 0 {
if err := json.Unmarshal(msg.Args, &rawArgs); err != nil {
return nil, fmt.Sprintf("unmarshal args: %v", err)
}
}
if len(rawArgs) != len(h.inTypes) {
return nil, fmt.Sprintf("arg count mismatch: want %d got %d", len(h.inTypes), len(rawArgs))
}
in := make([]reflect.Value, 0, len(h.inTypes)+1)
if h.hasCtx {
in = append(in, reflect.ValueOf(ctx))
}
for i, t := range h.inTypes {
v := reflect.New(t)
if err := json.Unmarshal(rawArgs[i], v.Interface()); err != nil {
return nil, fmt.Sprintf("unmarshal arg %d: %v", i, err)
}
in = append(in, v.Elem())
}
out := h.fn.Call(in)
if h.hasErr {
errVal := out[len(out)-1]
if !errVal.IsNil() {
return nil, errVal.Interface().(error).Error()
}
}
if h.outType == nil || len(out) == 0 {
return nil, ""
}
return out[0].Interface(), ""
}
func (p *pool) Close() {
for _, w := range p.workers {
w.stop()