小红薯一面,面试官出了一道代码题,按照顺序打印A,B,C,要求循环打印500次。
简单思考了一下,基本思路就是利用Go的channel,一个协程写,另一个协程读,来实现顺序打印的要求。话不多说,直接上代码。
第一版
func Test01(t *testing.T) {
ch := make(chan func(), 3)
go func() {
for i := 0; i < 500; i++ {
ch <- func() {
fmt.Println("A")
}
ch <- func() {
fmt.Println("B")
}
ch <- func() {
fmt.Println("C")
}
}
}()
count := 0
go func() {
for value := range ch {
value()
count++
}
}()
time.Sleep(10 * time.Second)
fmt.Println(count)
}
虽然勉强实现了要求,但是可以明显看到,这段代码还是有非常多的问题:
- 使用time.Sleep()的方式来等待协程执行完毕的方式非常不优雅,但是当时为了快速实现选择了最笨的办法。
- 创建的channel没有关闭,在真实生产环境可能导致内存泄露,永久阻塞导致超时等问题。
- 往协程中写函数变量的方式不太优雅,重复的代码写了三遍,不符合DRY原则。\
本着精益求精的想法,面试结束之后对第一版代码进行了优化。这是第二版代码:
func Test02(t *testing.T) {
ch := make(chan string, 3)
// 计数器,验证是打印次数是否正确
count := 0
// 将需要打印的数据抽取出来,减少重复代码
printSlice := []string{"A", "B", "C"}
wg := &sync.WaitGroup{}
// 在起一个协程的情况下,必须要先读后写,如果先写后读会报错:“all goroutines are asleep - deadlock!”
go func() {
time.Sleep(1 * time.Second) // sleep 1s,防止执行太快读到零值
for value := range ch {
fmt.Println(value)
wg.Done()
count++
}
}()
for i := 0; i < 500; i++ {
for _, v := range printSlice {
// 下面的两行代码顺序不能颠倒,否则会报“negative WaitGroup counter”的错误
// 这是因为先写数据再Add(),可能出现刚写进channel的数据马上就被读到并且执行了wg.Done()函数,wg.Done()在wg.Add()前面执行,就会导致等待执行的协程数小于0,从而报错
wg.Add(1)
ch <- v
}
}
wg.Wait()
close(ch) // 记得关闭channel
fmt.Println(count)
}
对我而言,第二版的代码实现还算标准,不过如果将打印字符串的逻辑改成具有业务逻辑的处理函数,并且放在真实的生产环境中还是有一些问题。比如自行启动的协程需要处理panic,如何防止超时或者处理超时等等。
以上就是本篇文章的全部内容了,如果有更好的实现方式,欢迎评论留言,show me your code。