天天看點

go逾時控制有4種寫法,你知道嗎?

當然,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()
}
      

  

go逾時控制有4種寫法,你知道嗎?