詳解Context包,看這一篇就夠了

2020-09-14 閃念基因

前言

最近在項目開發時,經常使用到 Context 這個包。 context.Context 是Go語言中獨特的設計,在其他程式語言中我們很少見到類似的概念。所以這一期我們就來好好講一講 Context 的基本概念與實際使用,麻麻再也不擔心我的並發編程啦~~~。

什麼是context

在理解 context 包之前,我們應該熟悉兩個概念,因為這能加深你對 context 的理解。

1. Goroutine

Goroutine 是一個輕量級的執行線程,多個 Goroutine 比一個線程輕量所以管理他們消耗的資源相對更少。 Goroutine 是Go中最基本的執行單元,每一個Go程序至少有一個 Goroutine :主 Goroutine 。程序啟動時會自動創建。這裡為了大家能更好的理解 Goroutine ,我們先來看一看線程與協程的概念。

  • 線程(Thread)

線程是一種輕量級進程,是CPU調度的最小單位。一個標準的線程由線程ID,當前指令指針(PC),寄存器集合和堆棧組成。線程是進程中的一個實體,是被系統獨立調度和分派的基本單位,線程自己不擁有系統資源,只擁有一點在運行中必不可少的資源,但它可與同屬於一個進程的其他線程共享進程所擁有的全部資源。線程擁有自己獨立的棧和共享的堆,共享堆,不共享棧,線程的切換一般也由作業系統調度。

  • 協程(coroutine)

又稱為微線程與子例程一樣,協程也是一種程序組建,相對子例程而言,協程更為靈活,但在實踐中使用沒有子例程那樣廣泛。和線程類似,共享堆,不共享棧,協程的切換一般由程式設計師在代碼中顯式控制。他避免了上下文切換的額外耗費,兼顧了多線程的優點,簡化了高並發程序的複雜。

Goroutine和其他語言的協程(coroutine)在使用方式上類似,但從字面意義上來看不同(一個是Goroutine,一個是coroutine),再就是協程是一種協作任務控制機制,在最簡單的意義上,協程不是並發的,而Goroutine支持並發的。因此Goroutine可以理解為一種Go語言的協程。同時它可以運行在一個或多個線程上。

我們來看一個簡單示例:

func Hello() { fmt.Println(&39;m asong&34;Golang夢工廠&FEFEFE; --tt-darkmode-bgcolor: F7F7F9; --tt-darkmode-bgcolor: F7F7F9; --tt-darkmode-bgcolor: FEFEFE; --tt-darkmode-bgcolor: F7F7F9; --tt-darkmode-bgcolor: F7F7F9; --tt-darkmode-bgcolor: F7F7F9; --tt-darkmode-bgcolor: F7F7F9; --tt-darkmode-bgcolor: F7F7F9; --tt-darkmode-bgcolor: F7F7F9; --tt-darkmode-bgcolor: FEFEFE; --tt-darkmode-bgcolor: FEFEFE; --tt-darkmode-bgcolor: FEFEFE; --tt-darkmode-bgcolor: FEFEFE; --tt-darkmode-bgcolor: 34;hello everybody , I&34;) ch <- 1}func main() { ch := make(chan int) go Hello(ch) <-ch fmt.Println(&34;)}

這裡我們使用通道進行等待,這樣 main 就會等待 goroutine 執行完。現在我們知道了 goroutinechannel 的概念了,下面我們就來介紹一下 context

3. 場景

有了上面的概念,我們在來看一個例子:

如下代碼,每次請求,Handler會創建一個goroutine來為其提供服務,而且連續請求3次, request 的地址也是不同的:

func main() { http.HandleFunc(&34;, SayHello) // 設置訪問的路由 log.Fatalln(http.ListenAndServe(&34;,nil))}func SayHello(writer http.ResponseWriter, request *http.Request) { fmt.Println(&request) writer.Write([]byte(&34;))}========================================================$ curl http://localhost:8080/0xc0000b80300xc0001860080xc000186018

而每個請求對應的Handler,常會啟動額外的的goroutine進行數據查詢或PRC調用等。

而當請求返回時,這些額外創建的goroutine需要及時回收。而且,一個請求對應一組請求域內的數據可能會被該請求調用鏈條內的各goroutine所需要。

現在我們對上面代碼在添加一點東西,當請求進來時, Handler 創建一個監控 goroutine ,這樣就會每隔1s列印一句 Current request is in progress

func main() { http.HandleFunc(&34;, SayHello) // 設置訪問的路由 log.Fatalln(http.ListenAndServe(&34;,nil))}func SayHello(writer http.ResponseWriter, request *http.Request) { fmt.Println(&request) go func() { for range time.Tick(time.Second) { fmt.Println(&34;) } }() time.Sleep(2 * time.Second) writer.Write([]byte(&34;))}

這裡我假定請求需要耗時2s,在請求2s後返回,我們期望監控goroutine在列印2次 Current request is in progress 後即停止。但運行發現,監控goroutine列印2次後,其仍不會結束,而會一直列印下去。

問題出在創建監控goroutine後,未對其生命周期作控制,下面我們使用context作一下控制,即監控程序列印前需檢測 request.Context() 是否已經結束,若結束則退出循環,即結束生命周期。

func main() { http.HandleFunc(&34;, SayHello) // 設置訪問的路由 log.Fatalln(http.ListenAndServe(&34;,nil))}func SayHello(writer http.ResponseWriter, request *http.Request) { fmt.Println(&request) go func() { for range time.Tick(time.Second) { select { case <- request.Context().Done(): fmt.Println(&34;) return default: fmt.Println(&34;) } } }() time.Sleep(2 * time.Second) writer.Write([]byte(&34;))}

基於如上需求,context包應用而生。context包可以提供一個請求從API請求邊界到各goroutine的請求域數據傳遞、取消信號及截至時間等能力。詳細原理請看下文。

4. context

在 Go 語言中 context 包允許您傳遞一個 &34; 到您的程序。 Context 如超時或截止日期(deadline)或通道,來指示停止運行和返回。例如,如果您正在執行一個 web 請求或運行一個系統命令,定義一個超時對生產級系統通常是個好主意。因為,如果您依賴的API運行緩慢,你不希望在系統上備份(back up)請求,因為它可能最終會增加負載並降低所有請求的執行效率。導致級聯效應。這是超時或截止日期 context 派上用場的地方。

4.1 設計原理

Go 語言中的每一個請求的都是通過一個單獨的 Goroutine 進行處理的,HTTP/RPC 請求的處理器往往都會啟動新的 Goroutine 訪問資料庫和 RPC 服務,我們可能會創建多個 Goroutine 來處理一次請求,而 Context 的主要作用就是在不同的 Goroutine 之間同步請求特定的數據、取消信號以及處理請求的截止日期。

每一個 Context 都會從最頂層的 Goroutine 一層一層傳遞到最下層,這也是 Golang 中上下文最常見的使用方式,如果沒有 Context ,當上層執行的操作出現錯誤時,下層其實不會收到錯誤而是會繼續執行下去。

當最上層的 Goroutine 因為某些原因執行失敗時,下兩層的 Goroutine 由於沒有接收到這個信號所以會繼續工作;但是當我們正確地使用 Context 時,就可以在下層及時停掉無用的工作減少額外資源的消耗:

這其實就是 Golang 中上下文的最大作用,在不同 Goroutine 之間對信號進行同步避免對計算資源的浪費,與此同時 Context 還能攜帶以請求為作用域的鍵值對信息。

這裡光說,其實也不能完全理解其中的作用,所以我們來看一個例子:

func main() { ctx,cancel := context.WithTimeout(context.Background(),1 * time.Second) defer cancel() go HelloHandle(ctx,500*time.Millisecond) select { case <- ctx.Done(): fmt.Println(&34;,ctx.Err()) }}func HelloHandle(ctx context.Context,duration time.Duration) { select { case <-ctx.Done(): fmt.Println(ctx.Err()) case <-time.After(duration): fmt.Println(&34;, duration) }}

上面的代碼,因為過期時間大於處理時間,所以我們有足夠的時間處理請求,所以運行代碼如下圖所示:

process request with 500msHello Handle context deadline exceeded

HelloHandle 函數並沒有進入超時的 select 分支,但是 main 函數的 select 卻會等待 context.Context 的超時並列印出 Hello Handle context deadline exceeded 。如果我們將處理請求的時間增加至 2000 ms,程序就會因為上下文過期而被終止。

context deadline exceededHello Handle context deadline exceeded

4.2 接口

context.Context 是 Go 語言在 1.7 版本中引入標準庫的接口 1 ,該接口定義了四個需要實現的方法,其中包括:

  • Deadline — 返回 context.Context 被取消的時間,也就是完成工作的截止日期;
  • Done — 返回一個 Channel,這個 Channel 會在當前工作完成或者上下文被取消之後關閉,多次調用 Done 方法會返回同一個 Channel;
  • Err — 返回 context.Context 結束的原因,它只會在 Done 返回的 Channel 被關閉時才會返回非空的值;如果 context.Context 被取消,會返回 Canceled 錯誤;如果 context.Context 超時,會返回 DeadlineExceeded 錯誤;
  • Value — 從 context.Context 中獲取鍵對應的值,對於同一個上下文來說,多次調用 Value 並傳入相同的 Key 會返回相同的結果,該方法可以用來傳遞請求特定的數據;

type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{}}

context 使用詳解

創建context

context 包允許一下方式創建和獲得 context :

  • context.Background() :這個函數返回一個空 context 。這只能用於高等級(在 main 或頂級請求處理中)。
  • context.TODO() :這個函數也是創建一個空 context 。也只能用於高等級或當您不確定使用什麼 context,或函數以後會更新以便接收一個 context 。這意味您(或維護者)計劃將來要添加 context 到函數。

其實我們查看原始碼。發現他倆都是通過 new(emptyCtx) 語句初始化的,它們是指向私有結構體 context.emptyCtx 的指針,這是最簡單、最常用的上下文類型:

var ( background = new(emptyCtx) todo = new(emptyCtx))type emptyCtx intfunc (*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}func (e *emptyCtx) String() string { switch e { case background: return &34; case todo: return &34; } return &34;}

從上述代碼,我們不難發現 context.emptyCtx 通過返回 nil 實現了 context.Context 接口,它沒有任何特殊的功能。

從原始碼來看, context.Backgroundcontext.TODO 函數其實也只是互為別名,沒有太大的差別。它們只是在使用和語義上稍有不同:

  • context.Background 是上下文的默認值,所有其他的上下文都應該從它衍生(Derived)出來。
  • context.TODO 應該只在不確定應該使用哪種上下文時使用;

在多數情況下,如果當前函數沒有上下文作為入參,我們都會使用 context.Background 作為起始的上下文向下傳遞。

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

這四個 With 函數,接收的都有一個partent參數,就是父Context,我們要基於這個父Context創建出子Context的意思,這種方式可以理解為子Context對父Context的繼承,也可以理解為基於父Context的衍生。

通過這些函數,就創建了一顆Context樹,樹的每個節點都可以有任意多個子節點,節點層級可以有任意多個。

WithCancel 函數,傳遞一個父Context作為參數,返回子Context,以及一個取消函數用來取消Context。 WithDeadline 函數,和 WithCancel 差不多,它會多傳遞一個截止時間參數,意味著到了這個時間點,會自動取消Context,當然我們也可以不等到這個時候,可以提前通過取消函數進行取消。

WithTimeoutWithDeadline 基本上一樣,這個表示是超時自動取消,是多少時間後自動取消Context的意思。

WithValue 函數和取消Context無關,它是為了生成一個綁定了一個鍵值對數據的Context,這個綁定的數據可以通過 Context.Value 方法訪問到,後面我們會專門講。

大家可能留意到,前三個函數都返回一個取消函數 CancelFunc ,這是一個函數類型,它的定義非常簡單。

type CancelFunc func()

這就是取消函數的類型,該函數可以取消一個Context,以及這個節點Context下所有的所有的Context,不管有多少層級。

下面我就展開來介紹一個每一個方法的使用。

WithValue

context 包中的 context.WithValue 函數能從父上下文中創建一個子上下文,傳值的子上下文使用 context.valueCtx 類型,我們看一下源碼:

// WithValue returns a copy of parent in which the value associated with key is// val.//// Use context Values only for request-scoped data that transits processes and// APIs, not for passing optional parameters to functions.//// The provided key must be comparable and should not be of type// string or any other built-in type to avoid collisions between// packages using context. Users of WithValue should define their own// types for keys. To avoid allocating when assigning to an// interface{}, context keys often have concrete type// struct{}. Alternatively, exported context key variables&34;nil key&34;key is not comparable&39;t// want context depending on the unicode tables. This is only used by// *valueCtx.String().func stringify(v interface{}) string { switch s := v.(type) { case stringer: return s.String() case string: return s } return &34;}func (c *valueCtx) String() string { return contextName(c.Context) + &34; + reflectlite.TypeOf(c.key).String() + &34; + stringify(c.val) + &34;}func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return c.Context.Value(key)}

此函數接收 context 並返回派生 context,其中值 val 與 key 關聯,並通過 context 樹與 context 一起傳遞。這意味著一旦獲得帶有值的 context,從中派生的任何 context 都會獲得此值。不建議使用 context 值傳遞關鍵參數,而是函數應接收籤名中的那些值,使其顯式化。

context.valueCtx 結構體會將除了 Value 之外的 ErrDeadline 等方法代理到父上下文中,它只會響應 context.valueCtx.Value 方法。如果 context.valueCtx 中存儲的鍵值對與 context.valueCtx.Value 方法中傳入的參數不匹配,就會從父上下文中查找該鍵對應的值直到在某個父上下文中返回 nil 或者查找到對應的值。

說了這麼多,比較枯燥,我們來看一下怎麼使用:

type key stringfunc main() { ctx := context.WithValue(context.Background(),key(&34;),&34;) Get(ctx,key(&34;)) Get(ctx,key(&34;))}func Get(ctx context.Context,k key) { if v, ok := ctx.Value(k).(string); ok { fmt.Println(v) }}

上面代碼我們基於 context.Background 創建一個帶值的ctx,然後可以根據key來取值。這裡為了避免多個包同時使用 context 而帶來衝突, key 不建議使用 string 或其他內置類型,所以建議自定義 key 類型.

WithCancel

此函數創建從傳入的父 context 派生的新 context。父 context 可以是後臺 context 或傳遞給函數的 context。返回派生 context 和取消函數。只有創建它的函數才能調用取消函數來取消此 context。如果您願意,可以傳遞取消函數,但是,強烈建議不要這樣做。這可能導致取消函數的調用者沒有意識到取消 context 的下遊影響。可能存在源自此的其他 context,這可能導致程序以意外的方式運行。簡而言之,永遠不要傳遞取消函數。

我們直接從 context.WithCancel 函數的實現來看它到底做了什麼:

// WithCancel returns a copy of parent with a new Done channel. The returned// context&39;s Done channel is closed, whichever happens first.//// Canceling this context releases resources associated with it, so code should// call cancel as soon as the operations running in this Context complete.func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { c := newCancelCtx(parent) propagateCancel(parent, &c) return &c, func() { c.cancel(true, Canceled) }}// newCancelCtx returns an initialized cancelCtx.func newCancelCtx(parent Context) cancelCtx { return cancelCtx{Context: parent}}

  • context.newCancelCtx 將傳入的上下文包裝成私有結構體 context.cancelCtx
  • context.propagateCancel 會構建父子上下文之間的關聯,當父上下文被取消時,子上下文也會被取消:

func propagateCancel(parent Context, child canceler) { done := parent.Done() if done == nil { return // 父上下文不會觸發取消信號 } select { case <-done: child.cancel(false, parent.Err()) // 父上下文已經被取消 return default: } if p, ok := parentCancelCtx(parent); ok { p.mu.Lock() if p.err != nil { child.cancel(false, p.err) } else { p.children[child] = struct{}{} } p.mu.Unlock() } else { go func() { select { case <-parent.Done(): child.cancel(false, parent.Err()) case <-child.Done(): } }() }}

上述函數總共與上下文相關的三種不同的情況:

  1. parent.Done() == nil ,也就是 parent 不會觸發取消事件時,當前函數會直接返回;
  2. child 的繼承鏈包含可以取消的上下文時,會判斷 parent 是否已經觸發了取消信號;如果已經被取消, child 會立刻被取消;如果沒有被取消, child 會被加入 parentchildren 列表中,等待 parent 釋放取消信號;
  3. 在默認情況下運行一個新的 Goroutine 同時監聽 parent.Done()child.Done() 兩個 Channel在 parent.Done() 關閉時調用 child.cancel 取消子上下文;

context.propagateCancel 的作用是在 parentchild 之間同步取消和結束的信號,保證在 parent 被取消時, child 也會收到對應的信號,不會發生狀態不一致的問題。

context.cancelCtx 實現的幾個接口方法也沒有太多值得分析的地方,該結構體最重要的方法是 cancel ,這個方法會關閉上下文中的 Channel 並向所有的子上下文同步取消信號:

func (c *cancelCtx) cancel(removeFromParent bool, err error) { c.mu.Lock() if c.err != nil { c.mu.Unlock() return } c.err = err if c.done == nil { c.done = closedchan } else { close(c.done) } for child := range c.children { child.cancel(false, err) } c.children = nil c.mu.Unlock() if removeFromParent { removeChild(c.Context, c) }}

說了這麼,看一例子,帶你感受一下使用方法:

func main() { ctx,cancel := context.WithCancel(context.Background()) defer cancel() go Speak(ctx) time.Sleep(10*time.Second)}func Speak(ctx context.Context) { for range time.Tick(time.Second){ select { case <- ctx.Done(): return default: fmt.Println(&34;) } }}

我們使用 withCancel 創建一個基於 Background 的ctx,然後啟動一個講話程序,每隔1s說一話, main 函數在10s後執行 cancel ,那麼 speak 檢測到取消信號就會退出。

WithDeadline

此函數返回其父項的派生 context,當截止日期超過或取消函數被調用時,該 context 將被取消。例如,您可以創建一個將在以後的某個時間自動取消的 context,並在子函數中傳遞它。當因為截止日期耗盡而取消該 context 時,獲此 context 的所有函數都會收到通知去停止運行並返回。

我們來看一下源碼:

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { if cur, ok := parent.Deadline(); ok && cur.Before(d) { return WithCancel(parent) } c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, } propagateCancel(parent, c) dur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded) // 已經過了截止日期 return c, func() { c.cancel(false, Canceled) } } c.mu.Lock() defer c.mu.Unlock() if c.err == nil { c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded) }) } return c, func() { c.cancel(true, Canceled) }}

context.WithDeadline 也都能創建可以被取消的計時器上下文 context.timerCtx

context.WithDeadline 方法在創建 context.timerCtx 的過程中,判斷了父上下文的截止日期與當前日期,並通過 time.AfterFunc 創建定時器,當時間超過了截止日期後會調用 context.timerCtx.cancel 方法同步取消信號。

context.timerCtx 結構體內部不僅通過嵌入了 context.cancelCtx 結構體繼承了相關的變量和方法,還通過持有的定時器 timer 和截止時間 deadline 實現了定時取消這一功能:

type timerCtx struct { cancelCtx timer *time.Timer // Under cancelCtx.mu. deadline time.Time}func (c *timerCtx) Deadline() (deadline time.Time, ok bool) { return c.deadline, true}func (c *timerCtx) cancel(removeFromParent bool, err error) { c.cancelCtx.cancel(false, err) if removeFromParent { removeChild(c.cancelCtx.Context, c) } c.mu.Lock() if c.timer != nil { c.timer.Stop() c.timer = nil } c.mu.Unlock()}

context.timerCtx.cancel 方法不僅調用了 context.cancelCtx.cancel ,還會停止持有的定時器減少不必要的資源浪費。

接下來我們來看一個例子:

func main() { now := time.Now() later,_:=time.ParseDuration(&34;) ctx,cancel := context.WithDeadline(context.Background(),now.Add(later)) defer cancel() go Monitor(ctx) time.Sleep(20 * time.Second)}func Monitor(ctx context.Context) { select { case <- ctx.Done(): fmt.Println(ctx.Err()) case <-time.After(20*time.Second): fmt.Println(&34;) }}

設置一個監控 goroutine ,使用WithTimeout創建一個基於Background的ctx,其會當前時間的10s後取消。驗證結果如下:

context deadline exceeded

10s,監控 goroutine 被取消了。

WithTimeout

此函數類似於 context.WithDeadline。不同之處在於它將持續時間作為參數輸入而不是時間對象。此函數返回派生 context,如果調用取消函數或超出超時持續時間,則會取消該派生 context。

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout))}

觀看源碼我們可以看出 WithTimeout 內部調用的就是 WithDeadline ,其原理都是一樣的,上面已經介紹過了,來看一個例子吧:

func main() { ctx,cancel := context.WithTimeout(context.Background(),10 * time.Second) defer cancel() go Monitor(ctx) time.Sleep(20 * time.Second)}func Monitor(ctx context.Context) { select { case <- ctx.Done(): fmt.Println(ctx.Err()) case <-time.After(20*time.Second): fmt.Println(&34;) }}

Context使用原則

  • context.Background 只應用在最高等級,作為所有派生 context 的根。
  • context 取消是建議性的,這些函數可能需要一些時間來清理和退出。
  • 不要把 Context 放在結構體中,要以參數的方式傳遞。
  • Context 作為參數的函數方法,應該把 Context 作為第一個參數,放在第一位。
  • 給一個函數方法傳遞Context的時候,不要傳遞nil,如果不知道傳遞什麼,就使用context.TODO
  • Context的Value相關方法應該傳遞必須的數據,不要什麼數據都使用這個傳遞。context.Value 應該很少使用,它不應該被用來傳遞可選參數。這使得 API 隱式的並且可以引起錯誤。取而代之的是,這些值應該作為參數傳遞。
  • Context是線程安全的,可以放心的在多個goroutine中傳遞。同一個Context可以傳給使用其的多個goroutine,且Context可被多個goroutine同時安全訪問。
  • Context 結構沒有取消方法,因為只有派生 context 的函數才應該取消 context。

Go 語言中的 context.Context 的主要作用還是在多個 Goroutine 組成的樹中同步取消信號以減少對資源的消耗和佔用,雖然它也有傳值的功能,但是這個功能我們還是很少用到。在真正使用傳值的功能時我們也應該非常謹慎,使用 context.Context 進行傳遞參數請求的所有參數一種非常差的設計,比較常見的使用場景是傳遞請求對應用戶的認證令牌以及用於進行分布式追蹤的請求 ID。

總結

好啦,這一期文章到這裡就結束啦。為了弄懂這裡,參考很多文章,會在結尾貼出來,供大家學習參考。因為這個包真的很重要,在平常項目開發中我們也是經常使用到,所以大家弄懂 context 的原理還是很有必要的。

文章的示例代碼已上傳github:

https://github.com/asong2020/Golang_Dream/tree/master/code_demo/context_example

相關焦點

  • Android_Context詳解
    每一個Activity和Service以及其對應的基礎context,對每個實例來說都是唯一的。BroadcastReciver - 它本身不是context,也沒有context在它裡面,但是每當一個新的廣播到達的時候,框架都傳遞一個context對象到onReceive()。
  • 學習JavaWeb這一篇就夠了 Servlet3 (8/8)
    學習JavaWeb這一篇就夠了,從入門到精通,由淺至深,共八章,此篇為第八章第3小節。附帶安裝包下載。修改AnnotationListener,以後三大組建的配置都是在contextInitialized
  • 學習JavaWeb這一篇就夠了 Listener (5/8)
    學習JavaWeb這一篇就夠了,從入門到精通,由淺至深,共八章,此篇為第五章。附帶安裝包下載。(ServletContextEvent servletContextEvent) { System.out.println("contextInitialized ..."); } @Override public void contextDestroyed(ServletContextEvent servletContextEvent) {
  • Golang源碼分析系列之官方Context包
    Context簡介Context是由Golang官方開發的並發控制包,一方面可以用於當請求超時或者取消時候,相關的goroutine馬上退出釋放資源,另一方面Context本身含義就是上下文,其可以在多個goroutine或者多個處理函數之間傳遞共享的信息。
  • 立式攪拌器安裝教程看這一篇就夠了
    立式攪拌器安裝教程看這一篇就夠了 ,「r44opv」   中藍水處理成套設備(南京)有限公司,立式攪拌器,產品齊全,質量可靠,廣泛應用,歡迎來電諮詢選購!
  • SpringMVC從入門到源碼,這一篇就夠
    這一期我們就來了解「SpringMVC的底層原理和源碼」,在以前的JSP時代,代碼中前端和後端都混在一起,可能比較老的程式設計師就寫過下面的代碼,這就是大名鼎鼎的JSP和Servlet時代。-- 給出需要掃描Dao接口包 --> <property name=&34; value=&34;/> </bean> <!
  • 快速讀懂Http/3協議,一篇就夠
    《網絡編程懶人入門(十):一泡尿的時間,快速讀懂QUIC協議》《網絡編程懶人入門(十一):一文讀懂什麼是IPv6》《網絡編程懶人入門(十二):快速讀懂Http/3協議,一篇就夠!》(本文)學習交流:- 即時通訊/推送技術開發交流5群:215477170 [推薦]- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM》歡迎關注「即時通訊技術圈」,更多好文會同步發布在公眾號。
  • 聖誕圖片素材,看這一篇就夠了!
    聖誕圖片素材,看這一篇就夠了! 世上本無聖誕老人 所有的驚喜和禮物都來自愛你的人 聖誕馬上就要到了 花姐把這組精美的圖片送給各位老闆
  • C語言 指針入門 看這一篇就夠了
    本篇文章是從 指針的運算開始講的 ,可能對完全沒基礎的同學不太友好建議從這一篇文章開始閱讀[C語言必知必會] 指針(https://mp.weixin.qq.com/s/x3un4tnaHSISUfP8n3V_6g)指針的運算
  • ServletContext用法詳解
    1.如何得到對象ServletContext context = getServletContext();//獲取對象2.有什麼作用?①獲取全局配置參數②獲取web工程中的資源獲取資源在tomcat裡面的絕對路徑context.getRealPath("");getResourceAsStream 獲取資源流對象直接給出相對的路徑,獲取流對象③存取數據,servlet間共享數據定義一個登陸的html頁面,定義一個form表單定義一個servlet,名為LoginServlet
  • SringMVC從入門到源碼,這一篇就夠
    這一期我們就來了解「SpringMVC的底層原理和源碼」,在以前的JSP時代,代碼中前端和後端都混在一起,可能比較老的程式設計師就寫過下面的代碼,這就是大名鼎鼎的JSP和Servlet時代。這樣的代碼前端和後端混在一起,相互依賴JSP與Java Bean之間嚴重耦合,java代碼和Html代碼混在一起,這要求開發人員既要會前端也要會後端,給測試帶來了很多不方便,代碼也不能復用。
  • 《絕地求生》使用外部語音包簡單方法詳解 怎麼在遊戲裡用語音包
    【絕地求生攻略】《絕地求生》中怎麼使用外部語音包呢?許多玩家可能還不太清楚在遊戲中使用外部語音包的方法,今天就為大家分享一篇「Ferrerocher」介紹的《絕地求生》使用外部語音包簡單方法詳解,一起來了解一下吧。
  • Linux 系統結構詳解,看這一篇就夠了
    一塊硬碟即使只有一個主分區,邏輯分區也是從5開始編號的,這點應特別注意。總結:一個硬碟分區首先要大確認在哪個硬碟,然後再確認它所在硬碟內的哪個分區。首先要安裝虛擬電腦工具包:在VirtualBox的菜單裡選擇」設備」->」安裝虛擬電腦工具包」,你會發現在Ubuntu桌面上多出一個光碟圖標,這張光碟默認被自動加載到了文件夾/media/cdom0,而且/cdrom自動指向這個文件夾。默認設置下文件管理器會自動打開這張光碟,可以看到裡面有個」VBoxLinuxAdditions.run」文件。
  • Go語言愛好者周刊:第 58 期—關於 context
    package mainimport (    &34;    &34;)func f(ctx context.Context) {    context.WithValue(ctx, &34
  • 詳解50個基礎體式的練習口令和重點,瑜伽初學者入門看這一篇就夠
    詳解50個基礎體式的練習方法和重點,瑜伽初學者入門看這一篇就夠很多瑜伽初學者會為找不到入門的方法,不知道從哪入手開始學習或者學了這個體式又忘了那個動作而苦惱。給大家梳理一下,一次性詳解50個瑜伽基礎體式的練習口令及重點,希望對瑜伽初學者所幫助。
  • 看這一篇就夠了
    看這一篇就夠了!一、外牆漏水三、衛生間門檻石和門框的連接部分原因:裝門檻石的時候,會留下幾毫米的縫隙,門檻石裝好後裝門框,門框會壓在門檻石上面,這幾毫米的縫隙就看不到了,洗澡的時水流量大,流到門檻石的位置就會從這幾毫米的縫隙滲出。
  • 看這一篇就夠了
    舊機場路餐廳將東南亞菜的「鮮香酸辣」與傳統粵菜的烹飪技法完美結合,配以新鮮的食材與原產地的輔料,來仲夏之夜感受這獨具匠心的美味。重視食材的日料,是一種做減法的料理,順應了自然的本真口味。井上屋將把這份本真帶來仲夏之夜,期待一下吧!
  • Dubbo和Zookeeper入門到實戰,看這篇就夠了
    開頭是介紹,只看實戰可直接下滑。內容很多,建議點讚收藏,電腦上觀看更佳哦!Dubbox簡介Dubbox 是一個分布式rpc框架,是阿里巴巴的開源產品,後阿里便不再維護,由當當網進行維護,並改名為Dubbo。
  • 彈力帶健身,看這一篇就夠了,值得收藏!!
    好了,今天關於《彈力帶健身,看這一篇就夠了,值得收藏!!》的內容就到這了,感謝您的收看,看完的朋友們反饋一下,哪些地方您覺得可以改進,任何看的不舒服的地方都可以提,非常歡迎大家留言、關注和分享。
  • 超全攻略 | 你和馬來西亞馬拉松的距離,這一篇就夠了!
    這個十一, 旅遊+跑步就在馬來西亞吉隆坡借跑馬之名,呼朋喚友、攜一家老小去旅行為此小編為各位有夢想的跑友準備了一篇超全的馬來西亞馬拉松攻略你和馬來西亞馬拉松的距離,看這一篇就夠了!2017馬來西亞馬拉松正在火熱報名中~還沒報名的戳這裡!