golangnumin的簡單介紹

本文目錄一覽:

golang反射框架Fx

Fx是一個golang版本的依賴注入框架,它使得golang通過可重用、可組合的模塊化來構建golang應用程序變得非常容易,可直接在項目中添加以下內容即可體驗Fx效果。

Fx是通過使用依賴注入的方式替換了全局通過手動方式來連接不同函數調用的複雜度,也不同於其他的依賴注入方式,Fx能夠像普通golang函數去使用,而不需要通過使用struct標籤或內嵌特定類型。這樣使得Fx能夠在很多go的包中很好的使用。

接下來會提供一些Fx的簡單demo,並說明其中的一些定義。

1、一般步驟

大致的使用步驟就如下。下面會給出一些完整的demo

2、簡單demo

將io.reader與具體實現類關聯起來

輸出:

3、使用struct參數

前面的使用方式一旦需要進行注入的類型過多,可以通過struct參數方式來解決

輸出

如果通過Provide提供構造函數是生成相同類型會有什麼問題?換句話也就是相同類型擁有多個值呢?

下面兩種方式就是來解決這樣的問題。

4、使用struct參數+Name標籤

在Fx未使用Name或Group標籤時不允許存在多個相同類型的構造函數,一旦存在會觸發panic。

輸出

上面通過Name標籤即可完成在Fx容器注入相同類型

5、使用struct參數+Group標籤

使用group標籤同樣也能完成上面的功能

輸出

基本上Fx簡單應用在上面的例子也做了簡單講解

1、Annotated(位於annotated.go文件) 主要用於採用annotated的方式,提供Provide注入類型

源碼中Name和Group兩個字段與前面提到的Name標籤和Group標籤是一樣的,只能選其一使用

2、App(位於app.go文件) 提供注入對象具體的容器、LiftCycle、容器的啟動及停止、類型變量及實現類注入和兩者映射等操作

至於Provide和Populate的源碼相對比較簡單易懂在這裡不在描述

具體源碼

3、Extract(位於extract.go文件)

主要用於在application啟動初始化過程通過依賴注入的方式將容器中的變量值來填充給定的struct,其中target必須是指向struct的指針,並且只能填充可導出的字段(golang只能通過反射修改可導出並且可尋址的字段),Extract將被Populate代替。 具體源碼

4、其他

諸如Populate是用來替換Extract的,而LiftCycle和inout.go涉及內容比較多後續會單獨提供專屬文件說明。

在Fx中提供的構造函數都是惰性調用,可以通過invocations在application啟動來完成一些必要的初始化工作:fx.Invoke(function); 通過也可以按需自定義實現LiftCycle的Hook對應的OnStart和OnStop用來完成手動啟動容器和關閉,來滿足一些自己實際的業務需求。

Fx框架源碼解析

主要包括app.go、lifecycle.go、annotated.go、populate.go、inout.go、shutdown.go、extract.go(可以忽略,了解populate.go)以及輔助的internal中的fxlog、fxreflect、lifecycle

GoLang內建類型 int 佔多少位元組

int is a signed integer type that is at least 32 bits in size. It is a distinct type, however, and not an alias for, say, int32.

int 是帶符號整數類型,其大小至少為32位。 它是一種確切的類型,而不是 int32 的別名。

int 不是int32,那 int 在內存站多少位元組呢?官方沒有明確,讓我們測試下。

GOARCH=”amd64″

package main

import (

“fmt”

“unsafe”

)

func main() {

i := int(1)

fmt.Println(unsafe.Sizeof(i)) // 4

j := 1

fmt.Println(unsafe.Sizeof(j)) // 4

u := uint(1)

fmt.Println(unsafe.Sizeof(u)) // 4

}

可以認為 int 是4位元組么?我不敢這樣認為,GoLang支持多種平台架構。如果對size有明確要求,那就用 int32 之類的吧。

補充:隨Go版本的變化,這個的確是在變化,所以到底佔用多少位元組,還是看具體版本吧

golang連續生成隨機數

const NUM int = 100

for i := 0; i NUM; i += 1 {

    rand.Seed(int64(i))

    fmt.Printf(“%d\t”, rand.Int63n(int64(NUM)))

}

其實在循環裏面這點時間間隔,納秒也是跟不上的。

還有,你用sleep的方法肯定是不能接受的!!!

golang unicode/utf8源碼分析

包 utf-8 實現的功能和常量用於文章utf8編碼,包含runes和utf8位元組序列的轉換功能.在unicode中,一個中文佔兩個位元組,utf-8中一個中文佔三個位元組,golang默認的編碼是utf-8編碼,因此默認一個中文佔三個位元組,但是golang中的字符串底層實際上是一個byte數組.

Output:

RuneSelf該值的位元組碼值為128,在判斷是否是常規的ascii碼是使用。hicb位元組碼值為191. FF 的對應的位元組碼為255。

計算字符串中的rune數量,原理:首先取出字符串的碼值,然後判斷是不是個小於128的,如果是小於則直接continue.rune個數++.

如果是個十六進制f1.的則是無效字符,直接continue.rune個數++,也就是說一個無效的字符也當成一個字長為1的rune.如果字符的碼值在first列表中的值和7按位的結果為其字長,比如上面示例中的 鋼 。其字長為三位,第一位的值為 233 .二進制形式為 11101001 ;與7按位與後的值為0.從acceptRanges中取出的結果為{locb, hicb}。也就是標識 ox80 到 0xbf 之間的值。而結果n也就是直接size+3跳過3個位元組後,rune個數++。其他函數的處理流程差不多,不再過多敘述。

示例:

ValidString返回值表明參數字符串是否是一個合法的可utf8編碼的字符串。

RuneCount返回參數中包含的rune數量,第一個例子中將 utf8.RuneCountInString ,改成該方法調用,返回的結果相同。錯誤的和短的被當成一個長一位元組的rune.單個字符 H 就表示一個長度為1位元組的rune.

該函數標識參數是否以一個可編碼的rune開頭,上面的例子中,因為字符串是以一個ascii碼值在0-127內的字符開頭,所以在執行

first[p[0]] 時,取到的是 p[0] 是72,在first列表中,127之前的值都相同都為 0xF0 ,十進制標識為240,與7按位與後值為0,所以,直接返回 true .

和FullRune類似,只是參數為字符串形式

徹底理解Golang Map

本文目錄如下,閱讀本文後,將一網打盡下面Golang Map相關面試題

Go中的map是一個指針,佔用8個位元組,指向hmap結構體; 源碼 src/runtime/map.go 中可以看到map的底層結構

每個map的底層結構是hmap,hmap包含若干個結構為bmap的bucket數組。每個bucket底層都採用鏈表結構。接下來,我們來詳細看下map的結構

bmap 就是我們常說的「桶」,一個桶裏面會最多裝 8 個 key,這些 key 之所以會落入同一個桶,是因為它們經過哈希計算後,哈希結果是「一類」的,關於key的定位我們在map的查詢和插入中詳細說明。在桶內,又會根據 key 計算出來的 hash 值的高 8 位來決定 key 到底落入桶內的哪個位置(一個桶內最多有8個位置)。

bucket內存數據結構可視化如下:

注意到 key 和 value 是各自放在一起的,並不是 key/value/key/value/… 這樣的形式。源碼里說明這樣的好處是在某些情況下可以省略掉 padding字段,節省內存空間。

當 map 的 key 和 value 都不是指針,並且 size 都小於 128 位元組的情況下,會把 bmap 標記為不含指針,這樣可以避免 gc 時掃描整個 hmap。但是,我們看 bmap 其實有一個 overflow 的字段,是指針類型的,破壞了 bmap 不含指針的設想,這時會把 overflow 移動到 extra 字段來。

map是個指針,底層指向hmap,所以是個引用類型

golang 有三個常用的高級類型 slice 、map、channel, 它們都是 引用類型 ,當引用類型作為函數參數時,可能會修改原內容數據。

golang 中沒有引用傳遞,只有值和指針傳遞。所以 map 作為函數實參傳遞時本質上也是值傳遞,只不過因為 map 底層數據結構是通過指針指向實際的元素存儲空間,在被調函數中修改 map,對調用者同樣可見,所以 map 作為函數實參傳遞時表現出了引用傳遞的效果。

因此,傳遞 map 時,如果想修改map的內容而不是map本身,函數形參無需使用指針

map 底層數據結構是通過指針指向實際的元素 存儲空間 ,這種情況下,對其中一個map的更改,會影響到其他map

map 在沒有被修改的情況下,使用 range 多次遍歷 map 時輸出的 key 和 value 的順序可能不同。這是 Go 語言的設計者們有意為之,在每次 range 時的順序被隨機化,旨在提示開發者們,Go 底層實現並不保證 map 遍歷順序穩定,請大家不要依賴 range 遍歷結果順序。

map 本身是無序的,且遍歷時順序還會被隨機化,如果想順序遍歷 map,需要對 map key 先排序,再按照 key 的順序遍歷 map。

map默認是並發不安全的,原因如下:

Go 官方在經過了長時間的討論後,認為 Go map 更應適配典型使用場景(不需要從多個 goroutine 中進行安全訪問),而不是為了小部分情況(並發訪問),導致大部分程序付出加鎖代價(性能),決定了不支持。

場景: 2個協程同時讀和寫,以下程序會出現致命錯誤:fatal error: concurrent map writes

如果想實現map線程安全,有兩種方式:

方式一:使用讀寫鎖 map + sync.RWMutex

方式二:使用golang提供的 sync.Map

sync.map是用讀寫分離實現的,其思想是空間換時間。和map+RWLock的實現方式相比,它做了一些優化:可以無鎖訪問read map,而且會優先操作read map,倘若只操作read map就可以滿足要求(增刪改查遍歷),那就不用去操作write map(它的讀寫都要加鎖),所以在某些特定場景中它發生鎖競爭的頻率會遠遠小於map+RWLock的實現方式。

golang中map是一個kv對集合。底層使用hash table,用鏈表來解決衝突 ,出現衝突時,不是每一個key都申請一個結構通過鏈表串起來,而是以bmap為最小粒度掛載,一個bmap可以放8個kv。在哈希函數的選擇上,會在程序啟動時,檢測 cpu 是否支持 aes,如果支持,則使用 aes hash,否則使用 memhash。

map有3鍾初始化方式,一般通過make方式創建

map的創建通過生成彙編碼可以知道,make創建map時調用的底層函數是 runtime.makemap 。如果你的map初始容量小於等於8會發現走的是 runtime.fastrand 是因為容量小於8時不需要生成多個桶,一個桶的容量就可以滿足

makemap函數會通過 fastrand 創建一個隨機的哈希種子,然後根據傳入的 hint 計算出需要的最小需要的桶的數量,最後再使用 makeBucketArray 創建用於保存桶的數組,這個方法其實就是根據傳入的 B 計算出的需要創建的桶數量在內存中分配一片連續的空間用於存儲數據,在創建桶的過程中還會額外創建一些用於保存溢出數據的桶,數量是 2^(B-4) 個。初始化完成返回hmap指針。

找到一個 B,使得 map 的裝載因子在正常範圍內

Go 語言中讀取 map 有兩種語法:帶 comma 和 不帶 comma。當要查詢的 key 不在 map 里,帶 comma 的用法會返回一個 bool 型變量提示 key 是否在 map 中;而不帶 comma 的語句則會返回一個 value 類型的零值。如果 value 是 int 型就會返回 0,如果 value 是 string 類型,就會返回空字符串。

map的查找通過生成彙編碼可以知道,根據 key 的不同類型,編譯器會將查找函數用更具體的函數替換,以優化效率:

函數首先會檢查 map 的標誌位 flags。如果 flags 的寫標誌位此時被置 1 了,說明有其他協程在執行「寫」操作,進而導致程序 panic。這也說明了 map 對協程是不安全的。

key經過哈希函數計算後,得到的哈希值如下(主流64位機下共 64 個 bit 位):

m: 桶的個數

從buckets 通過 hash m 得到對應的bucket,如果bucket正在擴容,並且沒有擴容完成,則從oldbuckets得到對應的bucket

計算hash所在桶編號:

用上一步哈希值最後的 5 個 bit 位,也就是 01010 ,值為 10,也就是 10 號桶(範圍是0~31號桶)

計算hash所在的槽位:

用上一步哈希值哈希值的高8個bit 位,也就是 10010111 ,轉化為十進制,也就是151,在 10 號 bucket 中尋找** tophash 值(HOB hash)為 151* 的 槽位**,即為key所在位置,找到了 2 號槽位,這樣整個查找過程就結束了。

如果在 bucket 中沒找到,並且 overflow 不為空,還要繼續去 overflow bucket 中尋找,直到找到或是所有的 key 槽位都找遍了,包括所有的 overflow bucket。

通過上面找到了對應的槽位,這裡我們再詳細分析下key/value值是如何獲取的:

bucket 里 key 的起始地址就是 unsafe.Pointer(b)+dataOffset。第 i 個 key 的地址就要在此基礎上跨過 i 個 key 的大小;而我們又知道,value 的地址是在所有 key 之後,因此第 i 個 value 的地址還需要加上所有 key 的偏移。

通過彙編語言可以看到,向 map 中插入或者修改 key,最終調用的是 mapassign 函數。

實際上插入或修改 key 的語法是一樣的,只不過前者操作的 key 在 map 中不存在,而後者操作的 key 存在 map 中。

mapassign 有一個系列的函數,根據 key 類型的不同,編譯器會將其優化為相應的「快速函數」。

我們只用研究最一般的賦值函數 mapassign 。

map的賦值會附帶着map的擴容和遷移,map的擴容只是將底層數組擴大了一倍,並沒有進行數據的轉移,數據的轉移是在擴容後逐步進行的,在遷移的過程中每進行一次賦值(access或者delete)會至少做一次遷移工作。

1.判斷map是否為nil

每一次進行賦值/刪除操作時,只要oldbuckets != nil 則認為正在擴容,會做一次遷移工作,下面會詳細說下遷移過程

根據上面查找過程,查找key所在位置,如果找到則更新,沒找到則找空位插入即可

經過前面迭代尋找動作,若沒有找到可插入的位置,意味着需要擴容進行插入,下面會詳細說下擴容過程

通過彙編語言可以看到,向 map 中刪除 key,最終調用的是 mapdelete 函數

刪除的邏輯相對比較簡單,大多函數在賦值操作中已經用到過,核心還是找到 key 的具體位置。尋找過程都是類似的,在 bucket 中挨個 cell 尋找。找到對應位置後,對 key 或者 value 進行「清零」操作,將 count 值減 1,將對應位置的 tophash 值置成 Empty

再來說觸發 map 擴容的時機:在向 map 插入新 key 的時候,會進行條件檢測,符合下面這 2 個條件,就會觸發擴容:

1、裝載因子超過閾值

源碼里定義的閾值是 6.5 (loadFactorNum/loadFactorDen),是經過測試後取出的一個比較合理的因子

我們知道,每個 bucket 有 8 個空位,在沒有溢出,且所有的桶都裝滿了的情況下,裝載因子算出來的結果是 8。因此當裝載因子超過 6.5 時,表明很多 bucket 都快要裝滿了,查找效率和插入效率都變低了。在這個時候進行擴容是有必要的。

對於條件 1,元素太多,而 bucket 數量太少,很簡單:將 B 加 1,bucket 最大數量( 2^B )直接變成原來 bucket 數量的 2 倍。於是,就有新老 bucket 了。注意,這時候元素都在老 bucket 里,還沒遷移到新的 bucket 來。新 bucket 只是最大數量變為原來最大數量的 2 倍( 2^B * 2 ) 。

2、overflow 的 bucket 數量過多

在裝載因子比較小的情況下,這時候 map 的查找和插入效率也很低,而第 1 點識別不出來這種情況。表面現象就是計算裝載因子的分子比較小,即 map 里元素總數少,但是 bucket 數量多(真實分配的 bucket 數量多,包括大量的 overflow bucket)

不難想像造成這種情況的原因:不停地插入、刪除元素。先插入很多元素,導致創建了很多 bucket,但是裝載因子達不到第 1 點的臨界值,未觸發擴容來緩解這種情況。之後,刪除元素降低元素總數量,再插入很多元素,導致創建很多的 overflow bucket,但就是不會觸發第 1 點的規定,你能拿我怎麼辦?overflow bucket 數量太多,導致 key 會很分散,查找插入效率低得嚇人,因此出台第 2 點規定。這就像是一座空城,房子很多,但是住戶很少,都分散了,找起人來很困難

對於條件 2,其實元素沒那麼多,但是 overflow bucket 數特別多,說明很多 bucket 都沒裝滿。解決辦法就是開闢一個新 bucket 空間,將老 bucket 中的元素移動到新 bucket,使得同一個 bucket 中的 key 排列地更緊密。這樣,原來,在 overflow bucket 中的 key 可以移動到 bucket 中來。結果是節省空間,提高 bucket 利用率,map 的查找和插入效率自然就會提升。

由於 map 擴容需要將原有的 key/value 重新搬遷到新的內存地址,如果有大量的 key/value 需要搬遷,會非常影響性能。因此 Go map 的擴容採取了一種稱為「漸進式」的方式,原有的 key 並不會一次性搬遷完畢,每次最多只會搬遷 2 個 bucket。

上面說的 hashGrow() 函數實際上並沒有真正地「搬遷」,它只是分配好了新的 buckets,並將老的 buckets 掛到了 oldbuckets 字段上。真正搬遷 buckets 的動作在 growWork() 函數中,而調用 growWork() 函數的動作是在 mapassign 和 mapdelete 函數中。也就是插入或修改、刪除 key 的時候,都會嘗試進行搬遷 buckets 的工作。先檢查 oldbuckets 是否搬遷完畢,具體來說就是檢查 oldbuckets 是否為 nil。

如果未遷移完畢,賦值/刪除的時候,擴容完畢後(預分配內存),不會馬上就進行遷移。而是採取 增量擴容 的方式,當有訪問到具體 bukcet 時,才會逐漸的進行遷移(將 oldbucket 遷移到 bucket)

nevacuate 標識的是當前的進度,如果都搬遷完,應該和2^B的長度是一樣的

在evacuate 方法實現是把這個位置對應的bucket,以及其衝突鏈上的數據都轉移到新的buckets上。

轉移的判斷直接通過tophash 就可以,判斷tophash中第一個hash值即可

遍歷的過程,就是按順序遍歷 bucket,同時按順序遍歷 bucket 中的 key。

map遍歷是無序的,如果想實現有序遍歷,可以先對key進行排序

為什麼遍歷 map 是無序的?

如果發生過遷移,key 的位置發生了重大的變化,有些 key 飛上高枝,有些 key 則原地不動。這樣,遍歷 map 的結果就不可能按原來的順序了。

如果就一個寫死的 map,不會向 map 進行插入刪除的操作,按理說每次遍歷這樣的 map 都會返回一個固定順序的 key/value 序列吧。但是 Go 杜絕了這種做法,因為這樣會給新手程序員帶來誤解,以為這是一定會發生的事情,在某些情況下,可能會釀成大錯。

Go 做得更絕,當我們在遍歷 map 時,並不是固定地從 0 號 bucket 開始遍歷,每次都是從一個**隨機值序號的 bucket 開始遍歷,並且是從這個 bucket 的一個 隨機序號的 cell **開始遍歷。這樣,即使你是一個寫死的 map,僅僅只是遍歷它,也不太可能會返回一個固定序列的 key/value 對了。

原創文章,作者:VNAP,如若轉載,請註明出處:https://www.506064.com/zh-hk/n/147723.html

(0)
打賞 微信掃一掃 微信掃一掃 支付寶掃一掃 支付寶掃一掃
VNAP的頭像VNAP
上一篇 2024-11-02 13:12
下一篇 2024-11-02 13:12

相關推薦

  • Python簡單數學計算

    本文將從多個方面介紹Python的簡單數學計算,包括基礎運算符、函數、庫以及實際應用場景。 一、基礎運算符 Python提供了基礎的算術運算符,包括加(+)、減(-)、乘(*)、除…

    編程 2025-04-29
  • Python滿天星代碼:讓編程變得更加簡單

    本文將從多個方面詳細闡述Python滿天星代碼,為大家介紹它的優點以及如何在編程中使用。無論是剛剛接觸編程還是資深程序員,都能從中獲得一定的收穫。 一、簡介 Python滿天星代碼…

    編程 2025-04-29
  • Python海龜代碼簡單畫圖

    本文將介紹如何使用Python的海龜庫進行簡單畫圖,並提供相關示例代碼。 一、基礎用法 使用Python的海龜庫,我們可以控制一個小海龜在窗口中移動,並利用它的「畫筆」在窗口中繪製…

    編程 2025-04-29
  • Python櫻花樹代碼簡單

    本文將對Python櫻花樹代碼進行詳細的闡述和講解,幫助讀者更好地理解該代碼的實現方法。 一、簡介 櫻花樹是一種圖形效果,它的實現方法比較簡單。Python中可以通過turtle這…

    編程 2025-04-28
  • Python大神作品:讓編程變得更加簡單

    Python作為一種高級的解釋性編程語言,一直被廣泛地運用於各個領域,從Web開發、遊戲開發到人工智能,Python都扮演着重要的角色。Python的代碼簡潔明了,易於閱讀和維護,…

    編程 2025-04-28
  • 用Python實現簡單爬蟲程序

    在當今時代,互聯網上的信息量是爆炸式增長的,其中很多信息可以被利用。對於數據分析、數據挖掘或者其他一些需要大量數據的任務,我們可以使用爬蟲技術從各個網站獲取需要的信息。而Pytho…

    編程 2025-04-28
  • 如何製作一個簡單的換裝遊戲

    本文將從以下幾個方面,為大家介紹如何製作一個簡單的換裝遊戲: 1. 遊戲需求和界面設計 2. 使用HTML、CSS和JavaScript開發遊戲 3. 實現遊戲的基本功能:拖拽交互…

    編程 2025-04-27
  • Guava Limiter——限流器的簡單易用

    本文將從多個維度對Guava Limiter進行詳細闡述,介紹其定義、使用方法、工作原理和案例應用等方面,並給出完整的代碼示例,希望能夠幫助讀者更好地了解和使用該庫。 一、定義 G…

    編程 2025-04-27
  • 製作一個簡單的管理系統的成本及實現

    想要製作一個簡單的管理系統,需要進行技術選型、開發、測試等過程,那麼這個過程會花費多少錢呢?我們將從多個方面來闡述製作一個簡單的管理系統的成本及實現。 一、技術選型 當我們開始思考…

    編程 2025-04-27
  • 2的32次方-1:一個看似簡單卻又複雜的數字

    對於計算機領域的人來說,2的32次方-1(也就是十進制下的4294967295)這個數字並不陌生。它經常被用來表示IPv4地址或者無符號32位整數的最大值。但實際上,這個數字卻包含…

    編程 2025-04-27

發表回復

登錄後才能評論