當然,go語言的超時控制肯定不止4種方法,起這個標題是我的一種自嘲,讓我想起了孔乙己說的茴香的茴有4種寫法。
本文寫的4種方程都借助於同一個套路:
workDoneCh := make(chan struct{}, 1) go func() { LongTimeWork() //這是我們要控制超時的函數 workDoneCh <- struct{}{} }() select { //下面的case只執行最早到來的那一個 case <-workDone: //LongTimeWork運行結束 fmt.Println("LongTimeWork return") case <-timeoutCh: //timeout到來 fmt.Println("LongTimeWork timeout") }
比如我們希望100ms超時,那么100ms之后<-timeoutCh這個讀管道的操作需要解除阻塞,而解除阻塞有2種方式,要么有人往管道里寫入了數據,要么管道被close了。下面的4種方法就圍繞<-timeoutCh如何解除阻塞展開。
式一:
這種方式最簡單直接
timeoutCh := make(chan struct{}, 1) go func() { time.Sleep(100 * time.Millisecond) timeoutCh <- struct{}{} }()
式二:
不需要像方式一那樣顯式地創建一個timeoutCh管道,借助於time.After(duration),這個函數會返回一個管道,並且經過一段時間duration后它還會自動向管道send一個數據。
select { //下面的case只執行最早到來的那一個 case <-workDone: //LongTimeWork運行結束 fmt.Println("LongTimeWork return") case <-time.After(100 * time.Millisecond): //timeout到來 fmt.Println("LongTimeWork timeout") }
比式一優雅簡潔了不少。
式三:
go語言Context是一個接口,它的Done()成員方法返回一個管道。
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Value(key interface{}) interface{} }
cancelCtx是Context的一個具體實現,當調用它的cancle()函數時,會關閉Done()這個管道,<-Done()會解除阻塞。
ctx, cancel := context.WithCancel(context.Background()) go func() { time.Sleep(100 * time.Millisecond) cancel() }() select { //下面的case只執行最早到來的那一個 case <-workDone: fmt.Println("LongTimeWork return") case <-ctx.Done(): //ctx.Done()是一個管道,調用了cancel()都會關閉這個管道,然后讀操作就會立即返回 fmt.Println("LongTimeWork timeout") }
式四:
跟式三類似,timerCtx也是Context的一個具體實現,當調用它的cancle()函數或者到達指定的超時時間后,都會關閉Done()這個管道,<-Done()會解除阻塞。
ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*100) select { //下面的case只執行最早到來的那一個 case <-workDone: fmt.Println("LongTimeWork return") case <-ctx.Done(): //ctx.Done()是一個管道,context超時或者調用了cancel()都會關閉這個管道,然后讀操作就會立即返回 fmt.Println("LongTimeWork timeout") }
總體來看,式二和式四的代碼量是最少的。最后附上完整代碼:
package main import ( "context" "fmt" "time" ) const ( WorkUseTime = 500 * time.Millisecond Timeout = 100 * time.Millisecond ) //模擬一個耗時較長的任務 func LongTimeWork() { time.Sleep(WorkUseTime) return } //模擬一個接口處理函數 func Handle1() { deadline := make(chan struct{}, 1) workDone := make(chan struct{}, 1) go func() { //把要控制超時的函數放到一個協程里 LongTimeWork() workDone <- struct{}{} }() go func() { //把要控制超時的函數放到一個協程里 time.Sleep(Timeout) deadline <- struct{}{} }() select { //下面的case只執行最早到來的那一個 case <-workDone: fmt.Println("LongTimeWork return") case <-deadline: fmt.Println("LongTimeWork timeout") } } //模擬一個接口處理函數 func Handle2() { workDone := make(chan struct{}, 1) go func() { //把要控制超時的函數放到一個協程里 LongTimeWork() workDone <- struct{}{} }() select { //下面的case只執行最早到來的那一個 case <-workDone: fmt.Println("LongTimeWork return") case <-time.After(Timeout): fmt.Println("LongTimeWork timeout") } } //模擬一個接口處理函數 func Handle3() { //通過顯式sleep再調用cancle()來實現對函數的超時控制 ctx, cancel := context.WithCancel(context.Background()) workDone := make(chan struct{}, 1) go func() { //把要控制超時的函數放到一個協程里 LongTimeWork() workDone <- struct{}{} }() go func() { //100毫秒后調用cancel(),關閉ctx.Done() time.Sleep(Timeout) cancel() }() select { //下面的case只執行最早到來的那一個 case <-workDone: fmt.Println("LongTimeWork return") case <-ctx.Done(): //ctx.Done()是一個管道,調用了cancel()都會關閉這個管道,然后讀操作就會立即返回 fmt.Println("LongTimeWork timeout") } } //模擬一個接口處理函數 func Handle4() { //借助於帶超時的context來實現對函數的超時控制 ctx, cancel := context.WithTimeout(context.Background(), Timeout) defer cancel() //純粹出於良好習慣,函數退出前調用cancel() workDone := make(chan struct{}, 1) go func() { //把要控制超時的函數放到一個協程里 LongTimeWork() workDone <- struct{}{} }() select { //下面的case只執行最早到來的那一個 case <-workDone: fmt.Println("LongTimeWork return") case <-ctx.Done(): //ctx.Done()是一個管道,context超時或者調用了cancel()都會關閉這個管道,然后讀操作就會立即返回 fmt.Println("LongTimeWork timeout") } } func main() { Handle1() Handle2() Handle3() Handle4() }