Go語言經典庫流量分析(二)| Gorilla Context

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

Go語言經典庫流量分析,未完待續,歡迎掃碼關注公眾號flysnow_org或者網站http://www.flysnow.org/,第一時間看後續系列。覺得有協助的話,順手分享到朋友圈吧,感謝支援。

在Go1.7之前,Go標準庫還沒有內建Context的時候,如果我們想在一個Http.Request裡附加值,怎麼做呢?一般都是Map對象,儲存對應的Request以及附加的值,然後在需要的時候取出來,今天我們介紹的這個就是實現了一個類似於這樣功能的庫,因為比較簡單,而且實用,所以就先選擇它來分析。

安裝

要使用這個庫,需要先安裝,在Go裡,任何庫的安裝都是一樣的,那就是通過go get。

1
$ go get github.com/gorilla/context

安裝之後,我們就可以使用了,下面來看一個儲存資料,取出資料的例子

123456789101112131415161718192021222324252627282930
package mainimport ("github.com/gorilla/context""net/http""strconv")func main() {//啟動一個Web服務http.Handle("/",http.HandlerFunc(myHander))http.ListenAndServe(":1234",nil)}//定義一個Handerfunc myHander(rw http.ResponseWriter, r *http.Request) {//類比為Request附加值,這裡附加了2個context.Set(r,"user","張三")context.Set(r,"age",18)//這個類比一個方法或者函數的調用,大部分情況下可能不在一個包裡doHander(rw ,r)}func doHander(rw http.ResponseWriter, r *http.Request) {//我們從這個Request裡取出對應的值。user:=context.Get(r,"user").(string)age:=context.Get(r,"age").(int)rw.WriteHeader(http.StatusOK)rw.Write([]byte("the user is "+user+",age is "+strconv.Itoa(age)))}

資料如何儲存

上面是一個很簡單的樣本,用過context.Setcontext.Get函數為一個*http.Request附加我們想儲存的索引值對,在需要的時候取出他們。這樣不管我們的*http.Request被傳遞到哪裡去,都可以獲得我們儲存的值,為我們為值得傳遞提供了便利。

123456789
func Set(r *http.Request, key, val interface{}) {mutex.Lock()if data[r] == nil {data[r] = make(map[interface{}]interface{})datat[r] = time.Now().Unix()}data[r][key] = valmutex.Unlock()}

Set函數接受三個參數,第一個是*http.Request,第二個是Key,第三個是值,從這三個參數看,我們可以為一個*http.Request儲存多個值。

值儲存在什麼地方呢?context庫裡使用的是map對象裡。

123
var (data  = make(map[*http.Request]map[interface{}]interface{}))

data定義的是一個雙層嵌套的map,第一層Key為*http.Request,Value為map[interface{}]interface{};第二層的map的key和value都是interface{},意味著我們可以儲存任何職。

在上面的Set函數中,先通過data[r] == nil判斷該Request對應的儲存資料的map是否存在,如果沒有的話,先建立該map:

1
data[r] = make(map[interface{}]interface{})

建立好了之後,就可以為這個map附加值了。通過data[r][key] = val進行賦值,就完成了一次儲存。這裡使用的是map,如果儲存的時候,已經有了舊值,舊的值會被新的覆蓋。

擷取儲存的值

現在我們知道,儲存值得對象其實是個map,所以擷取儲存的值也比較簡單了,像操作map一樣擷取值即可,現在看下context為我們提供的擷取值得函數代碼。

12345678910
func Get(r *http.Request, key interface{}) interface{} {mutex.RLock()if ctx := data[r]; ctx != nil {value := ctx[key]mutex.RUnlock()return value}mutex.RUnlock()return nil}

原始碼邏輯上做了一些判斷,存在就傳回值,不存在就返回nil。

判斷儲存的Key是否存在

有時候我們需要判斷我們儲存的Key是否存在,但是我們不能通過返回的值是nil來判斷,因為我們也可以為一個key設定一個nil的值,這是可行的,並且是存在的,所以我們不能使用這種方式來判斷。

context庫為我們特意提供了GetOk函數來判斷一個Key是否存在。

12345678910
func GetOk(r *http.Request, key interface{}) (interface{}, bool) {mutex.RLock()if _, ok := data[r]; ok {value, ok := data[r][key]mutex.RUnlock()return value, ok}mutex.RUnlock()return nil, false}

從上面的原始碼可以看出,它返回兩個值,第一個是Key對應的值,第二個表示該key是否存在。如果Key不存在,則對應的傳回值為nil,false

擷取儲存的所有索引值對

如果我們想擷取一個Reuqest上儲存的所有索引值對,我們可以使用context庫提供的GetAll函數,它返回一個map對象,包含該Request上儲存的所有索引值對,現在我們使用該函數重寫上面的樣本。

123456789
func doHander(rw http.ResponseWriter, r *http.Request) {//我們從這個Request裡取出對應的值。allParams:=context.GetAll(r)user:=allParams["user"].(string)age:=allParams["age"].(int)rw.WriteHeader(http.StatusOK)rw.Write([]byte("the user is "+user+",age is "+strconv.Itoa(age)))}

先擷取所有索引值對,然後使用map操作的方式,擷取對應key的值。

12345678910111213
func GetAll(r *http.Request) map[interface{}]interface{} {mutex.RLock()if context, ok := data[r]; ok {result := make(map[interface{}]interface{}, len(context))for k, v := range context {result[k] = v}mutex.RUnlock()return result}mutex.RUnlock()return nil}

GetAll函數的原始碼邏輯很簡單,但是這裡有幾個技巧,也是亮點。

我們根據data的儲存結構知道,data[r]取出的就是一個map[interface{}]interface{},但是為什麼要for迴圈一遍,返回一個新建立的map呢。這種做法是完全正確的,因為map是一個參考型別,如果我們直接返回了儲存的map,調用者就可能會對這個map進行修改,破壞了map的儲存,所以必須要返回一個map的拷貝,這是技巧亮點一

第二個亮點是map拷貝的時候,建立的拷貝map一定要指定大小,並且大小和原map一樣,這樣做的好處是map不用進行自動擴充,可以提高效能,result := make(map[interface{}]interface{}, len(context))

刪除和清理

當我們附加的索引值對不需要的時候,我們可以及時的把他們刪除掉,這樣可以釋放記憶體,提高效能。context包提供了Delete函數刪除指定的Key,還提供了Clear函數刪除一個*http.Request上所有的索引值對。使用方法都很簡單,這裡不再舉例。

123
context.Delete(r,key)context.Clear(r)

他們的函數實現原理都是對map的刪除操作,因為資料存放區本質上是一個map。

123456789101112131415161718
func Delete(r *http.Request, key interface{}) {mutex.Lock()if data[r] != nil {delete(data[r], key)}mutex.Unlock()}func Clear(r *http.Request) {mutex.Lock()clear(r)mutex.Unlock()}func clear(r *http.Request) {delete(data, r)delete(datat, r)}

儲存的生命週期

context儲存索引值對是有生命週期的,每個Request對應的儲存map被建立的時候,都會記錄該索引值對設定的時間,這個時間是指該Request上所有索引值對的時間,而不單單是哪一個索引值對的時間。

123456789
func Set(r *http.Request, key, val interface{}) {mutex.Lock()if data[r] == nil {data[r] = make(map[interface{}]interface{})datat[r] = time.Now().Unix()}data[r][key] = valmutex.Unlock()}

從上面的原始碼可以看到,當一個request第一次被附加值的時候,記錄該request對應的map的建立時間,儲存在datat這個map中。

123
var (datat = make(map[*http.Request]int64))

有了這個時間,我們就知道這個request對應的map索引值對被建立了多長時間,我們就可以清除被建立太久的索引值對,這個函數就是content.Purge,他可以保留最近maxAge秒的索引值對,超過這個時間的,都會被清理掉,然後返回清理的request個數。

12345678910111213141516171819
func Purge(maxAge int) int {mutex.Lock()count := 0if maxAge <= 0 {count = len(data)data = make(map[*http.Request]map[interface{}]interface{})datat = make(map[*http.Request]int64)} else {min := time.Now().Unix() - int64(maxAge)for r := range data {if datat[r] < min {clear(r)count++}}}mutex.Unlock()return count}

從原始碼中可以看到,如果我們傳遞 <=0 的值,那麼會把所有request上儲存的索引值對全部刪除掉。如果maxAge是 >0 的值,那麼就會通過time.Now().Unix() - int64(maxAge)算出一個最小的時間點min,在這個時間之前建立的request對應的map索引值對,都會被清理掉。

這個函數有很多應用情境,比如只保留最近一天在request上附加的值,超過一天就刪除了,可以做一些周期性的任務工作。

多goroutine安全

差不多快結尾了,從上面的程式碼分析中,可以看出,資料庫的儲存都是在map中,這個map本身在多goroutine中是不安全的,所以我們保證它們的安全,context裡所有函數的實現,都是用了讀寫鎖,這樣即可以提高讀的效率,又可以保證寫的安全。

123
var (mutex sync.RWMutex)

可以留意,上面我們分析的幾個函數原始碼裡都有,像Get函數,只用讀鎖,提高效能;Set等修改刪除清理方法,都是寫鎖,保證資料安全。

自動清理儲存的索引值對

有時候,我們附加在一個*http.Request上的索引值對,只用一次,也就是這些索引值對的生命週期,只有這次請求,用完就清理,如果是簡單的請求處理鏈,我們知道哪一個處理是最後一步,執行完調用context.Clear函數清理即可。

但是大部分時候我們都不知道哪段處理代碼是最後一步,而且代碼因為業務經常改動,可能又增加了一個函數,到時候忘記了調用清理,或者提前清理,都達不到我們的目的。

為了,context為我們提供了ClearHandler函數,只需要把我們的Handler封裝一下, 這個新的Hander就具有了自動清楚該Request上附加索引值對的能力。

123456
func ClearHandler(h http.Handler) http.Handler {return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {defer Clear(r)h.ServeHTTP(w, r)})}

對一個Handler封裝,返回的還是一個Handler,不影響前台的調用。這裡留意defer Clear(r),不管這次請求的處理鏈有多長,代碼都多少,都可以不管,最終請求處理完,清理儲存的索引值對就是,簡單吧,也是一種技巧。剛剛例子中,就可以換成如下這種寫法,達到自動清理的目的。

123
http.Handle("/",http.HandlerFunc(myHander))//換成http.Handle("/",context.ClearHandler(http.HandlerFunc(myHander)))

新的替代者

自動Go1.7引入了context之後,這個庫也停止維護了,因為標準庫的context,完全可以替代他,滿足我們的需求。

123456789101112131415161718192021
//定義一個Handerfunc myHander(rw http.ResponseWriter, r *http.Request) {//類比為Request附加值,這裡附加了2個userContext:=context.WithValue(context.Background(),"user","張三")ageContext:=context.WithValue(userContext,"age",18)rContext:=r.WithContext(ageContext)//這個類比一個方法或者函數的調用,大部分情況下可能不在一個包裡doHander(rw ,rContext)}func doHander(rw http.ResponseWriter, r *http.Request) {//我們從這個Request裡取出對應的值。user:=r.Context().Value("user").(string)age:=r.Context().Value("age").(int)rw.WriteHeader(http.StatusOK)rw.Write([]byte("the user is "+user+",age is "+strconv.Itoa(age)))}

這是使用Go標準庫裡的context包重寫的,和我們前面的例子完全等價。這個主要在於,我們可以使用*Request.WithContext函數,產生一個帶有Context的*Request,這樣儲存有索引值對的Context就跟著*Request一起傳遞了,不管到哪裡,都可以通過*Request.Context函數擷取附加在*Request上的Context,進而擷取Context上儲存的索引值對。

1234567891011121314151617
func (r *Request) WithContext(ctx context.Context) *Request {if ctx == nil {panic("nil context")}r2 := new(Request)*r2 = *rr2.ctx = ctxreturn r2}func (r *Request) Context() context.Context {if r.ctx != nil {return r.ctx}return context.Background()}

小結

到這裡,context庫的分析結束了,這裡可以學到的是一個簡單的函數庫的設計,map的複製效能,map參考型別的注意事項,以及多goroutine下資料讀寫的安全。

Go語言經典庫流量分析,未完待續,歡迎掃碼關注公眾號flysnow_org或者網站http://www.flysnow.org/,第一時間看後續系列。覺得有協助的話,順手分享到朋友圈吧,感謝支援。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.