伴鱼面试题
1 用time.After和context实现,两个方法本质上都是用了context或chan进行传递信息,如果go协程结束了,则用cancel关闭协程或往channel中传入值,同时用case time.After进行阻塞,若go协程超时了,则会走case time.After通道,
两种方法,第一种是go协程里嵌套了go协程,第二种是先启动n个go协程去运行,再启动n个go协程去监控,
package main import ( "context" "fmt" "sync" "time" ) //实现从三个网站爬取数据并用map保存的功能,如果协程超过1秒,则直接返回, // 1 用time.After结合context实现, // https://shockerli.net/post/golang-select-time-implement-timeout/ var res = make(map[string]string) var rw sync.RWMutex func writeMap(ctx context.Context, key string, cancel context.CancelFunc) { rw.Lock() res[key] = (" " + key + " 网站爬取结果") rw.Unlock() // 写这个是为了节省时间,因为有可能go协程的运行时间小于WithTimeout中所给的时间, // 导致程序已经取完数据了,go协程仍然没有结束, cancel() } // 这里必须要开启两个go协程,一个用于运行爬虫程序,另一个用于监控时间,其中一个case是time.After即超时的channel, // 另一个是go协程结束后,会close ctx.Done,或者在规定的WithTimeout时间内go协程没有结束的话,WithTimeout内部会close ctx.Done func doSomething(key string) { ctx, cancel := context.WithTimeout(context.Background(), time.Second * 4) // 这个是为了防止忘记关闭协程,导致内存泄漏, defer cancel() go writeMap(ctx, key, cancel) select { case <- ctx.Done(): fmt.Println(key + "网站爬取完毕!!!") // 注意这个time.After返回的是一个time类型的chan,所以这里可以这样写, case <-time.After(time.Second * 5): fmt.Println(key + "网站爬取超时!!!") } } func main() { var url = []string{ "www.baidu.com", "www.123.com", "www.456.com", } for _, num := range url { go doSomething(num) } time.Sleep(time.Second * 8) fmt.Println(res) }
用time.After和chan实现
// 只用time.After实现,这个方法的关键是当goroutine结束时,向chan通道中传入一个string,之后再用一个case去读取, // 如果能读到,则说明没有超时,否则走超时的case, var res sync.Map var wg sync.WaitGroup // 需要缓冲,不然阻塞 var sign = make(chan string, 3) func writeMap(key string) { res.Store(key, key+" 网站爬虫结果") } func doSomething(key string) { writeMap(key) sign <- key } func doSomething1(key string) { for { writeMap(key) } sign <- key } func main() { var url = []string{ "www.baidu.com", "www.123.com", "www.456.com", } for index, num := range url { if index == 2 { go doSomething1(num) } else { // 用死循环查看超时的情况, go doSomething(num) } } wg.Add(len(url)) for _, num := range url { go func() { defer wg.Done() select { case r := <-sign: fmt.Println(r + "网站爬取完毕!!!") return // 注意这个time.After返回的是一个time类型的chan,所以这里可以这样写, case <-time.After(time.Second * 3): fmt.Println(num + "网站爬取超时!!!") return } }() } wg.Wait() }
参考:https://shockerli.net/post/golang-select-time-implement-timeout/ 这是类似第二种方法,只不过只有一个goroutine,
2 火线安全笔试题
A 用两个协程交替输出数字和字母,12AB34CD56EF78GH910IJ1112KL1314MN1516OP1718QR1920ST2122UV2324WX2526YZ2728
发现这个题的一个问题是,两个协程的循环次数要一致,否则会出现死锁得情况,
思路一:关键是对channel的使用,channel有锁的作用,可以使用两个channel,协程1向chan2中写入,协程2向chan1中写入,chan1写在协程1的开头,chan2写在协程2的开头,这两个chan就像两个开关一样,互相控制着对方,协程1向chan2中写入数据,就像打开了协程2的开关,因为是两个协程交替执行,所以向chan2中写入数据要在协程1的末尾执行,
var chan1 = make(chan bool, 1) var chan2 = make(chan bool, 1) var index = make(chan bool, 1) func func1() { for i := 1; i < 26; i += 2 { // 2, 取走chan1里的元素,继续运行,打印两个数字 <-chan1 fmt.Print(i) fmt.Print(i + 1) // 3, 给chan2 放入一个元素,等待取走元素 chan2 <- true } time.Sleep(2000000) index <- true } func func2() { for i := 'A'; i <= 'Z'; i += 2 { // 4, chan2 取出元素,执行打印两个字符 , <-chan2 fmt.Print(string(i)) fmt.Print(string(i+1)) // 5, chan1 接收一个元素,进入阻塞状态,等待取走元素,进入第2步,2345步一直循环直到打印完 chan1 <- true } // 6, 结束循环,index通道接收一个元素,进入阻塞状态,等待取走 } func main() { // 1, chan1 里放一个值,进入阻塞状态,等待取走元素 chan1 <- true go func1() go func2() // 7, index通道取走元素,继续往下执行 <-index // 结果: 12AB34CD56EF78GH910IJ1112KL1314MN1516OP1718QR1920ST2122UV2324WX2526YZ }
思路二:也可以用waitgroup,结合defer更加优雅,
// 输出数字 func func1(){ defer wg.Done() for i := 1; i < 26; i += 2{ fmt.Print(i) fmt.Print(i+1) chan2 <- true <- chan1 } } // 输出字母 func func2(){ defer wg.Done() for i := 'A'; i < 'Z'; i += 2{ <- chan2 fmt.Print(string(i)) fmt.Print(string(i+1)) chan1 <- true } } var wg sync.WaitGroup var chan1 = make(chan bool, 1) var chan2 = make(chan bool, 1) func main(){ wg.Add(2) go func1() go func2() wg.Wait() }
参考:https://blog.csdn.net/swan_tang/article/details/103514633
3 蓝湖面试题
实现将三个生产者生产的数字放到一个数组中,并打印输出,题目给定了生产者返回的是一个channel,所以用select来监控,
package main import "fmt" // 三个协程是生产者,将它们生产的数据存起来, func Producer1() <-chan int{ a := make(chan int, 1) a <- 1 return a } func Producer2() <-chan int{ a := make(chan int, 1) a <- 2 return a } func Producer3() <-chan int{ a := make(chan int, 1) a <- 3 return a } func main(){ res := []int{} cou := 10 go Producer1() go Producer2() go Producer3() for cou > 0{ select{ case a1 := <- Producer1(): fmt.Println(a1) res = append(res, a1) go Producer1() case a2 := <- Producer2(): fmt.Println(a2) res = append(res, a2) go Producer1() case a3 := <- Producer3(): fmt.Println(a3) res = append(res, a3) go Producer1() } cou -= 1 } fmt.Println(res) }
自己构想的如果返回不是channel,即只用一个channel来实现,为了实现三个go协程的退出,这里使用了ctx来进行控制,因为三个go协程里都是死循环,无法通过WaitGroup来实现协程的退出
package main import ( "context" "fmt" "time" ) func Producer1(ctx context.Context){ for{ select { case <- ctx.Done(): return default: ch <- 1 time.Sleep(time.Second * 1) } } } func Producer2(ctx context.Context){ for{ select{ case <- ctx.Done(): return default: ch <- 2 time.Sleep(time.Second * 1) } } } func Producer3(ctx context.Context){ for{ select { case <- ctx.Done(): return default: ch <- 3 time.Sleep(time.Second * 1) } } } var ch chan int func main() { ctx, cancel := context.WithCancel(context.Background()) res := []int{} ch = make(chan int, 5) timeout := time.After(time.Second * 2) go Producer1(ctx) go Producer2(ctx) go Producer3(ctx) for{ select{ case a := <- ch: res = append(res, a) continue case <- timeout: // 这种写法是错误的,会每次都重新计时 //case <- time.After(time.Millisecond * 2): fmt.Println("abc") } // 注意这里break是专门为第二个case准备的,第一个case进去后会执行continue, // 这个break必须有,没有的话会死循环,也无法发挥定时器的作用, break } fmt.Println(res) cancel() }
4 LeetCode https://leetcode-cn.com/problems/print-zero-even-odd/comments/
输入5 打印0102030405
思路:func0控制func1和func2,func1 func2控制func0,只不过添加了个sign用于判断是该开启func1还是func2,
package main import ( "fmt" "sync" ) // 打印0与奇偶数, // 输入:n = 5 // 输出:"0102030405" func func1(){ for i := 0; i < n; i++ { <- ch0 fmt.Print(0) if sign == 1{ sign = 2 ch1 <- true } else { sign = 1 ch2 <- true } wg.Done() } } // 生成奇数 func func2(){ for i := 1; i <= n; i += 2{ <- ch1 fmt.Print(i) ch0 <- true wg.Done() } } // 生成偶数 func func3(){ for i := 2; i <= n; i += 2{ <- ch2 fmt.Print(i) ch0 <- true wg.Done() } } var ch0 = make(chan bool, 1) var ch1 = make(chan bool, 1) var ch2 = make(chan bool, 1) var sign int var wg sync.WaitGroup var n int func main() { n = 9 sign = 1 wg.Add(2 * n) // 这个相当于一个启动子,类似于总开关, // 先让func0工作,func1和func2才能工作 ch0 <- true go func1() go func2() go func3() wg.Wait() }