我很喜欢的一道 Golang 编程题 | 简单,但很有效

要求:

  1. 只能编辑 foo 函数
  2. foo 函数必须调用 slow 函数
  3. foo 函数在 ctx 超时后必须立刻返回

【加分项】如果 slow 结束的比 ctx 快,也立刻返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"context"
"fmt"
"math/rand"
"time"
)

func main() {
rand.Seed(time.Now().UnixNano())

ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()

foo(ctx)
}

func foo(ctx context.Context) {
// TODO
slow()
}

func slow() {
n := rand.Intn(3)
fmt.Printf("sleep %d\n", n)
time.Sleep(time.Duration(n) * time.Second)
}

分析

我们来 one by onecheck 一遍题目要求:

  1. 只能编辑 foo 函数
  2. foo 函数必须调用 slow 函数
  3. foo 函数在 ctx 超时后必须 立刻 返回

很明显,这里涉及到 go 语言里面利用 context 控制上下文问题,超时返回,那必然要用到 for-select 编程模式了。

但由于 slow 执行时会 sleep ,阻塞当前协程,因此 slow 不能在 for-select 函数体内,必须在一个单独的协程中,结束后通过 channel (如 sigCh)通知其它协程。

【加分项】如果 slow 结束的比 ctx 快,也 立刻 返回。说明在 for-select 函数体内,除了要监听 context 的信号,也要监听 sigCh 是否有数据返回。

综合考虑这几点后,实现起来就不难了。

参考实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

import (
"context"
"log"
"math/rand"
"time"
)

func main() {
rand.Seed(time.Now().UnixNano())
ctx := context.Background()

log.Println("start...")
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()

foo(ctx)
}

func foo(ctx context.Context) {
sigCh := make(chan struct{})
fn := func() {
slow()
close(sigCh)
}

go func() {
for {
select {
case <-ctx.Done():
log.Println("foo:parent ctx done, reason:", ctx.Err())
return
case <-sigCh:
log.Println("slow finish before ctx timeout")
return
default:
}
}
}()

fn()
}

func slow() {
n := rand.Intn(3)
log.Printf("sleep %d\n", n)
time.Sleep(time.Duration(n) * time.Second)
}

扩展

有个细节需要注意:

close(sigCh) 处能不能改成 sigCh <- struct{}{} ?为什么?

请读者自己试验下,并分析输出结果^_^

彦祖老师 wechat