Java程式設計師學習Go指南(終)
我的部落格:https://www.luozhiyun.com/archives/215
context.Context型別
Context型別可以提供一類代表上下文的值。此類值是併發安全的,也就是說它可以被傳播給多個 goroutine。
Context型別的值(以下簡稱Context值)是可以繁衍的,這意味著我們可以通過一個Context值產生出任意個子值。這些子值可以攜帶其父值的屬性和資料,也可以響應我們通過其父值傳達的訊號。
context包中還包含了四個用於繁衍Context值的函式,即:WithCancel、WithDeadline、WithTimeout和WithValue。
所有的Context值共同構成了一顆代表了上下文全貌的樹形結構。通過呼叫context.Background函式就可以得到上下文根節點,然後通過根節點可以產生子節點。如下:
rootNode := context.Background()
node1, cancelFunc1 := context.WithCancel(rootNode)
在上面的例子中,初始化了一個撤銷節點,這個節點是可以給它所有子節點發送撤銷訊號的,如下:
cxt, cancelFunc := context.WithCancel(context.Background())
//傳送撤銷訊號
cancelFunc()
//接受撤銷訊號
<-cxt.Done()
在撤銷函式被呼叫之後,對應的Context值會先關閉它內部的接收通道,也就是它的Done方法會返回的那個通道。
然後,它會向它的所有子值(或者說子節點)傳達撤銷訊號。這些子值會如法炮製,把撤銷訊號繼續傳播下去。最後,這個Context值會斷開它與其父值之間的關聯。
WithValue攜帶資料
WithValue函式在產生新的Context值(以下簡稱含資料的Context值)的時候需要三個引數,即:父值、鍵和值。
在我們呼叫含資料的Context值的Value方法時,它會先判斷給定的鍵,是否與當前值中儲存的鍵相等,如果相等就把該值中儲存的值直接返回,否則就到其父值中繼續查詢。
如:
node2 := context.WithValue(node1, 20, values[0]) node3 := context.WithValue(node2, 30, values[1]) fmt.Printf("The value of the key %v found in the node3: %v\n", keys[0], node3.Value(keys[0])) fmt.Printf("The value of the key %v found in the node3: %v\n", keys[1], node3.Value(keys[1])) fmt.Printf("The value of the key %v found in the node3: %v\n", keys[2], node3.Value(keys[2])) fmt.Println()
最後,提醒一下,Context介面並沒有提供改變資料的方法。
物件池sync.Pool
sync.Pool型別只有兩個方法——Put和Get。Put 用於在當前的池中存放臨時物件,它接受一個interface{}型別的引數;Get方法可能會從當前的池中刪除掉任何一個值,然後把這個值作為結果返回。如果沒有那麼會使用當前池的New欄位建立一個新值,並直接將其返回。
如下:
var ppFree = sync.Pool{
New: func() interface{} { return new(pp) },
}
Go 語言執行時系統中的垃圾回收器,所以在每次開始執行之前,都會對所有已建立的臨時物件池中的值進行全面地清除。
臨時物件池資料結構
在臨時物件池中,有一個多層的資料結構。這個資料結構的頂層,我們可以稱之為本地池列表。
在本地池列表中的每個本地池都包含了三個欄位(或者說元件),它們是:儲存私有臨時物件的欄位private、代表了共享臨時物件列表的欄位shared,以及一個sync.Mutex型別的嵌入欄位。
臨時物件池的Put方法總會先試圖把新的臨時物件,儲存到對應的本地池的private欄位中,只有當這個private欄位已經存有某個值時,該方法才會去訪問本地池的shared欄位。
Put方法會在互斥鎖的保護下,把新的臨時物件追加到共享臨時物件列表的末尾。
臨時物件池的Get方法,總會先試圖從對應的本地池的private欄位處獲取一個臨時物件。只有當這個private欄位的值為nil時,它才會去訪問本地池的shared欄位。
Get方法也會在互斥鎖的保護下,試圖把該共享臨時物件列表中的最後一個元素值取出並作為結果。
併發安全字典sync.Map
鍵的實際型別不能是函式型別、字典型別和切片型別。由於這些鍵值的實際型別只有在程式執行期間才能夠確定,所以 Go 語言編譯器是無法在編譯期對它們進行檢查的,不正確的鍵值實際型別肯定會引發 panic。
也是因為Go沒有類似java的泛型,所以我們通常要自己做型別限制,如下:
type IntStrMap struct {
m sync.Map
}
func (iMap *IntStrMap) Delete(key int) {
iMap.m.Delete(key)
}
func (iMap *IntStrMap) Load(key int) (value string, ok bool) {
v, ok := iMap.m.Load(key)
if v != nil {
value = v.(string)
}
return
}
func (iMap *IntStrMap) LoadOrStore(key int, value string) (actual string, loaded bool) {
a, loaded := iMap.m.LoadOrStore(key, value)
actual = a.(string)
return
}
func (iMap *IntStrMap) Range(f func(key int, value string) bool) {
f1 := func(key, value interface{}) bool {
return f(key.(int), value.(string))
}
iMap.m.Range(f1)
}
func (iMap *IntStrMap) Store(key int, value string) {
iMap.m.Store(key, value)
}
在IntStrMap型別的方法簽名中,明確了鍵的型別為int,且值的型別為string。這些方法在接受鍵和值的時候,就不用再做型別檢查了。
或者可以用反射來做型別校驗,如下:
type ConcurrentMap struct {
m sync.Map
keyType reflect.Type
valueType reflect.Type
}
func NewConcurrentMap(keyType, valueType reflect.Type) (*ConcurrentMap, error) {
if keyType == nil {
return nil, errors.New("nil key type")
}
if !keyType.Comparable() {
return nil, fmt.Errorf("incomparable key type: %s", keyType)
}
if valueType == nil {
return nil, errors.New("nil value type")
}
cMap := &ConcurrentMap{
keyType: keyType,
valueType: valueType,
}
return cMap, nil
}
func (cMap *ConcurrentMap) Load(key interface{}) (value interface{}, ok bool) {
if reflect.TypeOf(key) != cMap.keyType {
return
}
return cMap.m.Load(key)
}
func (cMap *ConcurrentMap) Store(key, value interface{}) {
if reflect.TypeOf(key) != cMap.keyType {
panic(fmt.Errorf("wrong key type: %v", reflect.TypeOf(key)))
}
if reflect.TypeOf(value) != cMap.valueType {
panic(fmt.Errorf("wrong value type: %v", reflect.TypeOf(value)))
}
cMap.m.Store(key, value)
}