中間件介紹
中間件可以理解為框架的鈎子,對所有的請求和響應進行攔截
- 簡單的中間件Demo
package main import ( "fmt" "github.com/kataras/iris" "strings" ) // 中間件小Demo func main() { app := iris.New() app.Get("/name/{name}",before,mainHandler,after) // before, mainHandler,after感覺這個設計思路好像那個koa2 app.Run(iris.Addr(":8085"), iris.WithCharset("UTF-8")) } func before(ctx iris.Context) { name := ctx.Params().Get("name") // 不區分大小寫的判斷 if strings.EqualFold(name, "wang"){ fmt.Println("before.............") ctx.Next() return } ctx.WriteString("error nane") } func after(ctx iris.Context) { fmt.Println("after..............") } func mainHandler (ctx iris.Context) { fmt.Println("main.............") ctx.WriteString("OK............") ctx.Next() }
/*
before.............
main.............
after..............
before.............
main.............
after..............
*/ - 全局中間件
func main() { app := iris.New() // 注冊前置全局中間件 app.Use(before) // 主持后置 app.Done(after) app.Get("/", func(ctx iris.Context) { ctx.HTML("<h1>Hello</h1>") ctx.Next() }) app.Run(iris.Addr(":8085"),iris.WithCharset("UTF-8")) } func before(ctx iris.Context) { header := ctx.GetHeader("token") fmt.Println("全局前置..........",header) ctx.Next() } func after(ctx iris.Context) { fmt.Println("后置............") }
Go語言中的Context
-
-
- 產生背景:
1.web編程中,一個請求對應多個goroutine之間的數據交互 2.超時控制 3.上下文控制
- 產生背景:
-
- 我們知道在go語言中goroutie是一個輕量級的協程,但是同時幾個gorountie進行通信我們使用到了channel,但是這樣的方式需要被優化,這就出現了Context。它是專門用來簡化對於處理單個請求的多個goroutine之間與請求域的數據、取消信號、截止時間等相關操作,這些操作可能涉及多個 API 調用。
-
比如有一個網絡請求Request,每個Request都需要開啟一個goroutine做一些事情,這些goroutine又可能會開啟其他的goroutine。這樣的話, 我們就可以通過Context,來跟蹤這些goroutine,並且通過Context來控制他們的目的,這就是Go語言為我們提供的Context,中文可以稱之為“上下文”。另外一個實際例子是,在Go服務器程序中,每個請求都會有一個goroutine去處理。然而,處理程序往往還需要創建額外的goroutine去訪問后端資源,比如數據庫、RPC服務等。由於這些goroutine都是在處理同一個請求,所以它們往往需要訪問一些共享的資源,比如用戶身份信息、認證token、請求截止時間等。而且如果請求超時或者被取消后,所有的goroutine都應該馬上退出並且釋放相關的資源。這種情況也需要用Context來為我們取消掉所有goroutine
Context 定義
Context的主要數據結構是一種嵌套的結構或者說是單向的繼承關系的結構,比如最初的context是一個小盒子,里面裝了一些數據,之后從這個context繼承下來的children就像在原本的context中又套上了一個盒子,然后里面裝着一些自己的數據。或者說context是一種分層的結構,根據使用場景的不同,每一層context都具備有一些不同的特性,這種層級式的組織也使得context易於擴展,職責清晰。
context 包的核心是 struct Context,聲明如下:
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
可以看到Context是一個interface,在golang里面,interface是一個使用非常廣泛的結構,它可以接納任何類型。Context定義很簡單,一共4個方法,我們需要能夠很好的理解這幾個方法
-
Deadline方法是獲取設置的截止時間的意思,第一個返回式是截止時間,到了這個時間點,Context會自動發起取消請求;第二個返回值ok==false時表示沒有設置截止時間,如果需要取消的話,需要調用取消函數進行取消。
-
Done方法返回一個只讀的chan,類型為struct{},我們在goroutine中,如果該方法返回的chan可以讀取,則意味着parent context已經發起了取消請求,我們通過Done方法收到這個信號后,就應該做清理操作,然后退出goroutine,釋放資源。之后,Err 方法會返回一個錯誤,告知為什么 Context 被取消。
-
Err方法返回取消的錯誤原因,因為什么Context被取消。
-
Value方法獲取該Context上綁定的值,是一個鍵值對,所以要通過一個Key才可以獲取對應的值,這個值一般是線程安全的。
Context 的實現方法
Context 雖然是個接口,但是並不需要使用方實現,golang內置的context 包,已經幫我們實現了2個方法,一般在代碼中,開始上下文的時候都是以這兩個作為最頂層的parent context,然后再衍生出子context。這些 Context 對象形成一棵樹:當一個 Context 對象被取消時,繼承自它的所有 Context 都會被取消。兩個實現如下:
var ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo }
一個是Background,主要用於main函數、初始化以及測試代碼中,作為Context這個樹結構的最頂層的Context,也就是根Context,它不能被取消。一個是TODO,如果我們不知道該使用什么Context的時候,可以使用這個,但是實際應用中,暫時還沒有使用過這個TODO。他們兩個本質上都是emptyCtx結構體類型,是一個不可取消,沒有設置截止時間,沒有攜帶任何值的Context。
type emptyCtx int func (*emptyCtx) Deadline() (deadline time.Time, ok bool) { return } func (*emptyCtx) Done() <-chan struct{} { return nil } func (*emptyCtx) Err() error { return nil } func (*emptyCtx) Value(key interface{}) interface{} { return nil }
Context 的 繼承
有了如上的根Context,那么是如何衍生更多的子Context的呢?這就要靠context包為我們提供的With系列的函數了。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) func WithValue(parent Context, key, val interface{}) Context
通過這些函數,就創建了一顆Context樹,樹的每個節點都可以有任意多個子節點,節點層級可以有任意多個。
WithCancel函數,傳遞一個父Context作為參數,返回子Context,以及一個取消函數用來取消Context。
WithDeadline函數,和WithCancel差不多,它會多傳遞一個截止時間參數,意味着到了這個時間點,會自動取消Context,當然我們也可以不等到這個時候,可以提前通過取消函數進行取消。
WithTimeout和WithDeadline基本上一樣,這個表示是超時自動取消,是多少時間后自動取消Context的意思。
WithValue函數和取消Context無關,它是為了生成一個綁定了一個鍵值對數據的Context,這個綁定的數據可以通過Context.Value方法訪問到,這是我們實際用經常要用到的技巧,一般我們想要通過上下文來傳遞數據時,可以通過這個方法,如我們需要tarce追蹤系統調用棧的時候。
With 系列函數詳解
WithCancel
context.WithCancel生成了一個withCancel的實例以及一個cancelFuc,這個函數就是用來關閉ctxWithCancel中的 Done channel 函數。
下面來分析下源碼實現,首先看看初始化,如下:
func newCancelCtx(parent Context) cancelCtx { return cancelCtx{ Context: parent, done: make(chan struct{}), } } func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { c := newCancelCtx(parent) propagateCancel(parent, &c) return &c, func() { c.cancel(true, Canceled) } }
newCancelCtx返回一個初始化的cancelCtx,cancelCtx結構體繼承了Context,實現了canceler方法:
//*cancelCtx 和 *timerCtx 都實現了canceler接口,實現該接口的類型都可以被直接canceled type canceler interface { cancel(removeFromParent bool, err error) Done() <-chan struct{} } type cancelCtx struct { Context done chan struct{} // closed by the first cancel call. mu sync.Mutex children map[canceler]bool // set to nil by the first cancel call err error // 當其被cancel時將會把err設置為非nil } func (c *cancelCtx) Done() <-chan struct{} { return c.done } func (c *cancelCtx) Err() error { c.mu.Lock() defer c.mu.Unlock() return c.err } func (c *cancelCtx) String() string { return fmt.Sprintf("%v.WithCancel", c.Context) } //核心是關閉c.done //同時會設置c.err = err, c.children = nil //依次遍歷c.children,每個child分別cancel //如果設置了removeFromParent,則將c從其parent的children中刪除 func (c *cancelCtx) cancel(removeFromParent bool, err error) { if err == nil { panic("context: internal error: missing cancel error") } c.mu.Lock() if c.err != nil { c.mu.Unlock() return // already canceled } c.err = err close(c.done) for child := range c.children { // NOTE: acquiring the child's lock while holding parent's lock. child.cancel(false, err) } c.children = nil c.mu.Unlock() if removeFromParent { removeChild(c.Context, c) // 從此處可以看到 cancelCtx的Context項是一個類似於parent的概念 } }
可以看到,所有的children都存在一個map中;Done方法會返回其中的done channel, 而另外的cancel方法會關閉Done channel並且逐層向下遍歷,關閉children的channel,並且將當前canceler從parent中移除。
WithCancel初始化一個cancelCtx的同時,還執行了propagateCancel方法,最后返回一個cancel function。
propagateCancel 方法定義如下:
// propagateCancel arranges for child to be canceled when parent is. func propagateCancel(parent Context, child canceler) { if parent.Done() == nil { return // parent is never canceled } if p, ok := parentCancelCtx(parent); ok { p.mu.Lock() if p.err != nil { // parent has already been canceled child.cancel(false, p.err) } else { if p.children == nil { p.children = make(map[canceler]struct{}) } p.children[child] = struct{}{} } p.mu.Unlock() } else { go func() { select { case <-parent.Done(): child.cancel(false, parent.Err()) case <-child.Done(): } }() } }
propagateCancel 的含義就是傳遞cancel,從當前傳入的parent開始(包括該parent),向上查找最近的一個可以被cancel的parent, 如果找到的parent已經被cancel,則將方才傳入的child樹給cancel掉,否則,將child節點直接連接為找到的parent的children中(Context字段不變,即向上的父親指針不變,但是向下的孩子指針變直接了); 如果沒有找到最近的可以被cancel的parent,即其上都不可被cancel,則啟動一個goroutine等待傳入的parent終止,則cancel傳入的child樹,或者等待傳入的child終結。
WithDeadLine
在withCancel的基礎上進行的擴展,如果時間到了之后就進行cancel的操作,具體的操作流程基本上與withCancel一致,只不過控制cancel函數調用的時機是有一個timeout的channel所控制的。
Context 使用原則 和 技巧
- 不要把Context放在結構體中,要以參數的方式傳遞,parent Context一般為Background
- 應該要把Context作為第一個參數傳遞給入口請求和出口請求鏈路上的每一個函數,放在第一位,變量名建議都統一,如ctx。
- 給一個函數方法傳遞Context的時候,不要傳遞nil,否則在tarce追蹤的時候,就會斷了連接
- Context的Value相關方法應該傳遞必須的數據,不要什么數據都使用這個傳遞
- Context是線程安全的,可以放心的在多個goroutine中傳遞
- 可以把一個 Context 對象傳遞給任意個數的 gorotuine,對它執行 取消 操作時,所有 goroutine 都會接收到取消信號。
Context的常用方法實例
-
調用Context Done方法取消
func Stream(ctx context.Context, out chan<- Value) error { for { v, err := DoSomething(ctx) if err != nil { return err } select { case <-ctx.Done(): return ctx.Err() case out <- v: } } }
- 通過 context.WithValue 來傳值
func main() { ctx, cancel := context.WithCancel(context.Background()) valueCtx := context.WithValue(ctx, key, "add value") go watch(valueCtx) time.Sleep(10 * time.Second) cancel() time.Sleep(5 * time.Second) } func watch(ctx context.Context) { for { select { case <-ctx.Done(): //get value fmt.Println(ctx.Value(key), "is cancel") return default: //get value fmt.Println(ctx.Value(key), "int goroutine") time.Sleep(2 * time.Second) } } }
- 超時取消 context.WithTimeout
package main import ( "fmt" "sync" "time" "golang.org/x/net/context" ) var ( wg sync.WaitGroup ) func work(ctx context.Context) error { defer wg.Done() for i := 0; i < 1000; i++ { select { case <-time.After(2 * time.Second): fmt.Println("Doing some work ", i) // we received the signal of cancelation in this channel case <-ctx.Done(): fmt.Println("Cancel the context ", i) return ctx.Err() } } return nil } func main() { ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) defer cancel() fmt.Println("Hey, I'm going to do some work") wg.Add(1) go work(ctx) wg.Wait() fmt.Println("Finished. I'm going home") }
- 截止時間 取消 context.WithDeadline
package main import ( "context" "fmt" "time" ) func main() { d := time.Now().Add(1 * time.Second) ctx, cancel := context.WithDeadline(context.Background(), d) // Even though ctx will be expired, it is good practice to call its // cancelation function in any case. Failure to do so may keep the // context and its parent alive longer than necessary. defer cancel() select { case <-time.After(2 * time.Second): fmt.Println("oversleep") case <-ctx.Done(): fmt.Println(ctx.Err()) } }
參考:https://www.jianshu.com/p/e5df3cd0708b