channel 关闭时机不对

  1. func main() {
  2. ch := make(chan int, 1000)
  3. go func() {
  4. for i := 0; i < 10; i++ {
  5. ch <- i
  6. }
  7. }()
  8. go func() {
  9. for {
  10. a, ok := <-ch
  11. if !ok {
  12. fmt.Println("close")
  13. return
  14. }
  15. fmt.Println("a: ", a)
  16. }
  17. }()
  18. close(ch)
  19. fmt.Println("ok")
  20. time.Sleep(time.Second * 100)
  21. }

在 golang 中 goroutine 的调度时间是不确定的,在题目中,第一个写 channelgoroutine 可能还未调用,或已调用但没有写完时直接 close 管道,可能导致写失败,既然出现 panic 错误。

channel 关闭时机不对2

  1. func main() {
  2. abc := make(chan int, 1000)
  3. for i := 0; i < 10; i++ {
  4. abc <- i
  5. }
  6. go func() {
  7. for {
  8. a := <-abc
  9. fmt.Println("a: ", a)
  10. }
  11. }()
  12. close(abc)
  13. fmt.Println("close")
  14. time.Sleep(time.Second * 100)
  15. }

协程可能还未启动,管道就关闭了。

runtime.GOMAXPROCS(1)

  1. func main() {
  2. runtime.GOMAXPROCS(1)
  3. wg := sync.WaitGroup{}
  4. wg.Add(20)
  5. for i := 0; i < 10; i++ {
  6. go func() {
  7. fmt.Println("i: ", i)
  8. wg.Done()
  9. }()
  10. }
  11. for i := 0; i < 10; i++ {
  12. go func(i int) {
  13. fmt.Println("i: ", i)
  14. wg.Done()
  15. }(i)
  16. }
  17. wg.Wait()
  18. }

最后输出结果是第一个循环结果不固定,主要看协程的调度时间,如果调度时间很慢,可能是全部是10。第二个循环会依次输出0-9

select case 随机执行

  1. func main() {
  2. runtime.GOMAXPROCS(1)
  3. int_chan := make(chan int, 1)
  4. string_chan := make(chan string, 1)
  5. int_chan <- 1
  6. string_chan <- "hello"
  7. select {
  8. case value := <-int_chan:
  9. fmt.Println(value)
  10. case value := <-string_chan:
  11. panic(value)
  12. }
  13. }

结果是随机执行。golang 在多个case 可读的时候会公平的选中一个执行。

defer在定义的时候输出

  1. func calc(index string, a, b int) int {
  2. ret := a + b
  3. fmt.Println(index, a, b, ret)
  4. return ret
  5. }
  6. func main() {
  7. a := 1
  8. b := 2
  9. defer calc("1", a, calc("10", a, b))
  10. a = 0
  11. defer calc("2", a, calc("20", a, b))
  12. b = 1
  13. }
  1. 10 1 2 3
  2. 20 0 2 2
  3. 2 0 2 2
  4. 1 1 3 4

defer 在定义的时候会计算好调用函数的参数,所以会优先输出1020 两个参数。然后根据定义的顺序倒序执行。

map不安全

  1. type UserAges struct {
  2. ages map[string]int
  3. sync.Mutex
  4. }
  5. func (ua *UserAges) Add(name string, age int) {
  6. ua.Lock()
  7. defer ua.Unlock()
  8. ua.ages[name] = age
  9. }
  10. func (ua *UserAges) Get(name string) int {
  11. if age, ok := ua.ages[name]; ok {
  12. return age
  13. }
  14. return -1
  15. }

在执行 Get方法时可能被panic。
虽然有使用sync.Mutex做写锁,但是map是并发读写不安全的。map属于引用类型,并发读写时多个协程见是通过指针访问同一个地址,即访问共享变量,此时同时读写资源存在竞争关系。会报错误信息:“fatal error: concurrent map read and map write”。
因此,在 Get 中也需要加锁,因为这里只是读,建议使用读写锁 sync.RWMutex

无缓冲channel阻塞

  1. func (set *threadSafeSet) Iter() <-chan interface{} {
  2. ch := make(chan interface{})
  3. go func() {
  4. set.RLock()
  5. for elem := range set.s {
  6. ch <- elem
  7. }
  8. close(ch)
  9. set.RUnlock()
  10. }()
  11. return ch
  12. }

默认情况下 make 初始化的 channel 是无缓冲的,也就是在迭代写时会阻塞。

在 golang 协程和channel配合使用

写代码实现两个 goroutine,其中一个产生随机数并写入到 go channel 中,另外一个从 channel 中读取数字并打印到标准输出。最终输出五个随机数。

这是一道很简单的golang基础题目,实现方法也有很多种,一般想答让面试官满意的答案还是有几点注意的地方。

  1. goroutine 在golang中式非阻塞的
  2. channel 无缓冲情况下,读写都是阻塞的,且可以用for循环来读取数据,当管道关闭后,for 退出。
  3. golang 中有专用的select case 语法从管道读取数据。
  1. func main() {
  2. out := make(chan int)
  3. wg := sync.WaitGroup{}
  4. wg.Add(2)
  5. go func() {
  6. defer wg.Done()
  7. for i := 0; i < 5; i++ {
  8. out <- rand.Intn(5)
  9. }
  10. close(out)
  11. }()
  12. go func() {
  13. defer wg.Done()
  14. for i := range out {
  15. fmt.Println(i)
  16. }
  17. }()
  18. wg.Wait()
  19. }

实现阻塞读且并发安全的map

GO里面MAP如何实现key不存在 get操作等待 直到key存在或者超时,保证并发安全,且需要实现以下接口:

  1. type sp interface {
  2. Out(key string, val interface{}) //存入key /val,如果该key读取的goroutine挂起,则唤醒。此方法不会阻塞,时刻都可以立即执行并返回
  3. Rd(key string, timeout time.Duration) interface{} //读取一个key,如果key不存在阻塞,等待key存在或者超时
  4. }

看到阻塞协程第一个想到的就是channel,题目中要求并发安全,那么必须用锁,还要实现多个goroutine读的时候如果值不存在则阻塞,直到写入值,那么每个键值需要有一个阻塞goroutinechannel

  1. type Map struct {
  2. c map[string]*entry
  3. rmx *sync.RWMutex
  4. }
  5. type entry struct {
  6. ch chan struct{}
  7. value interface{}
  8. isExist bool
  9. }
  10. func (m *Map) Out(key string, val interface{}) {
  11. m.rmx.Lock()
  12. defer m.rmx.Unlock()
  13. if e, ok := m.c[key]; ok {
  14. e.value = val
  15. e.isExist = true
  16. close(e.ch)
  17. } else {
  18. e = &entry{ch: make(chan struct{}), isExist: true,value:val}
  19. m.c[key] = e
  20. close(e.ch)
  21. }
  22. }
  23. func (m *Map) Rd(key string, timeout time.Duration) interface{} {
  24. m.rmx.Lock()
  25. if e, ok := m.c[key]; ok && e.isExist {
  26. m.rmx.Unlock()
  27. return e.value
  28. } else if !ok {
  29. e = &entry{ch: make(chan struct{}), isExist: false}
  30. m.c[key] = e
  31. m.rmx.Unlock()
  32. fmt.Println("协程阻塞 -> ", key)
  33. select {
  34. case <-e.ch:
  35. return e.value
  36. case <-time.After(timeout):
  37. fmt.Println("协程超时 -> ", key)
  38. return nil
  39. }
  40. } else {
  41. m.rmx.Unlock()
  42. fmt.Println("协程阻塞 -> ", key)
  43. select {
  44. case <-e.ch:
  45. return e.value
  46. case <-time.After(timeout):
  47. fmt.Println("协程超时 -> ", key)
  48. return nil
  49. }
  50. }
  51. }

高并发下的锁与map的读写

场景:在一个高并发的web服务器中,要限制IP的频繁访问。现模拟100个IP同时并发访问服务器,每个IP要重复访问1000次。
每个IP三分钟之内只能访问一次。修改以下代码完成该过程,要求能成功输出 success:100

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. type Ban struct {
  7. visitIPs map[string]time.Time
  8. }
  9. func NewBan() *Ban {
  10. return &Ban{visitIPs: make(map[string]time.Time)}
  11. }
  12. func (o *Ban) visit(ip string) bool {
  13. if _, ok := o.visitIPs[ip]; ok {
  14. return true
  15. }
  16. o.visitIPs[ip] = time.Now()
  17. return false
  18. }
  19. func main() {
  20. success := 0
  21. ban := NewBan()
  22. for i := 0; i < 1000; i++ {
  23. for j := 0; j < 100; j++ {
  24. go func() {
  25. ip := fmt.Sprintf("192.168.1.%d", j)
  26. if !ban.visit(ip) {
  27. success++
  28. }
  29. }()
  30. }
  31. }
  32. fmt.Println("success:", success)
  33. }

该问题主要考察了并发情况下map的读写问题,而给出的初始代码,又存在for循环中启动goroutine时变量使用问题以及goroutine执行滞后问题。
因此,首先要保证启动的goroutine得到的参数是正确的,然后保证map的并发读写,最后保证三分钟只能访问一次。
多CPU核心下修改int的值极端情况下会存在不同步情况,因此需要原子性的修改int值。
下面给出的实例代码,是启动了一个协程每分钟检查一下map中的过期ipfor启动协程时传参。

  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "sync"
  6. "sync/atomic"
  7. "time"
  8. )
  9. type Ban struct {
  10. visitIPs map[string]time.Time
  11. lock sync.Mutex
  12. }
  13. func NewBan(ctx context.Context) *Ban {
  14. o := &Ban{visitIPs: make(map[string]time.Time)}
  15. go func() {
  16. timer := time.NewTimer(time.Minute * 1)
  17. for {
  18. select {
  19. case <-timer.C:
  20. o.lock.Lock()
  21. for k, v := range o.visitIPs {
  22. if time.Now().Sub(v) >= time.Minute*1 {
  23. delete(o.visitIPs, k)
  24. }
  25. }
  26. o.lock.Unlock()
  27. timer.Reset(time.Minute * 1)
  28. case <-ctx.Done():
  29. return
  30. }
  31. }
  32. }()
  33. return o
  34. }
  35. func (o *Ban) visit(ip string) bool {
  36. o.lock.Lock()
  37. defer o.lock.Unlock()
  38. if _, ok := o.visitIPs[ip]; ok {
  39. return true
  40. }
  41. o.visitIPs[ip] = time.Now()
  42. return false
  43. }
  44. func main() {
  45. success := int64(0)
  46. ctx, cancel := context.WithCancel(context.Background())
  47. defer cancel()
  48. ban := NewBan(ctx)
  49. wait := &sync.WaitGroup{}
  50. wait.Add(1000 * 100)
  51. for i := 0; i < 1000; i++ {
  52. for j := 0; j < 100; j++ {
  53. go func(j int) {
  54. defer wait.Done()
  55. ip := fmt.Sprintf("192.168.1.%d", j)
  56. if !ban.visit(ip) {
  57. atomic.AddInt64(&success, 1)
  58. }
  59. }(j)
  60. }
  61. }
  62. wait.Wait()
  63. fmt.Println("success:", success)
  64. }

写出以下逻辑,要求每秒钟调用一次proc并保证程序不退出?

  1. package main
  2. func main() {
  3. go func() {
  4. // 1 在这里需要你写算法
  5. // 2 要求每秒钟调用一次proc函数
  6. // 3 要求程序不能退出
  7. }()
  8. select {}
  9. }
  10. func proc() {
  11. panic("ok")
  12. }

题目主要考察了两个知识点:

  1. 定时执行执行任务
  2. 捕获 panic 错误

题目中要求每秒钟执行一次,首先想到的就是 time.Ticker对象,该函数可每秒钟往chan中放一个Time,正好符合我们的要求。
golang 中捕获 panic 一般会用到 recover() 函数。

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. go func() {
  8. // 1 在这里需要你写算法
  9. // 2 要求每秒钟调用一次proc函数
  10. // 3 要求程序不能退出
  11. t := time.NewTicker(time.Second * 1)
  12. for {
  13. select {
  14. case <-t.C:
  15. go func() {
  16. defer func() {
  17. if err := recover(); err != nil {
  18. fmt.Println(err)
  19. }
  20. }()
  21. proc()
  22. }()
  23. }
  24. }
  25. }()
  26. select {}
  27. }
  28. func proc() {
  29. panic("ok")
  30. }

为 sync.WaitGroup 中Wait函数支持 WaitTimeout 功能.

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. "time"
  6. )
  7. func main() {
  8. wg := sync.WaitGroup{}
  9. c := make(chan struct{})
  10. for i := 0; i < 10; i++ {
  11. wg.Add(1)
  12. go func(num int, close <-chan struct{}) {
  13. defer wg.Done()
  14. <-close
  15. fmt.Println(num)
  16. }(i, c)
  17. }
  18. if WaitTimeout(&wg, time.Second*5) {
  19. close(c)
  20. fmt.Println("timeout exit")
  21. }
  22. time.Sleep(time.Second * 10)
  23. }
  24. func WaitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
  25. // 要求手写代码
  26. // 要求sync.WaitGroup支持timeout功能
  27. // 如果timeout到了超时时间返回true
  28. // 如果WaitGroup自然结束返回false
  29. }

首先 sync.WaitGroup 对象的 Wait 函数本身是阻塞的,同时,超时用到的time.Timer 对象也需要阻塞的读。
同时阻塞的两个对象肯定要每个启动一个协程,每个协程去处理一个阻塞,难点在于怎么知道哪个阻塞先完成。
目前我用的方式是声明一个没有缓冲的chan,谁先完成谁优先向管道中写入数据。

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. "time"
  6. )
  7. func main() {
  8. wg := sync.WaitGroup{}
  9. c := make(chan struct{})
  10. for i := 0; i < 10; i++ {
  11. wg.Add(1)
  12. go func(num int, close <-chan struct{}) {
  13. defer wg.Done()
  14. <-close
  15. fmt.Println(num)
  16. }(i, c)
  17. }
  18. if WaitTimeout(&wg, time.Second*5) {
  19. close(c)
  20. fmt.Println("timeout exit")
  21. }
  22. time.Sleep(time.Second * 10)
  23. }
  24. func WaitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
  25. // 要求手写代码
  26. // 要求sync.WaitGroup支持timeout功能
  27. // 如果timeout到了超时时间返回true
  28. // 如果WaitGroup自然结束返回false
  29. ch := make(chan bool)
  30. go time.AfterFunc(timeout, func() {
  31. ch <- true
  32. })
  33. go func() {
  34. wg.Wait()
  35. ch <- false
  36. }()
  37. return <- ch
  38. }

这里会有多少个goroutine泄露

  1. package main
  2. import (
  3. "fmt"
  4. "io/ioutil"
  5. "net/http"
  6. "runtime"
  7. )
  8. func main() {
  9. num := 6
  10. for index := 0; index < num; index++ {
  11. resp, _ := http.Get("https://www.baidu.com")
  12. _, _ = ioutil.ReadAll(resp.Body)
  13. }
  14. fmt.Printf("此时goroutine个数= %d\n", runtime.NumGoroutine())
  15. }

每次泄漏一个读和写goroutine,就是12个goroutine,加上main函数本身也是一个goroutine,一共有13个goroutine,

  • 所以结论呼之欲出了,虽然执行了 6 次循环,而且每次都没有执行 Body.Close() ,就是因为执行了ioutil.ReadAll()把内容都读出来了,连接得以复用,因此只泄漏了一个读goroutine和一个写goroutine,最后加上main goroutine,所以答案就是3个goroutine
  • 这是用同一个域名的情况下

https://github.com/lifei6671/interview-go/blob/master/question/q015.md