Go | First Response Pattern

Aug 7, 2025
#go
// 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 做到无资源泄露。


  • https://www.concurrency.rocks/
https://inasa.dev/posts/rss.xml