Go | First Response Pattern
Table of Contents
// Avoid timeout - Replicate servers and use first response
type Result string
type Search func(query string) Result
func fakeSearch(kind string) Search {
return func(query string) Result {
time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
return Result(fmt.Sprintf("%s result for %q", kind, query))
}
}
func First(query string, replicas ...Search) Result {
c := make(chan Result)
searchReplica := func(i int) { c <- replicas[i](query) }
for i := range replicas {
go searchReplica(i)
}
return <-c
}
func main() {
start := time.Now()
result := First("golang",
fakeSearch("replica1"),
fakeSearch("replica2"),
fakeSearch("replica3"))
elapsed := time.Since(start)
fmt.Println(result)
fmt.Printf("%.3fms\n", float64(elapsed.Nanoseconds())/1e6)
}
这是 Go 并发经典实践之一:“用副本竞速的方式避免超时,返回最早的可用结果”——常见于高可用分布式系统的请求快速返回(比如多个搜索副本、微服务副本集请谁先响应)。
#
主要思想
- 业务背景:你有好几个“备用服务器/副本/搜索实现”,哪个响应最快用哪个,“慢的都不用等”,避免整个请求因单点慢或偶发延迟变慢。
- 这是现实世界中 Google、Amazon 等公司搜索引擎“边缘副本”、“异地容灾高可用”大量使用的技术方案。
#
现实应用场景
- 微服务多活:多个服务副本响应同一请求,优先拿到第一个并快速返回,极大降低99.99延迟。
- CDN/边缘计算:拉取数据时向多节点并发请求,客户端只拿第一个响应的内容。
- 多家供应商比价/辅助搜索聚合:哪个接口快就显示谁的内容。
#
使用context控制goroutine leak
这个简单写法确实会导致 goroutine 泄露(goroutine leak)的问题。
- 你发起了多个副本 goroutine,每个都会做完自己的“搜索”然后把结果写入同一个 channel
c。 - 但
First只从 channel 取第一个结果:return <-c - 其它 goroutine 后续写入 channel 时,如果没人读,它们会永远阻塞在
c <- ...这一行上——此时这些 goroutine 无法退出,即内存泄露。
func First(query string, replicas ...Search) Result {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
c := make(chan Result)
searchReplica := func(i int) {
select {
case c <- replicas[i](query):
case <-ctx.Done():
return
}
}
for i := range replicas {
go searchReplica(i)
}
result := <-c
cancel() // 让其他 goroutine 自动退出
return result
}
主流程一旦拿到第一个结果,cancel() 会通知其他所有 goroutine 退出。
复杂情况下可配合 context、WaitGroup 做到无资源泄露。