
go 的并发模型主张以同步、阻塞式 api 为主,由使用者按需通过 goroutine 封装实现异步;标准做法是提供简洁的同步方法,而非内置 channel 返回的异步变体。
go 的并发模型主张以同步、阻塞式 api 为主,由使用者按需通过 goroutine 封装实现异步;标准做法是提供简洁的同步方法,而非内置 channel 返回的异步变体。
在 Go 生态中,“同步优先、异步可选”是被广泛遵循的设计哲学。你的 rpcClient.Call() 方法采用阻塞式 HTTP 调用(如 http.Get())完全符合 Go 的惯用实践——它语义清晰、易于测试、便于错误传播,且天然兼容 Go 的轻量级 goroutine 调度机制。
例如,用户可在需要并发调用时,以极简方式启动 goroutine:
// 同步调用(推荐默认用法)
res, err := rpcClient.Call("myMethod", param1, param2)
// 并发调用(用户自主控制)
ch := make(chan struct{ Res interface{}; Err error }, 1)
go func() {
res, err := rpcClient.Call("myMethod", param1, param2)
ch <- struct{ Res interface{}; Err error }{res, err}
}()
// 后续 select 或接收
select {
case r := <-ch:
if r.Err != nil {
log.Printf("RPC failed: %v", r.Err)
} else {
handle(r.Res)
}
}你无需为每个方法都提供 CallAsync() 变体。原因有三:
- 职责分离:异步调度逻辑(如超时、重试、扇出/扇入、上下文取消)应由调用方根据具体场景决定,库不宜越俎代庖;
-
API 简洁性:双接口(
Call/CallAsync)会显著增加维护成本与文档复杂度,且易引发一致性问题(如错误处理逻辑不统一); -
Go 运行时优势:goroutine 开销极小(初始栈仅 2KB),
go rpcClient.Call(...)的封装成本远低于大多数语言中的线程开销,所谓“ugly wrapper”实为 Go 风格的自然表达。
若确有高频异步需求,可提供可选的工具函数(非核心 API),例如:
type AsyncResponse struct {
Result interface{}
Error error
}
// CallAsync 是便利函数,非必需 —— 它只是对 go + channel 模式的语法糖
func (c *RPCClient) CallAsync(method string, params ...interface{}) <-chan AsyncResponse {
ch := make(chan AsyncResponse, 1)
go func() {
res, err := c.Call(method, params...)
ch <- AsyncResponse{Result: res, Error: err}
close(ch) // 防止泄漏,确保 channel 可安全 range
}()
return ch
}⚠️ 注意事项:
- 若暴露 channel,务必
close(ch)或使用带缓冲 channel(如上例),避免接收方永久阻塞; - 始终支持
context.Context参数(如Call(ctx, method, params...)),使调用方可统一控制超时与取消——这才是比CallAsync更重要、更 Go-idiomatic 的扩展点; - 避免在库中引入
sync.WaitGroup、time.Timer等调度细节,保持接口正交与可组合。
总结:坚持提供清晰、阻塞、Context-aware 的同步 API;把并发权交给使用者——这既是 Go 的设计信条,也是构建健壮、可演进库的基石。

















