本教程介紹 Go 中多模塊工作區(qū)的基礎知識。使用多模塊工作區(qū),您可以告訴 Go 命令您正在同時在多個模塊中編寫代碼,并輕松地在這些模塊中構建和運行代碼。
成都創(chuàng)新互聯(lián)公司專注于大興安嶺企業(yè)網(wǎng)站建設,自適應網(wǎng)站建設,成都做商城網(wǎng)站。大興安嶺網(wǎng)站建設公司,為大興安嶺等地區(qū)提供建站服務。全流程定制網(wǎng)站設計,專業(yè)設計,全程項目跟蹤,成都創(chuàng)新互聯(lián)公司專業(yè)和態(tài)度為您提供的服務
在本教程中,您將在共享的多模塊工作區(qū)中創(chuàng)建兩個模塊,對這些模塊進行更改,并在構建中查看這些更改的結果。
本教程需要 go1.18 或更高版本。使用go.dev/dl中的鏈接確保您已在 Go 1.18 或更高版本中安裝了 Go 。
首先,為您要編寫的代碼創(chuàng)建一個模塊。
1、打開命令提示符并切換到您的主目錄。
在 Linux 或 Mac 上:
在 Windows 上:
2、在命令提示符下,為您的代碼創(chuàng)建一個名為工作區(qū)的目錄。
3、初始化模塊
我們的示例將創(chuàng)建一個hello依賴于 golang.org/x/example 模塊的新模塊。
創(chuàng)建你好模塊:
使用 . 添加對 golang.org/x/example 模塊的依賴項go get。
在 hello 目錄下創(chuàng)建 hello.go,內容如下:
現(xiàn)在,運行 hello 程序:
在這一步中,我們將創(chuàng)建一個go.work文件來指定模塊的工作區(qū)。
在workspace目錄中,運行:
該go work init命令告訴為包含目錄中模塊的工作空間go創(chuàng)建一個文件 。go.work./hello
該go命令生成一個go.work如下所示的文件:
該go.work文件的語法與go.mod相同。
該go指令告訴 Go 應該使用哪個版本的 Go 來解釋文件。它類似于文件中的go指令go.mod 。
該use指令告訴 Go在進行構建時hello目錄中的模塊應該是主模塊。
所以在模塊的任何子目錄中workspace都會被激活。
2、運行工作區(qū)目錄下的程序
在workspace目錄中,運行:
Go 命令包括工作區(qū)中的所有模塊作為主模塊。這允許我們在模塊中引用一個包,即使在模塊之外。在模塊或工作區(qū)之外運行go run命令會導致錯誤,因為該go命令不知道要使用哪些模塊。
接下來,我們將golang.org/x/example模塊的本地副本添加到工作區(qū)。然后,我們將向stringutil包中添加一個新函數(shù),我們可以使用它來代替Reverse.
在這一步中,我們將下載包含該模塊的 Git 存儲庫的副本golang.org/x/example,將其添加到工作區(qū),然后向其中添加一個我們將從 hello 程序中使用的新函數(shù)。
1、克隆存儲庫
在工作區(qū)目錄中,運行git命令來克隆存儲庫:
2、將模塊添加到工作區(qū)
該go work use命令將一個新模塊添加到 go.work 文件中。它現(xiàn)在看起來像這樣:
該模塊現(xiàn)在包括example.com/hello模塊和 `golang.org/x/example 模塊。
這將允許我們使用我們將在模塊副本中編寫的新代碼,而不是使用命令stringutil下載的模塊緩存中的模塊版本。
3、添加新功能。
我們將向golang.org/x/example/stringutil包中添加一個新函數(shù)以將字符串大寫。
將新文件夾添加到workspace/example/stringutil包含以下內容的目錄:
4、修改hello程序以使用該功能。
修改workspace/hello/hello.go的內容以包含以下內容:
從工作區(qū)目錄,運行
Go 命令在go.work文件指定的hello目錄中查找命令行中指定的example.com/hello模塊 ,同樣使用go.work文件解析導入golang.org/x/example。
go.work可以用來代替添加replace 指令以跨多個模塊工作。
由于這兩個模塊在同一個工作區(qū)中,因此很容易在一個模塊中進行更改并在另一個模塊中使用它。
現(xiàn)在,要正確發(fā)布這些模塊,我們需要發(fā)布golang.org/x/example 模塊,例如在v0.1.0. 這通常通過在模塊的版本控制存儲庫上標記提交來完成。發(fā)布完成后,我們可以增加對 golang.org/x/example模塊的要求hello/go.mod:
這樣,該go命令可以正確解析工作區(qū)之外的模塊。
類型 在變量名后邊
也可不顯式聲明類型, 類型推斷, 但是是靜態(tài)語言, name一開始放字符串就不能再賦值數(shù)字
方法,屬性 分開 方法名首字母大寫就是就是外部可調的
面向對象設計的一個重要原則:“優(yōu)先使用組合而不是繼承”
Dog 也是Animal , 要復用Animal 的屬性和方法,
只需要在結構體 type 里面寫 Animal
入口也是main, 用用試試
多態(tài), 有這個方法就是這個接口的實現(xiàn), 具體的類 不需要知道自己實現(xiàn)了什么接口,
使用: 在一個函數(shù)調用之前加上關鍵字go 就啟動了一個goroutine
創(chuàng)建一個goroutine,它會被加入到一個全局的運行隊列當中,
調度器 會把他們分配給某個 邏輯處理器 的隊列,
一個邏輯處理器 綁定到一個 操作系統(tǒng)線程 ,在上面運行goroutine,
如果goroutine需要讀寫文件, 阻塞 ,就脫離邏輯處理器 直接 goroutine - 系統(tǒng)線程 綁定
編譯成同名.exe 來執(zhí)行, 不通過虛擬機, 直接是機器碼, 和C 一樣, 所以非常快
但是也有自動垃圾回收,每個exe文件當中已經(jīng)包含了一個類似于虛擬機的runtime,進行goroutine的調度
默認是靜態(tài)鏈接的,那個exe會把運行時所需要的所有東西都加進去,這樣就可以把exe復制到任何地方去運行了, 因此 生成的 .exe 文件非常大
本教程介紹了使用 Godatabase/sql及其標準庫中的包訪問關系數(shù)據(jù)庫的基礎知識。
您將使用的database/sql包包括用于連接數(shù)據(jù)庫、執(zhí)行事務、取消正在進行的操作等的類型和函數(shù)。
在本教程中,您將創(chuàng)建一個數(shù)據(jù)庫,然后編寫代碼來訪問該數(shù)據(jù)庫。您的示例項目將是有關老式爵士樂唱片的數(shù)據(jù)存儲庫。
首先,為您要編寫的代碼創(chuàng)建一個文件夾。
1、打開命令提示符并切換到您的主目錄。
在 Linux 或 Mac 上:
在 Windows 上:
2、在命令提示符下,為您的代碼創(chuàng)建一個名為 data-access 的目錄。
3、創(chuàng)建一個模塊,您可以在其中管理將在本教程中添加的依賴項。
運行go mod init命令,為其提供新代碼的模塊路徑。
此命令創(chuàng)建一個 go.mod 文件,您添加的依賴項將在其中列出以供跟蹤。
注意: 在實際開發(fā)中,您會指定一個更符合您自己需求的模塊路徑。有關更多信息,請參閱一下文章。
GO語言(二十五):管理依賴項(上)
GO語言(二十六):管理依賴項(中)
GO語言(二十七):管理依賴項(下)
接下來,您將創(chuàng)建一個數(shù)據(jù)庫。
在此步驟中,您將創(chuàng)建要使用的數(shù)據(jù)庫。您將使用 DBMS 本身的 CLI 創(chuàng)建數(shù)據(jù)庫和表,以及添加數(shù)據(jù)。
您將創(chuàng)建一個數(shù)據(jù)庫,其中包含有關黑膠唱片上的老式爵士樂錄音的數(shù)據(jù)。
這里的代碼使用MySQL CLI,但大多數(shù) DBMS 都有自己的 CLI,具有類似的功能。
1、打開一個新的命令提示符。
在命令行,登錄到您的 DBMS,如下面的 MySQL 示例所示。
2、在mysql命令提示符下,創(chuàng)建一個數(shù)據(jù)庫。
3、切到您剛剛創(chuàng)建的數(shù)據(jù)庫,以便您可以添加表。
4、在文本編輯器的 data-access 文件夾中,創(chuàng)建一個名為 create-tables.sql 的文件來保存用于添加表的 SQL 腳本。
將以下 SQL 代碼粘貼到文件中,然后保存文件。
在此 SQL 代碼中:
(1)刪除名為album表。 首先執(zhí)行此命令可以讓您更輕松地稍后重新運行腳本。
(2)創(chuàng)建一個album包含四列的表:title、artist和price。每行的id值由 DBMS 自動創(chuàng)建。
(3)添加帶有值的四行。
5、在mysql命令提示符下,運行您剛剛創(chuàng)建的腳本。
您將使用以下形式的source命令:
6、在 DBMS 命令提示符處,使用SELECT語句來驗證您是否已成功創(chuàng)建包含數(shù)據(jù)的表。
接下來,您將編寫一些 Go 代碼進行連接,以便進行查詢。
現(xiàn)在你已經(jīng)有了一個包含一些數(shù)據(jù)的數(shù)據(jù)庫,開始你的 Go 代碼。
找到并導入一個數(shù)據(jù)庫驅動程序,該驅動程序會將您通過database/sql包中的函數(shù)發(fā)出的請求轉換為數(shù)據(jù)庫可以理解的請求。
1、在您的瀏覽器中,訪問SQLDrivers wiki 頁面以識別您可以使用的驅動程序。
2、使用頁面上的列表來識別您將使用的驅動程序。為了在本教程中訪問 MySQL,您將使用 Go-MySQL-Driver。
3、請注意驅動程序的包名稱 - 此處為github.com/go-sql-driver/mysql.
4、使用您的文本編輯器,創(chuàng)建一個用于編寫 Go 代碼的文件,并將該文件作為 main.go 保存在您之前創(chuàng)建的數(shù)據(jù)訪問目錄中。
5、進入main.go,粘貼以下代碼導入驅動包。
在此代碼中:
(1)將您的代碼添加到main包中,以便您可以獨立執(zhí)行它。
(2)導入 MySQL 驅動程序github.com/go-sql-driver/mysql。
導入驅動程序后,您將開始編寫代碼以訪問數(shù)據(jù)庫。
現(xiàn)在編寫一些 Go 代碼,讓您使用數(shù)據(jù)庫句柄訪問數(shù)據(jù)庫。
您將使用指向結構的指針sql.DB,它表示對特定數(shù)據(jù)庫的訪問。
編寫代碼
1、進入 main.go,在import您剛剛添加的代碼下方,粘貼以下 Go 代碼以創(chuàng)建數(shù)據(jù)庫句柄。
在此代碼中:
(3)使用 MySQL 驅動程序Config和FormatDSN類型以收集連接屬性并將它們格式化為連接字符串的 DSN。
該Config結構使代碼比連接字符串更容易閱讀。
(4)調用sql.Open 初始化db變量,傳遞 FormatDSN。
(5)檢查來自 的錯誤sql.Open。例如,如果您的數(shù)據(jù)庫連接細節(jié)格式不正確,它可能會失敗。
為了簡化代碼,您調用log.Fatal結束執(zhí)行并將錯誤打印到控制臺。在生產(chǎn)代碼中,您會希望以更優(yōu)雅的方式處理錯誤。
(6)調用DB.Ping以確認連接到數(shù)據(jù)庫有效。在運行時, sql.Open可能不會立即連接,具體取決于驅動程序。您在Ping此處使用以確認 database/sql包可以在需要時連接。
(7)檢查來自Ping的錯誤,以防連接失敗。
(8)Ping如果連接成功,則打印一條消息。
文件的頂部現(xiàn)在應該如下所示:
3、保存 main.go。
1、開始跟蹤 MySQL 驅動程序模塊作為依賴項。
使用go get 添加 github.com/go-sql-driver/mysql 模塊作為您自己模塊的依賴項。使用點參數(shù)表示“獲取當前目錄中代碼的依賴項”。
2、在命令提示符下,設置Go 程序使用的DBUSER和DBPASS環(huán)境變量。
在 Linux 或 Mac 上:
在 Windows 上:
3、在包含 main.go 的目錄中的命令行中,通過鍵入go run來運行代碼。
連接成功了!
接下來,您將查詢一些數(shù)據(jù)。
Goroutine調度是一個很復雜的機制,下面嘗試用簡單的語言描述一下Goroutine調度機制,想要對其有更深入的了解可以去研讀一下源碼。
首先介紹一下GMP什么意思:
G ----------- goroutine: 即Go協(xié)程,每個go關鍵字都會創(chuàng)建一個協(xié)程。
M ---------- thread內核級線程,所有的G都要放在M上才能運行。
P ----------- processor處理器,調度G到M上,其維護了一個隊列,存儲了所有需要它來調度的G。
Goroutine 調度器P和 OS 調度器是通過 M 結合起來的,每個 M 都代表了 1 個內核線程,OS 調度器負責把內核線程分配到 CPU 的核上執(zhí)行
模型圖:
避免頻繁的創(chuàng)建、銷毀線程,而是對線程的復用。
1)work stealing機制
當本線程無可運行的G時,嘗試從其他線程綁定的P偷取G,而不是銷毀線程。
2)hand off機制
當本線程M0因為G0進行系統(tǒng)調用阻塞時,線程釋放綁定的P,把P轉移給其他空閑的線程執(zhí)行。進而某個空閑的M1獲取P,繼續(xù)執(zhí)行P隊列中剩下的G。而M0由于陷入系統(tǒng)調用而進被阻塞,M1接替M0的工作,只要P不空閑,就可以保證充分利用CPU。M1的來源有可能是M的緩存池,也可能是新建的。當G0系統(tǒng)調用結束后,根據(jù)M0是否能獲取到P,將會將G0做不同的處理:
如果有空閑的P,則獲取一個P,繼續(xù)執(zhí)行G0。
如果沒有空閑的P,則將G0放入全局隊列,等待被其他的P調度。然后M0將進入緩存池睡眠。
如下圖
GOMAXPROCS設置P的數(shù)量,最多有GOMAXPROCS個線程分布在多個CPU上同時運行
在Go中一個goroutine最多占用CPU 10ms,防止其他goroutine被餓死。
具體可以去看另一篇文章
【Golang詳解】go語言調度機制 搶占式調度
當創(chuàng)建一個新的G之后優(yōu)先加入本地隊列,如果本地隊列滿了,會將本地隊列的G移動到全局隊列里面,當M執(zhí)行work stealing從其他P偷不到G時,它可以從全局G隊列獲取G。
協(xié)程經(jīng)歷過程
我們創(chuàng)建一個協(xié)程 go func()經(jīng)歷過程如下圖:
說明:
這里有兩個存儲G的隊列,一個是局部調度器P的本地隊列、一個是全局G隊列。新創(chuàng)建的G會先保存在P的本地隊列中,如果P的本地隊列已經(jīng)滿了就會保存在全局的隊列中;處理器本地隊列是一個使用數(shù)組構成的環(huán)形鏈表,它最多可以存儲 256 個待執(zhí)行任務。
G只能運行在M中,一個M必須持有一個P,M與P是1:1的關系。M會從P的本地隊列彈出一個可執(zhí)行狀態(tài)的G來執(zhí)行,如果P的本地隊列為空,就會想其他的MP組合偷取一個可執(zhí)行的G來執(zhí)行;
一個M調度G執(zhí)行的過程是一個循環(huán)機制;會一直從本地隊列或全局隊列中獲取G
上面說到P的個數(shù)默認等于CPU核數(shù),每個M必須持有一個P才可以執(zhí)行G,一般情況下M的個數(shù)會略大于P的個數(shù),這多出來的M將會在G產(chǎn)生系統(tǒng)調用時發(fā)揮作用。類似線程池,Go也提供一個M的池子,需要時從池子中獲取,用完放回池子,不夠用時就再創(chuàng)建一個。
work-stealing調度算法:當M執(zhí)行完了當前P的本地隊列隊列里的所有G后,P也不會就這么在那躺尸啥都不干,它會先嘗試從全局隊列隊列尋找G來執(zhí)行,如果全局隊列為空,它會隨機挑選另外一個P,從它的隊列里中拿走一半的G到自己的隊列中執(zhí)行。
如果一切正常,調度器會以上述的那種方式順暢地運行,但這個世界沒這么美好,總有意外發(fā)生,以下分析goroutine在兩種例外情況下的行為。
Go runtime會在下面的goroutine被阻塞的情況下運行另外一個goroutine:
用戶態(tài)阻塞/喚醒
當goroutine因為channel操作或者network I/O而阻塞時(實際上golang已經(jīng)用netpoller實現(xiàn)了goroutine網(wǎng)絡I/O阻塞不會導致M被阻塞,僅阻塞G,這里僅僅是舉個栗子),對應的G會被放置到某個wait隊列(如channel的waitq),該G的狀態(tài)由_Gruning變?yōu)開Gwaitting,而M會跳過該G嘗試獲取并執(zhí)行下一個G,如果此時沒有可運行的G供M運行,那么M將解綁P,并進入sleep狀態(tài);當阻塞的G被另一端的G2喚醒時(比如channel的可讀/寫通知),G被標記為,嘗試加入G2所在P的runnext(runnext是線程下一個需要執(zhí)行的 Goroutine。), 然后再是P的本地隊列和全局隊列。
系統(tǒng)調用阻塞
當M執(zhí)行某一個G時候如果發(fā)生了阻塞操作,M會阻塞,如果當前有一些G在執(zhí)行,調度器會把這個線程M從P中摘除,然后再創(chuàng)建一個新的操作系統(tǒng)的線程(如果有空閑的線程可用就復用空閑線程)來服務于這個P。當M系統(tǒng)調用結束時候,這個G會嘗試獲取一個空閑的P執(zhí)行,并放入到這個P的本地隊列。如果獲取不到P,那么這個線程M變成休眠狀態(tài), 加入到空閑線程中,然后這個G會被放入全局隊列中。
隊列輪轉
可見每個P維護著一個包含G的隊列,不考慮G進入系統(tǒng)調用或IO操作的情況下,P周期性的將G調度到M中執(zhí)行,執(zhí)行一小段時間,將上下文保存下來,然后將G放到隊列尾部,然后從隊列中重新取出一個G進行調度。
除了每個P維護的G隊列以外,還有一個全局的隊列,每個P會周期性地查看全局隊列中是否有G待運行并將其調度到M中執(zhí)行,全局隊列中G的來源,主要有從系統(tǒng)調用中恢復的G。之所以P會周期性地查看全局隊列,也是為了防止全局隊列中的G被餓死。
除了每個P維護的G隊列以外,還有一個全局的隊列,每個P會周期性地查看全局隊列中是否有G待運行并將其調度到M中執(zhí)行,全局隊列中G的來源,主要有從系統(tǒng)調用中恢復的G。之所以P會周期性地查看全局隊列,也是為了防止全局隊列中的G被餓死。
M0
M0是啟動程序后的編號為0的主線程,這個M對應的實例會在全局變量rutime.m0中,不需要在heap上分配,M0負責執(zhí)行初始化操作和啟動第一個G,在之后M0就和其他的M一樣了
G0
G0是每次啟動一個M都會第一個創(chuàng)建的goroutine,G0僅用于負責調度G,G0不指向任何可執(zhí)行的函數(shù),每個M都會有一個自己的G0,在調度或系統(tǒng)調用時會使用G0的??臻g,全局變量的G0是M0的G0
一個G由于調度被中斷,此后如何恢復?
中斷的時候將寄存器里的棧信息,保存到自己的G對象里面。當再次輪到自己執(zhí)行時,將自己保存的棧信息復制到寄存器里面,這樣就接著上次之后運行了。
我這里只是根據(jù)自己的理解進行了簡單的介紹,想要詳細了解有關GMP的底層原理可以去看Go調度器 G-P-M 模型的設計者的文檔或直接看源碼
參考: ()
()
sync.Map是1.9才推薦的并發(fā)安全的map,除了互斥量以外,還運用了原子操作,所以在這之前,有必要了解下 Go語言——原子操作
go1.10\src\sync\map.go
entry分為三種情況:
從read中讀取key,如果key存在就tryStore。
注意這里開始需要加鎖,因為需要操作dirty。
條目在read中,首先取消標記,然后將條目保存到dirty里。(因為標記的數(shù)據(jù)不在dirty里)
最后原子保存value到條目里面,這里注意read和dirty都有條目。
總結一下Store:
這里可以看到dirty保存了數(shù)據(jù)的修改,除非可以直接原子更新read,繼續(xù)保持read clean。
有了之前的經(jīng)驗,可以猜測下load流程:
與猜測的 區(qū)別 :
由于數(shù)據(jù)保存兩份,所以刪除考慮:
先看第二種情況。加鎖直接刪除dirty數(shù)據(jù)。思考下貌似沒什么問題,本身就是臟數(shù)據(jù)。
第一種和第三種情況唯一的區(qū)別就是條目是否被標記。標記代表刪除,所以直接返回。否則CAS操作置為nil。這里總感覺少點什么,因為條目其實還是存在的,雖然指針nil。
看了一圈貌似沒找到標記的邏輯,因為刪除只是將他變成nil。
之前以為這個邏輯就是簡單的將為標記的條目拷貝給dirty,現(xiàn)在看來大有文章。
p == nil,說明條目已經(jīng)被delete了,CAS將他置為標記刪除。然后這個條目就不會保存在dirty里面。
這里其實就跟miss邏輯串起來了,因為miss達到閾值之后,dirty會全量變成read,也就是說標記刪除在這一步最終刪除。這個還是很巧妙的。
真正的刪除邏輯:
很繞。。。。
GO是編譯性語言,所以函數(shù)的順序是無關緊要的,為了方便閱讀,建議入口函數(shù) main 寫在最前面,其余函數(shù)按照功能需要進行排列
GO的函數(shù) 不支持嵌套,重載和默認參數(shù)
GO的函數(shù) 支持 無需聲明變量,可變長度,多返回值,匿名,閉包等
GO的函數(shù)用 func 來聲明,且左大括號 { 不能另起一行
一個簡單的示例:
輸出為:
參數(shù):可以傳0個或多個值來供自己用
返回:通過用 return 來進行返回
輸出為:
上面就是一個典型的多參數(shù)傳遞與多返回值
對例子的說明:
按值傳遞:是對某個變量進行復制,不能更改原變量的值
引用傳遞:相當于按指針傳遞,可以同時改變原來的值,并且消耗的內存會更少,只有4或8個字節(jié)的消耗
在上例中,返回值 (d int, e int, f int) { 是進行了命名,如果不想命名可以寫成 (int,int,int){ ,返回的結果都是一樣的,但要注意:
當返回了多個值,我們某些變量不想要,或實際用不到,我們可以使用 _ 來補位,例如上例的返回我們可以寫成 d,_,f := test(a,b,c) ,我們不想要中間的返回值,可以以這種形式來舍棄掉
在參數(shù)后面以 變量 ... type 這種形式的,我們就要以判斷出這是一個可變長度的參數(shù)
輸出為:
在上例中, strs ...string 中, strs 的實際值是b,c,d,e,這就是一個最簡單的傳遞可變長度的參數(shù)的例子,更多一些演變的形式,都非常類似
在GO中 defer 關鍵字非常重要,相當于面相對像中的析構函數(shù),也就是在某個函數(shù)執(zhí)行完成后,GO會自動這個;
如果在多層循環(huán)中函數(shù)里,都定義了 defer ,那么它的執(zhí)行順序是先進后出;
當某個函數(shù)出現(xiàn)嚴重錯誤時, defer 也會被調用
輸出為
這是一個最簡單的測試了,當然還有更復雜的調用,比如調試程序時,判斷是哪個函數(shù)出了問題,完全可以根據(jù) defer 打印出來的內容來進行判斷,非常快速,這種留給你們去實現(xiàn)
一個函數(shù)在函數(shù)體內自己調用自己我們稱之為遞歸函數(shù),在做遞歸調用時,經(jīng)常會將內存給占滿,這是非常要注意的,常用的比如,快速排序就是用的遞歸調用
本篇重點介紹了GO函數(shù)(func)的聲明與使用,下一篇將介紹GO的結構 struct