面试官:如何按顺序循环打印ABC

328 阅读2分钟

小红薯一面,面试官出了一道代码题,按照顺序打印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)
}

虽然勉强实现了要求,但是可以明显看到,这段代码还是有非常多的问题:

  1. 使用time.Sleep()的方式来等待协程执行完毕的方式非常不优雅,但是当时为了快速实现选择了最笨的办法。
  2. 创建的channel没有关闭,在真实生产环境可能导致内存泄露,永久阻塞导致超时等问题。
  3. 往协程中写函数变量的方式不太优雅,重复的代码写了三遍,不符合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。