前置:轉Go學習筆記1語法入門
目錄
- Golang進階
- groutine協程并發
- 概念梳理
- 創建goroutine語法
- channel實現goroutine之間通信
- channel與range、select
- GoModules
- Go Modules與GOPATH
- Go Modules模式
- 用Go Modules初始化項目
- 修改模塊的版本依賴關系
- Go Modules 版本號規范
- vendor 模式實踐
Golang進階
groutine協程并發
概念梳理
Go 語言最具代表性的核心特性之一,就是其輕量級用戶態協程——Goroutine。在深入理解 goroutine 之前,我們先回顧一下協程的基本概念以及它們解決的并發痛點。
1.單線程與多線程的限制
(1)單線程模型:在早期單核系統中,計算機只能順序執行單一任務。當遇到 I/O 阻塞時,整個線程只能等待,導致 CPU 空轉,浪費了大量計算資源。
(2)多線程/多進程:為提升 CPU 利用率,引入了多線程/多進程并發模型。通過操作系統的時間片輪轉機制,多個線程/進程在邏輯上“同時”執行,實則在 CPU 核心間快速切換:
- 優點:當一個線程阻塞時,CPU 可以調度其他線程執行,提升總體利用率。
- 缺點:頻繁的上下文切換帶來額外開銷(保存/恢復寄存器狀態、內核態切換、線程棧空間大(通常幾 MB)等),尤其在高并發場景下切換成本呈指數增長,影響整體性能。
2.協程模型演進
為降低多線程模型下的切換和調度開銷,業界引入了用戶態協程(coroutine)模型,其核心思想是將調度邏輯上移到用戶態,避開內核態的頻繁切換。常見調度模型對比:
-
1:1 模型:每個用戶線程綁定一個內核線程,調度仍完全依賴操作系統調度器,無法解決內核態切換開銷。
-
N:1 模型:多個用戶協程復用一個內核線程,切換由用戶態調度器管理,內核無感,極大減少上下文切換開銷。但當某個協程執行系統調用或純阻塞操作時,會阻塞其所在的內核線程,導致所有復用該線程的協程均被阻塞。
-
M:N 模型:M 個內核線程復用 N 個用戶協程,調度邏輯由語言運行時管理,能充分利用多核 CPU 并緩解阻塞問題。Go 語言采用的是 M:N 模型,并通過一套自研的調度器設計高效規避了 N:1 模型的典型阻塞痛點:
-
- P 與 M 解耦:當 goroutine 阻塞時,調度器會將邏輯處理器 P 從阻塞的內核線程 M 中分離,并遷移到其他空閑或新建線程繼續調度其他 goroutine,避免阻塞擴散。
-
- 非阻塞 I/O 封裝:Go 運行時內部將大部分系統調用(如網絡、文件 I/O)封裝為非阻塞模型,結合內置的網絡輪詢器(netpoller)機制,在用戶態實現高效的 I/O 多路復用。
Go 在實現 goroutine 時,不僅更名為 “Goroutine”,更在核心設計上做了優化:
- 內存占用更小:每個 goroutine 棧空間通常只占用幾 KB,且支持按需動態擴展,相比傳統線程動輒幾 MB 的棧空間大幅降低內存壓力,進程甚至可能達到幾GB。
- 調度開銷更低:輕量化特性讓調度器可以頻繁快速地切換執行 goroutine,整體并發性能得到大幅提升。
Go語言早期的調度器設計存在較大問題。如下圖所示:其中 G 表示 Goroutine,M 表示操作系統線程。早期調度器的做法是:假設有一個 4 核 CPU,它維護一個全局隊列,所有新創建的 Goroutine 都被加入到這個全局隊列中。每個線程(M)在執行時,會先獲取全局隊列的鎖,拿到一個 Goroutine 執行。執行后,其余未執行的 Goroutine 會被移動到隊列頭,等待下一個線程調度;執行完成的 Goroutine 會被放回隊列尾部。整個流程簡單粗暴,但存在以下明顯缺陷:
-
激烈的鎖競爭:創建、銷毀和調度 Goroutine 時,所有線程都需獲取全局隊列的鎖,導致頻繁的同步與阻塞,性能大幅下降。
-
任務遷移延遲:如果某個 M 在運行 G 時新創建了 G’,理論上希望 G’ 也在當前 M 上執行以保持數據局部性。但由于使用全局隊列,G’ 可能被其他 M 取走,增加了切換成本,降低了緩存命中率。
-
系統調用頻繁:線程在切換、阻塞與喚醒過程中,頻繁進行系統調用,進一步增加了系統開銷。
3.Go 的 GMP 模型
Go 采納 M:N 模型,并引入了 G(goroutine)、M(machine,內核線程)、P(processor,邏輯處理器) 三元結構──GMP 模型:
-
G:輕量級協程,初始棧大小僅幾 KB,按需動態增長,內存占用極小。
-
M:操作系統線程,真正執行 goroutine 的載體。
-
P:邏輯處理器,調度器核心單元,,持有本地任務隊列(本地 runnable G 列表),決定哪個 G 由哪個 M 執行。P 的數量由
GOMAXPROCS
環境變量決定,最多并行(注意是并行而非并發)運行 P 個協程。
此外,還維護一個全局隊列用于存放溢出的 goroutine,保證負載均衡。新創建的 goroutine 優先放入其所屬 P 的本地隊列,若本地隊列已滿,才會轉移到全局隊列,確保整體調度平衡。全隊列還有一個鎖的保護,所以從全隊列取東西效率會比較慢一些。
4.Go 調度器的關鍵策略
Go調度器的設計包含四大核心策略:線程復用、并行利用、搶占機制、全局G隊列。下面分別說明:
(1)線程復用(Work Stealing與Hand Off機制)
Go通過復用線程提升調度效率,主要依靠Work Stealing與Hand Off兩種機制:
-
Work Stealing(工作竊取)
每個P(Processor)有自己的本地G隊列。當某個M(Machine)空閑時,它會從其他 P 的本地隊列尾部"竊取"任務,充分提升資源利用率與并行度,避免任務堆積或線程空閑。
-
Hand Off(讓渡機制)
當運行中的G發生阻塞(如IO或鎖等待),綁定其所在P的M會嘗試將P遷移給其他可用的M(新建或喚醒線程),繼續執行本地隊列中的其他G任務。阻塞的M進入休眠,待阻塞解除后再參與調度。該機制確保阻塞不會影響其他G的執行,最大化CPU利用率。
(2)并行利用
-
通過設置 GOMAXPROCS 控制 P 數量,合理分配 CPU 資源。
-
比如在 8 核 CPU 下,若將 GOMAXPROCS 設為 4,Go 運行時僅會使用 4 核心資源,剩余可供其他系統任務使用,提供良好的資源隔離能力。
(3)搶占機制
傳統協程調度依賴協程主動讓出CPU,容易導致長時間占用。Go 從 1.14 版本起引入強制搶占機制:每個G最多運行約10ms,無需其主動讓出,調度器可強制將CPU分配給其他等待的G。此設計保證了調度公平性和系統響應性,避免某些G長期獨占CPU。
(4)全局G隊列
在每個P的本地隊列之外,Go還維護一個全局G隊列作為任務緩沖。新創建的G優先進入本地隊列,若本地已滿才進入全局隊列。空閑的M在本地與其他P的隊列均無任務時,最后嘗試從全局隊列取任務。全局隊列的訪問需要加鎖,相比本地隊列性能略低,但作為兜底機制,保障了任務分配的完整性與平衡性。
總結一下Go 調度器的關鍵策略:
- 1.線程復用(Work Stealing & Hand Off)
-
- 工作竊取:當某個 P 的本地隊列空閑時,會從其它 P 竊取可執行的 G,避免某些線程閑置。
-
- P 與 M 分離(Hand Off):當執行中的 G 阻塞(如網絡 I/O),調度器會將對應的 P 從當前 M 分離,掛載到其他空閑或新建的 M 上,保持剩余 G 在本地隊列不中斷執行。
2.并行 通過 GOMAXPROCS 設置 P 的數量,決定最大并行協程數,靈活利用多核 CPU。
3.搶占 Go 從 1.14 起支持協程搶占,當某個 G 占用 CPU 超過一定時間(約 10 ms)或出現函數調用邊界時,可強制調度,避免單個 G 長期占用,保證所有 G 的公平執行。
4.本地與全局隊列 大部分 G 都存放在 P 的本地隊列,只有在本地隊列滿時才會入全局隊列。空閑時優先竊取本地隊列,只有在無其他可用 G 時才訪問全局隊列,降低全局鎖競爭。
小結 —— 為什么 Goroutine 如此高效?
-
低內存開銷:初始棧極小,且支持動態伸縮,百萬級并發成為可能;
-
高效調度:用戶態調度極大減少內核切換次數,整體并發性能遠優于傳統線程;
-
搶占式公平性:保證調度不會被單個 goroutine 長時間壟斷;
-
本地+全局隊列:高效的本地隊列配合全局隊列兜底,確保任務平衡與快速分發;
-
I/O 封裝優化:大部分阻塞 I/O 在用戶態實現了非阻塞封裝,極大緩解系統調用瓶頸。
創建goroutine語法
如下代碼所示,通過go
關鍵字創造goroutine
package mainimport ("fmt""time"
)// 一個用于演示的子goroutine任務函數,不斷地每秒打印當前計數值。
func newTask() {i := 0for {i++fmt.Printf("new Goroutine : i = %d\n", i) // 其中 %d 表示格式化為十進制整數time.Sleep(1 * time.Second) // // 通過 time.Sleep 讓當前 goroutine 休眠 1 秒鐘}
}// main 函數是 Go 程序的入口函數,同時它本身就是一個 goroutine(稱為主 goroutine)
func main() {// 通過 go 關鍵字創建一個新的 goroutine,去異步執行 newTask() 函數go newTask()// 此處主 goroutine 繼續往下執行,不會等待 newTask 執行結束fmt.Println("main goroutine exit")i := 0for {i++// 主 goroutine 也每秒打印一次當前計數值fmt.Printf("main goroutine: i = %d\n", i)time.Sleep(1 * time.Second)}// 1. 在 Go 語言中,使用 go 關鍵字可以在運行時動態創建新的 goroutine(輕量級線程)。// Go 運行時會負責調度多個 goroutine,通常在一個或多個操作系統線程上并發執行。//// 2. 主 goroutine 退出時,整個進程隨之結束,所有其他子 goroutine 無論是否完成都會被強制終止。// 因此,如果將上面的 for 循環注釋掉,僅執行 fmt.Println 后主函數直接退出,// 那么子 goroutine newTask 也無法執行或只執行極短時間后被終止。//// 3. 在實際項目中,如果希望主 goroutine 等待其他 goroutine 執行結束,可以使用 sync.WaitGroup、// channel 或 context 等機制來實現 goroutine 之間的同步與協調。
}
實際上在承載一個go程的時候不一定要把go程寫為一個定義好的函數,我們直接寫一個匿名函數去加載也可以,這里演示一下:
package mainimport ("fmt""runtime""time"
)// 本示例主要演示了在 Go 語言中:
// 1. 使用匿名函數(函數字面量)直接創建 goroutine;
// 2. 使用 runtime.Goexit() 退出當前 goroutine;
// 3. 說明 goroutine 函數中無法直接返回值給調用者。func main() {// 使用 go 關鍵字創建 goroutine,并在其中定義并調用匿名函數(沒有參數和返回值)go func() {defer fmt.Println("A.defer") // 延遲執行,在當前匿名函數退出時執行// 內層匿名函數func() {defer fmt.Println("B.defer") // 延遲執行,在當前匿名函數退出時執行// 如何在go程中退出當前goroutine? 用runtime.Goexit()// runtime.Goexit() 用于立即終止當前 goroutine 的執行。// 注意:它只終止當前 goroutine,不會影響其他 goroutine,包括主 goroutine。// 此外,它在退出時仍會調用所有已注冊的 defer 函數(類似于正常退出時的清理邏輯)。// 因此 "B.defer" 會被打印,而 "B" 不會被打印。// 注意如果這里是用return的話 只是退出了當前函數調用棧幀 "A"仍會被打印runtime.Goexit()// 由于上面調用了 Goexit(),所以下面這句不會被執行:fmt.Println("B")}() // 如果只是寫這個函數,就只是定義了但沒被調用,加個()等于我定義了這么一個函數,同時調用起來// 調用時我們沒有傳遞任何參數,因為這里的函數定義就沒有任何參數// 由于外層 goroutine 也被 Goexit() 終止了,因此這句也不會被執行:fmt.Println("A")// runtime.Goexit() 并不是像 return 那樣只退出當前函數調用棧幀,// 它直接終止整個當前 goroutine,跳出所有調用棧,當然 defer 仍然會執行。}()// 使用匿名函數創建并立即調用帶參數的 goroutinego func(a int, b int) bool {fmt.Println("a = ", a, ", b = ", b)return true}(10, 20) // 這里匿名函數定義后立刻通過()調用,并傳入參數 10 和 20// 即使匿名函數有返回值 (bool),但由于 goroutine 是并發執行的,無法通過 return 直接獲取結果/* 補充說明:- Go 語言中不支持像 flag := go func() bool {...}() 這樣的語法,因為 go 關鍵字啟動的 goroutine 是異步執行的,其返回值不會傳遞回主 goroutine。- goroutine 之間默認無法返回值或傳遞數據,若要實現結果返回或通信,需要借助 channel、sync 包或 context 機制來實現同步與通信。*/// 死循環用于防止 main goroutine 提前退出,確保前面創建的 goroutine 有機會執行完畢for {time.Sleep(1 * time.Second)}
}
在 Go 語言中,main 函數的退出意味著整個程序的結束。所以如果 main 函數提前退出,所有未執行完的子 goroutine 會立即被強制終止。在實際應用中,通常不建議用死循環阻塞主 goroutine,可以使用 sync.WaitGroup
更優雅地等待子 goroutine 結束。這里寫一份goroutine + WaitGroup 基礎通用模板:
package mainimport ("fmt""sync""time"
)// 子任務函數:可以傳參,支持 defer、panic 恢復等
func worker(id int, wg *sync.WaitGroup) {defer wg.Done() // 每啟動一個 goroutine,結束時必須調用 Done()// panic 保護(可選,但建議加上,避免單個 goroutine 崩潰導致全局異常)defer func() {if err := recover(); err != nil {fmt.Printf("Worker %d recovered from panic: %v\n", id, err)}}()fmt.Printf("Worker %d start\n", id)// 模擬任務執行時間time.Sleep(time.Duration(id) * time.Second)fmt.Printf("Worker %d done\n", id)
}func main() {var wg sync.WaitGroupnumWorkers := 5 // 啟動 5 個并發任務for i := 1; i <= numWorkers; i++ {wg.Add(1) // 每個任務啟動前,先增加計數go worker(i, &wg)}// 阻塞等待所有子 goroutine 完成wg.Wait()fmt.Println("所有任務執行完畢,主程序退出")
}
wg.Add(1)
: 每個 goroutine 啟動前,先登記 1 個待完成任務
defer wg.Done()
: 每個 goroutine 執行完后自動減一,防止漏掉
recover()
:捕獲 panic,避免整個程序因某個 goroutine 崩潰
wg.Wait()
: 阻塞主 goroutine,直到所有登記的任務完成
time.Sleep()
: 模擬任務處理時間,實際可替換成任何邏輯
channel實現goroutine之間通信
channel是Go語言中的一個核心數據類型,可以把它看成管道,,主要用來解決go程的同步問題以及go程之間數據共享(數據傳遞)的問題。并發核心單元通過它就可以發送或者接收數據進行通訊,這在一定程度上又進一步降低了編程的難度。
goroutine運行在相同的地址空間,因此訪問共享內存必須做好同步。goroutine 奉行通過通信來共享內存,而不是共享內存來通信。
下面我們學習一下channel的基本用法:
package mainimport "fmt"func main() {// 定義一個 channel,用于傳遞 int 類型的數據。// 這里使用的是無緩沖(unbuffered)channel:只能同時存放一個數據。// 當向無緩沖 channel 發送數據時,發送操作會阻塞直到有其他 goroutine 從 channel 中接收數據。c := make(chan int)// 啟動一個新的 goroutine(協程,相當于一個輕量級線程)。// channel 通常用于多個 goroutine 之間的通信,這里就是 main goroutine 和新開啟的 goroutine 之間的通信。go func() {// 在函數退出時輸出一句話,表明這個 goroutine 結束了defer fmt.Println("goroutine結束")fmt.Println("goroutine 正在運行...")// 向 channel 中發送數據:666// 發送操作:c <- 666// 因為 channel 是無緩沖的,如果 main goroutine 沒有準備好接收數據,發送操作會阻塞在這里c <- 666 //將666發送給c 這個是發送的語法}()// 從 channel 中接收數據:<-c// 這個接收操作會阻塞,直到有數據被發送到 channel 中// 接收到的數據賦值給變量 numnum := <-c //從c中接受數據,并賦值給num 這個是接收的語法// - <-c 是接收操作,把 channel 中的數據取出// - <-c 也可以單獨寫成:<-c 只取出數據而不保存(丟棄)// 例如: <-c // 取出數據但不保存任何變量中,數據被丟棄fmt.Println("num = ", num) // num = 666fmt.Println("main goroutine 結束...")
}
這里因為使用的是無緩沖channel,當向無緩沖 channel 發送數據時,發送操作會阻塞直到有其他 goroutine 從 channel 中接收數據,接收操作會阻塞,直到有數據被發送到 channel 中。
在 Go 語言中,channel 分為無緩沖(unbuffered)和有緩沖(buffered)兩種。
-
無緩沖 channel:
-
發送和接收必須同步進行。
-
發送操作會阻塞,直到有接收者從 channel 中取走數據;接收操作也會阻塞,直到有發送者發送數據。
-
適用于需要確保發送方與接收方同步的場景,常用于協程之間的同步控制。
-
-
有緩沖 channel:
-
在內部有一個有限的緩沖區,可以容納一定數量的元素。
-
發送操作在緩沖未滿時不會阻塞;只有當緩沖區滿時才會阻塞發送方。
-
接收操作在緩沖非空時不會阻塞;只有當緩沖區為空時才會阻塞接收方。
-
適用于發送和接收速度不完全匹配的場景,可以提升一定的并發性能和吞吐能力。
-
-
簡單來說:無緩沖更偏向同步,有緩沖更偏向異步。
下面我們測試一下有緩沖channel的效果:
package mainimport ("fmt""time"
)func main() {// 創建一個帶緩沖區的 channel,類型為 int,緩沖區大小為 3。// 這意味著最多可以緩存 3 個尚未被接收的元素。c := make(chan int, 3)// 打印當前 channel 的長度和容量:// len(c): 當前緩沖區中已有的數據個數(初始為 0)// cap(c): 緩沖區總容量(此處為 3)fmt.Println("len(c) = ", len(c), ", cap(c)", cap(c)) // 輸出: len(c) = 0 , cap(c) = 3// 啟動一個新的 goroutine 來向 channel 中發送數據go func() {defer fmt.Println("子go程結束") // 在函數結束時自動打印,標記子 goroutine 結束// 循環向 channel 中發送 4 個整數(注意:發送次數 > 緩沖區容量)for i := 0; i < 4; i++ {c <- i // 發送數據到 channelfmt.Println("子go程正在運行, 發送的元素=", i, " len(c)=", len(c), ", cap(c)=", cap(c))}}()// 主 goroutine 休眠 2 秒,確保子 goroutine 有時間執行發送操作// 這只是為了演示方便,實際中應使用同步機制(如 wait group)time.Sleep(2 * time.Second)// 從 channel 中依次取出 4 個元素(注意:實際發送了 4 個元素)for i := 0; i < 4; i++ {num := <-c //從c中接收數據,并賦值給numfmt.Println("num = ", num)}fmt.Println("main 結束")
}
運行結果為:
len? = 0 , cap? 3
子go程正在運行, 發送的元素= 0 len?= 1 , cap?= 3
子go程正在運行, 發送的元素= 1 len?= 2 , cap?= 3
子go程正在運行, 發送的元素= 2 len?= 3 , cap?= 3
num = 0
num = 1
num = 2
num = 3
main 結束
一開始 len? 是 0,因為還沒有任何數據發送到 channel。
子 goroutine 發送前 3 個元素時:因為緩沖區容量為 3,每次發送成功后,緩沖區長度 len? 依次變為 1、2、3。此時發送都是非阻塞的(因為緩沖區未滿)。
當嘗試發送第 4 個元素(i=3)時:緩沖區已滿,發送操作阻塞,直到主 goroutine 從 channel 中讀取數據,騰出空間。由于主 goroutine 在 time.Sleep 中睡眠,子 goroutine 此時會卡在 c <- i 第 4 次發送這里,等待空間騰出。
睡眠結束后,主 goroutine 依次從 channel 中讀取 4 個數據:前 3 個立即取出緩沖區中的數據(0、1、2)。取出第 3 個數據時,緩沖區變為不滿,子 goroutine 解除阻塞,成功發送最后一個元素 3。主 goroutine 繼續取出最后一個數據 3。
所有數據接收完成后,程序結束。
介紹了有緩沖和無緩沖channel的基本定義與使用后,我們再來看看channel的關閉特點:
package mainimport "fmt"// 在Go語言中,channel不像文件那樣需要頻繁關閉;通常只有以下兩種情況需要關閉:
// 1. 確定不再向channel發送任何數據了(即:發送方完成了全部發送任務)。
// 2. 想要通過關閉channel通知接收方,配合range、for-select等結構優雅退出。
// 注意:關閉channel只是禁止繼續發送數據(引發panic錯誤后導致接收立即返回零值);
// 而接收數據仍然是允許的,直到channel被完全讀空。
// 另外:nil channel(值為nil的channel)在收發操作時都會永久阻塞。
func main() {c := make(chan int) // 創建一個無緩沖的整型channel,類型為chan intgo func() { // 啟動一個匿名goroutine作為發送者for i := 0; i < 5; i++ { // 向channel中發送5個整數:0到4c <- i // 向channel發送數據,若沒有接收方則會阻塞//close(c) // 注意:如果在這里關閉channel,將在第一次發送后關閉,再發送時panic!}//close可以關閉一個channelclose(c) // 循環發送完所有數據后,關閉channel,通知接收方:不會再有新的數據發送進來了}()for { // 啟動主goroutine作為接收者// 這里使用了逗號ok的慣用寫法:data接收從channel讀取的數據// ok為布爾值,若channel未關閉或還有數據,ok為true;當channel關閉且數據讀完后,ok返回falseif data, ok := <-c; ok { // channel仍然有數據可以讀取fmt.Println(data)} else { // channel已關閉且數據讀完,退出循環break}}fmt.Println("Main Finished..")// 如果不在子程里調用close(c) 或不在子goroutine里發送數據 // 如果不在子goroutine里發送數據,而直接在主goroutine中執行接收// 由于主goroutine會阻塞在 <-c ,而沒有其他goroutine發送數據,最終會導致:// fatal error: all goroutines are asleep - deadlock// 這是因為Go運行時檢測到了所有goroutine都阻塞,程序無法繼續執行,因此直接panic報死鎖。
}
這里
if data, ok := <-c; ok {
里面有個分號,這是 Go 語言里 “if 語句支持短變量聲明” 的語法,在 Go 里,if 語句可以有兩部分:if 簡短變量聲明; 條件判斷 {// ... }
也就是說:分號 ; 把變量聲明和條件判斷隔開。if 語句執行時,先執行分號前面的短變量聲明(這里是 `data, ok := <-c`),然后判斷分號后面的條件(這里是 `ok`)。這句代碼拆開理解就是:data, ok := <-c // 從channel接收數據,同時判斷channel是否已關閉 if ok {fmt.Println(data) }
但是因為 Go 允許你把聲明寫在 if 里,就可以縮寫成一行
channel與range、select
下面我們再看一下channel跟兩個比較特殊的關鍵字的配合使用
channel與range
package mainimport "fmt"func main() {c := make(chan int) // 創建一個無緩沖的整型channel,類型為chan intgo func() { // 啟動一個匿名goroutine作為發送者for i := 0; i < 5; i++ { // 向channel中連續發送5個整數:0到4c <- i // 發送數據到channel,若無接收方會阻塞等待}// 發送完所有數據后,關閉channel,關閉channel的作用是通知接收方:不會再有新的數據了close(c)}()// =================== 之前寫法(手動 for + ok 檢查) ===================/* for { // 啟動主goroutine作為接收者// 這里使用了逗號ok的慣用寫法:data接收從channel讀取的數據// ok為布爾值,若channel未關閉或還有數據,ok為true;當channel關閉且數據讀完后,ok返回falseif data, ok := <-c; ok { // channel仍然有數據可以讀取fmt.Println(data)} else { // channel已關閉且數據讀完,退出循環break}}*/// =================== 更簡潔的寫法:使用range迭代channel ===================// 使用range可以自動從channel中不斷接收數據,直到channel被關閉且數據讀空后自動退出// 注意:只有關閉了channel,range才能正常結束,否則會一直阻塞等待新數據for data := range c {fmt.Println(data)}// 本質上兩種代碼邏輯一樣,但寫法不同。fmt.Println("Main Finished..")// 總結:// 1. for + ok 寫法:更通用,能靈活處理接收結果、區分接收失敗(例如關閉時返回零值和ok=false)// 2. range 寫法:語法更簡潔,適用于簡單讀取全部channel數據直到關閉// 3. 不管哪種寫法,關閉channel后都無法再向其中發送數據,否則panic// 4. 未關閉channel時,range會一直阻塞等待,容易導致程序卡死(死鎖)
}
channel與select
單流程下一個go只能監控一個channel的狀態,select可以完成監控多個channel的狀態:
package mainimport "fmt"// 定義一個生成斐波那契數列的函數,使用channel與select控制流程
func fibonacii(c, quit chan int) {x, y := 1, 1 // 斐波那契數列的前兩個數for {select {// select語句可以同時監聽多個channel的通信狀態// 當某個case對應的channel準備好后(發送/接收不再阻塞),select就會執行對應的casecase c <- x:// 當c可寫時(即:有人在接收c的數據時),就會進入這個case// 把當前的x發送到channel c中// 然后計算下一個斐波那契數x = yy = x + ycase <-quit:// 當從quit channel中接收到數據時(不關心數據內容,所以直接用<-quit)// 表示收到停止信號,打印"quit",退出函數fmt.Println("quit")return // return,當前goroutine結束}}
}func main() {// 創建兩個無緩沖channel:// c 用于傳遞斐波那契數列數據// quit 用于通知fibonacci函數何時退出c := make(chan int)quit := make(chan int)// 啟動一個子goroutine負責消費fibonacci生成的數列數據go func() {for i := 0; i < 10; i++ {// 每次從c中接收一個數據并打印fmt.Println(<-c)}// 接收完10個數據后,通知fibonacci函數可以停止了quit <- 0}()// 主goroutine調用fibonacci函數,開始生成數據// 注意:該函數內是一個無限循環,直到收到quit信號才會退出fibonacii(c, quit)
}
用你更熟悉的
Java switch
來對比著幫你徹底講清楚:
一句話總結:Go 的select
每次執行時,先掃描所有 case 中的 channel,如果有一個或多個可以立即執行的,就隨機選擇其中一個執行(注意:真的隨機
,不是順序!);一旦選定執行一個 case,本輪 select 立即結束,不會執行其他 case。如果沒有任何 case 滿足條件:如果有default
,則直接執行 default;如果沒有 default,則整個 select 阻塞等待,直到至少有一個 case 滿足條件。注意:只在所有case都無法執行時才會進入default。
每次 select 執行一輪:
+-----------------------------+
| 檢查每個 case 是否 ready |
+-----------------------------+↓有多個ready? ——→ 是 ——→ 隨機選1個執行↓否↓是否有default? ——→ 有 ——→ 執行default↓沒有↓阻塞等待
補充一點底層:
Go select 底層其實和調度器有關:Go runtime 會維護一個 goroutine 等待隊列;
每當執行 select,實際上在 runtime 層面做了一次channel 狀態 polling(檢測收發是否能立即完成);
只要有任意一個 channel ready,就從 ready set 里隨機取一個執行;
所以它既像“非阻塞的多路復用器”,也像是輕量的“并發調度器”——這也是為什么 Go select 很適合用來做高性能并發通信控制的原因。
GoModules
Go Modules與GOPATH
1.什么是Go Modules?
Go modules 是 Go 語言官方推薦的依賴管理工具,自 Go 1.11 引入,Go 1.13 后功能基本完善,在 Go 1.16 開始默認啟用,完全取代了早期的 GOPATH 模式。
在 Go 1.11 之前,Go 一直依賴 GOPATH 進行代碼組織和依賴管理,但存在諸多痛點:
- 缺乏版本控制機制;
- 不便于多個項目管理不同版本依賴;
- 無法輕松復現項目依賴環境;
- 不支持私有模塊、鏡像代理、校驗等高級功能。
Go modules 徹底解決了這些問題,成為 Go 語言現代化開發的標配。
2.GOPATH的工作模式
Go Modoules的目的之一就是淘汰GOPATH, 那么GOPATH是個什么?為什么不再推薦 GOPATH 的模式了呢?
(1) What is GOPATH?
$ go envGOPATH="/home/itheima/go"
...
我們輸入go env
命令行后可以查看到 GOPATH 變量的結果,我們進入到該目錄下進行查看,如下:
go
├── bin # 可執行文件
├── pkg # 預編譯緩存
└── src # 所有源碼(項目 & 第三方庫)├── github.com├── golang.org├── google.golang.org├── gopkg.in....
GOPATH目錄下一共包含了三個子目錄,分別是:
- bin:存儲所編譯生成的二進制文件。
- pkg:存儲預編譯的目標文件,以加快程序的后續編譯速度。
- src:存儲所有.go文件或源代碼。在編寫 Go 應用程序,程序包和庫時,一般會以
$GOPATH/src/github.com/foo/bar
的路徑進行存放。
因此在使用 GOPATH 模式下,我們需要將應用代碼存放在固定的$GOPATH/src
目錄下,并且如果執行go get來拉取外部依賴會自動下載并安裝到$GOPATH
目錄下。
(2) GOPATH模式的弊端
在 GOPATH 的 $GOPATH/src
下進行 .go 文件或源代碼的存儲,我們可以稱其為 GOPATH 的模式,這個模式擁有一些弊端:
-
沒有版本控制:go get 無法指定具體版本,只能拉取最新。
-
依賴不可復現:團隊成員很難保持依賴版本一致。
-
無法支持模塊多版本共存:如 v1/v2 無法同時存在,容易出現包沖突。
Go Modules模式
我們接下來用Go Modules的方式創建一個項目, 建議為了與GOPATH分開,不要將項目創建在$GOPATH/src
下.
(1) 常用go mod命令
命令 | 作用 |
---|---|
go mod init | 初始化 Go 項目并創建 go.mod 文件 通常在你開始一個新的 Go 項目時使用 |
go get | go get 用于獲取并安裝 Go 依賴的包,通常用于下載依賴、更新依賴版本,或者安裝可執行包 這個命令通常用于添加新的依賴,或更新已安裝的依賴。 |
go mod download | 下載 go.mod 中聲明的依賴 你可以在從代碼倉庫 pull 最新代碼后使用該命令來確保本地已經下載了項目中聲明的所有依賴。 |
go mod tidy | 整理依賴、清理未使用的依賴 這條命令非常常用,可以幫助你保持 go.mod 和 go.sum 文件的干凈整潔。你可以在:新增依賴時使用;從代碼倉庫 pull 后,執行這條命令來清理任何不再使用的依賴;在你修改了代碼后,刪除了一些不再需要的包時運行;在 push 代碼之前使用,確保沒有冗余依賴。 |
go mod graph | 查看項目的依賴圖,了解哪些模塊依賴于哪些其他模塊 例如,你遇到了一些版本沖突或依賴錯誤時,這個命令可以幫助你查看依賴關系,找到沖突的根源。 |
go mod edit | 手動編輯 go.mod 文件,可以修改模塊名稱、添加模塊或調整模塊版本等 這個命令較少直接使用 ,如果需要手動指定版本號,或處理一些模塊相關的高級需求時可以使用。 |
go mod vendor | 導出項目所有的依賴到vendor目錄(依賴本地化) 在需要保證依賴的穩定性時使用,尤其是當你需要在沒有網絡連接的環境中工作,或者將代碼部署到不穩定網絡的環境中時。通常,大型公司或團隊項目會使用該命令來確保依賴的完整性。 |
go mod verify | 驗證 go.mod 中列出的依賴是否完整,檢查模塊是否被篡改 用于驗證依賴的完整性,確保 go.mod 和 go.sum 文件中的依賴沒有被篡改,常在 CI/CD 流程中使用,確保代碼的安全性。 |
go mod why | 查看某個依賴為何被引用,幫助你了解該依賴的使用情況 當你發現某個模塊在 go.mod 中被列出,但你不清楚為什么需要這個依賴時,可以用 go mod why 查看其引用來源。它能夠幫助你跟蹤依賴鏈,尤其是復雜項目中的依賴分析。 |
可以通go mod help
查看學習這些指令,強烈建議多用 go mod tidy
,隨時清理無效依賴,保持 go.mod
& go.sum
干凈整潔。
go get
和go mod download
的區別:
go get
:用于獲取并安裝依賴,可能會更新 go.mod 中的依賴版本。
go mod download
:只會下載 go.mod 中列出的依賴,不會更新或修改任何文件,只保證依賴的存在。
簡單來說,go get
主要用于獲取和安裝新的依賴,并可能改變項目依賴版本,而go mod download
只是用來確保下載go.mod
文件中列出的所有依賴。
(2) go mod環境變量
可以通過 go env
命令來進行查看:
$ go env
GO111MODULE="auto"
GOPROXY="https://proxy.golang.org,direct"
GONOPROXY=""
GOSUMDB="sum.golang.org"
GONOSUMDB=""
GOPRIVATE=""
...
GO111MODULE
Go語言提供了 GO111MODULE
這個環境變量來作為 Go modules 的開關,(Go 1.16 及以后默認已廢棄該變量,默認就是on),其允許設置以下參數:
- auto:在含有 go.mod 時啟用,目前在 Go1.11 至 Go1.14 中仍然是默認值。
- on:始終啟用 Go modules(推薦),未來版本中的默認值。
- off:全禁用 Go modules(不推薦)。
可以通過下面的命令來設置:
$ go env -w GO111MODULE=on
GOPROXY
這個環境變量主要是用于設置 Go 模塊代理(Go module proxy),其作用是用于使 Go 在后續拉取模塊版本時直接通過鏡像站點來快速拉取。
GOPROXY 的默認值是:https://proxy.golang.org,direct
proxy.golang.org
國內訪問不了,需要設置國內的代理
阿里云:https://mirrors.aliyun.com/goproxy/
七牛云: https://goproxy.cn,direct
如:
$ go env -w GOPROXY=https://goproxy.cn,direct
GOPROXY 的值是一個以英文逗號 “,” 分割的 Go 模塊代理列表,允許設置多個模塊代理,假設你不想使用,也可以將其設置為 “off” ,這將會禁止 Go 在后續操作中使用任何 Go 模塊代理。
設置多個模塊代理:
$ go env -w GOPROXY=https://goproxy.cn,https://mirrors.aliyun.com/goproxy/,direct
而在剛剛設置的值中,我們可以發現值列表中有 “direct” 標識,它又有什么作用呢?
實際上 “direct” 是一個特殊指示符,用于指示 Go 回源到模塊版本的源地址去抓取(比如 GitHub 等),場景如下:當值列表中上一個 Go 模塊代理返回 404 或 410 錯誤時,Go 自動嘗試列表中的下一個,遇見 “direct” 時回源,也就是回到源地址去抓取,而遇見 EOF 時終止并拋出類似 “invalid version: unknown revision…” 的錯誤。
GOSUMDB
它的值是一個 Go checksum database
,用于在拉取模塊版本時(無論是從源站拉取還是通過 Go module proxy 拉取)保證拉取到的模塊版本數據未經過篡改,若發現不一致,也就是可能存在篡改,將會立即中止。
GOSUMDB 的默認值為:sum.golang.org
,在國內也是無法訪問的,但是 GOSUMDB 可以被 Go 模塊代理所代理,即GOPROXY默認充當這個網站。
因此我們可以通過設置 GOPROXY 來解決,而先前我們所設置的模塊代理 goproxy.cn
就能支持代理 sum.golang.org
,所以這一個問題在設置 GOPROXY 后,你可以不需要過度關心。
另外若對 GOSUMDB 的值有自定義需求,其支持如下格式:
- 格式 1:
<SUMDB_NAME>+<PUBLIC_KEY>
- 格式 2:
<SUMDB_NAME>+<PUBLIC_KEY> <SUMDB_URL>
也可以將其設置為“off”,也就是禁止 Go 在后續操作中校驗模塊版本,不推薦。
GONOPROXY/GONOSUMDB/GOPRIVATE
這三個環境變量都是用在當前項目依賴了私有模塊,例如像是你公司的私有 git 倉庫,又或是 github 中的私有庫,都是屬于私有模塊,都是要進行設置的,否則會拉取失敗。
更細致來講,就是依賴了由 GOPROXY 指定的 Go 模塊代理或由 GOSUMDB 指定 Go checksum database 都無法訪問到的模塊時的場景。
而一般建議直接設置 GOPRIVATE,它的值將作為 GONOPROXY 和 GONOSUMDB 的默認值,所以建議的最佳姿勢是直接使用 GOPRIVATE。
并且它們的值都是一個以英文逗號 “,” 分割的模塊路徑前綴,也就是可以設置多個,例如:
$ go env -w GOPRIVATE="git.example.com,github.com/eddycjy/mquote"
如果不想每次都重新設置,還支持通配符:
$ go env -w GOPRIVATE="*.example.com"
設置后,后綴為 .example.com 的模塊都會被認為是私有模塊,都不會經過GOPROXY并經過GOSUMDB檢驗。需要注意的是不包括 example.com 本身
用Go Modules初始化項目
(1) 開啟Go Modules
$ go env -w GO111MODULE=on
又或是可以通過直接設置系統環境變量(寫入對應的~/.bash_profile 文件亦可)來實現這個目的:
$ export GO111MODULE=on
(2) 初始化項目
創建項目目錄
$ mkdir -p $HOME/aceld/modules_test
$ cd $HOME/aceld/modules_test
我們后面會在modules_test下寫代碼,首先要執行Go modules 初始化的工作,如下所示,會在本地創建一個go.mod文件。go mod init
后面要跟一個當前模塊的名稱,這個名稱是自定義寫的,這個名稱他決定于今后導包的時候,即其他人import的時候怎么寫
$ go mod init github.com/aceld/modules_testgo: creating new go.mod: module github.com/aceld/modules_test
生成的 go.mod:
module github.com/aceld/modules_testgo 1.14
在執行 go mod init
命令時,我們指定了模塊導入路徑為 github.com/aceld/modules_test
。接下來我們在該項目根目錄下創建 main.go
文件,如下:
package mainimport ("fmt""github.com/aceld/zinx/znet""github.com/aceld/zinx/ziface"
)//ping test 自定義路由
type PingRouter struct {znet.BaseRouter
}//Ping Handle
func (this *PingRouter) Handle(request ziface.IRequest) {//先讀取客戶端的數據fmt.Println("recv from client : msgId=", request.GetMsgID(), ", data=", string(request.GetData()))//再回寫ping...ping...pingerr := request.GetConnection().SendBuffMsg(0, []byte("ping...ping...ping"))if err != nil {fmt.Println(err)}
}func main() {//1 創建一個server句柄s := znet.NewServer()//2 配置路由s.AddRouter(0, &PingRouter{})//3 開啟服務s.Serve()
}
OK, 我們先不要關注代碼本身,我們看當前的main.go也就是我們的aceld/modules_test
項目,是依賴一個叫github.com/aceld/zinx
庫的. znet
和ziface
只是zinx的兩個模塊.
明顯我們的項目沒有下載剛才代碼中導入的那互聯網上的兩個包,我們只是import導入進來了,如果是之前GOPATH模式的話,應該去GOPATH下的src/git/github.com/aceld
去go get
下來,或者直接手動下載放在指定目錄。
但是我們現在是Go Modules,接下來我們在$HOME/aceld/modules_test
,本項目的根目錄執行下面的命令,假設我們用到了znet包:
$ go get github.com/aceld/zinx/znetgo: downloading github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100
go: found github.com/aceld/zinx/znet in github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100
還有go get github.com/aceld/zinx/ziface
,當然你也可以直接把整個模塊下載下來:go get github.com/aceld/zinx
這樣就會幫我們把代碼下載下來了,我們會看到 我們的go.mod
被修改,同時多了一個go.sum
文件。同時go run main.go
也能運行了。
(3) 查看go.mod文件
$HOME/aceld/modules_test/go.mod:
module github.com/aceld/modules_testgo 1.14require github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100 // indirect
發現多了一段require,表示項目需要一個庫github.com/aceld/zinx
,版本是v0.0.0-20200221135252-8a8954e75100
我們來簡單看一下這里面的關鍵字
module
: 用于定義當前項目的模塊路徑/模塊名稱,建議填寫倉庫實際地址go
:標識當前Go版本.即初始化版本require
: 列出所有直接和間接依賴模塊版本// indirect
: 示該模塊為間接依賴,也就是在當前應用程序中的 import 語句中,并沒有發現這個模塊的明確引用,有可能是你先手動 go get 拉取下來的,也有可能是你所依賴的模塊所依賴的.我們的代碼很明顯是依賴的"github.com/aceld/zinx/znet
"和"github.com/aceld/zinx/ziface
",所以就間接的依賴了github.com/aceld/zinx
(4) 查看go.sum文件
在第一次拉取模塊依賴后,會發現多出了一個 go.sum 文件,其詳細羅列了當前項目直接或間接依賴的所有模塊版本,并寫明了那些模塊版本的 SHA-256 哈希值以備 Go 在今后的操作中保證項目所依賴的那些模塊版本不會被篡改。
github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100 h1:Ez5iM6cKGMtqvIJ8nvR9h74Ln8FvFDgfb7bJIbrKv54=
github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100/go.mod h1:bMiERrPdR8FzpBOo86nhWWmeHJ1cCaqVvWKCGcDVJ5M=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
我們可以看到一個模塊路徑可能有如下兩種:
h1:hash
情況
github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100 h1:Ez5iM6cKGMtqvIJ8nvR9h74Ln8FvFDgfb7bJIbrKv54=
go.mod hash
情況:
github.com/aceld/zinx v0.0.0-20200221135252-8a8954e75100/go.mod h1:bMiERrPdR8FzpBOo86nhWWmeHJ1cCaqVvWKCGcDVJ5M=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
h1 hash
是 Go modules 將目標模塊版本的 zip 文件開包后,針對所有包內文件依次進行 hash,然后再把它們的 hash 結果按照固定格式和算法組成總的 hash 值。
而go.mod hash
顧名思義就是對mod文件做一次hash。而 h1 hash 和 go.mod hash 兩者,要不就是同時存在,要不就是只存在 go.mod hash。那什么情況下會不存在 h1 hash 呢,就是當 Go 認為肯定用不到某個模塊版本的時候就會省略它的 h1 hash,就會出現不存在 h1 hash,只存在 go.mod hash 的情況。
那我們剛剛go get
的文件下載到哪了呢?其實是給我們下載到了$GOPATH/pkg/mod/github.com/aceld
下面,這樣我
修改模塊的版本依賴關系
為了作嘗試,假定我們現在對zinx版本作了升級, 由zinx v0.0.0-20200221135252-8a8954e75100
升級到 zinx v0.0.0-20200306023939-bc416543ae24
(注意zinx是一個沒有打版本tag打第三方庫,如果有的版本號是有tag的,那么可以直接對應v后面的版本號即可)
那么,我們是怎么知道zinx做了升級呢, 我們又是如何知道的最新的zinx版本號是多少呢?
先回到$HOME/aceld/modules_test
,本項目的根目錄執行:
$ go get github.com/aceld/zinx/znet
go: downloading github.com/aceld/zinx v0.0.0-20200306023939-bc416543ae24
go: found github.com/aceld/zinx/znet in github.com/aceld/zinx v0.0.0-20200306023939-bc416543ae24
go: github.com/aceld/zinx upgrade => v0.0.0-20200306023939-bc416543ae24
這樣我們,下載了最新的zinx, 版本是v0.0.0-20200306023939-bc416543ae24
, 然后,我們看一下go.mod
module github.com/aceld/modules_testgo 1.14require github.com/aceld/zinx v0.0.0-20200306023939-bc416543ae24 // indirect
我們會看到,當我們執行go get
的時候, 會自動的將本地將當前項目的require
更新了.變成了最新的依賴.
好了, 現在我們就要做另外一件事,就是,我們想用一個舊版本的zinx. 來修改當前zinx模塊的依賴版本號.
目前我們在$GOPATH/pkg/mod/github.com/aceld
(可以理解為本地倉庫)下,已經有了兩個版本的zinx庫:
/go/pkg/mod/github.com/aceld$ ls
zinx@v0.0.0-20200221135252-8a8954e75100
zinx@v0.0.0-20200306023939-bc416543ae24
目前,我們/aceld/modules_test
依賴的是zinx@v0.0.0-20200306023939-bc416543ae24
這個是最新版, 我們要改成之前的版本zinx@v0.0.0-20200306023939-bc416543ae24
.
回到/aceld/modules_test
項目目錄下,執行:
$ go mod edit -replace=zinx@v0.0.0-20200306023939-bc416543ae24=zinx@v0.0.0-20200221135252-8a8954e75100
然后我們打開go.mod查看一下:
module github.com/aceld/modules_testgo 1.14require github.com/aceld/zinx v0.0.0-20200306023939-bc416543ae24 // indirectreplace zinx v0.0.0-20200306023939-bc416543ae24 => zinx v0.0.0-20200221135252-8a8954e75100
這里出現了replace關鍵字.用于將一個模塊版本替換為另外一個模塊版本。
replace和直接修改require的區別: 直接改require版本是可行的,前提是該版本能被正常下載;而replace不僅可以指定版本,也可以把模塊替換到本地路徑或 fork 地址,功能更強,適合調試/開發/本地模塊。
Go Modules 版本號規范
Go Modules 遵循 語義化版本(Semantic Versioning,SemVer) 標準。
1.基本的語義化版本規則
SemVer 格式為:vMAJOR.MINOR.PATCH,如:v1.2.3
-
MAJOR(主版本號):發生不兼容 API 修改時遞增;
-
MINOR(次版本號):向后兼容的新功能遞增;
-
PATCH(修訂號):向后兼容的問題修正遞增。
例如:
版本號 | 說明 |
---|---|
v1.0.0 | 穩定版本發布 |
v1.2.0 | 增加了新功能,兼容老版本 |
v1.2.3 | 修復了某個 bug,兼容老版本 |
v2.0.0 | 存在破壞性改動,不兼容老版本 |
2.Go Modules 對 MAJOR 版本的特殊處理
Go Modules 在處理 主版本號 v2 及以上 時,有額外要求:主版本號 v2 及以上,必須在模塊路徑中加入版本后綴。
例如,假設你有一個庫:倉庫地址: github.com/foo/bar
;當前版本: v1.5.0
當你要發布 v2.0.0 時,模塊路徑需修改為:module github.com/foo/bar/v2
否則,在使用時會導致依賴拉取異常或不兼容的問題。
# 例子# v1 版本 module 路徑
module github.com/foo/bar# v2 版本及以上 module 路徑
module github.com/foo/bar/v2
這種設計的好處:保持對舊版本的兼容性;明確標識重大版本分支;避免不同版本沖突。
實踐建議:升級到 v2+ 時,務必修改 go.mod 中的 module 路徑;發布新版本時,在 Git 中打上對應 tag,例如:v2.0.0;消費方導入時需使用完整路徑:
import "github.com/foo/bar/v2/mypkg"
切勿隨意跳過版本號規范,否則會導致下游依賴管理困難,尤其在企業內部的庫管理中尤為重要。
vendor 模式實踐
1.什么是 vendor 模式?
Go Modules 默認采用 proxy 模式 拉取依賴。但在某些場景下,vendor 模式更適合:企業內網,無法訪問公網;離線部署,無法實時拉取依賴;安全審計,依賴需提前鎖定;持續集成(CI/CD),確保構建穩定性。
vendor 模式即將所有依賴源碼復制到本地的 vendor/
目錄中,構建時直接從本地依賴目錄讀取,無需訪問外部網絡。
2.如何啟用 vendor 模式
生成 vendor 目錄:go mod vendor
執行后,會將 go.mod
和 go.sum
中聲明的依賴下載并復制到項目下的 vendor/
目錄。
強制使用 vendor 編譯:go build -mod=vendor
或者:GOFLAGS=-mod=vendor go build
測試時也可指定使用 vendor:go test -mod=vendor ./...
日常開發中,啟用全局 vendor,可在項目根目錄設置環境變量:export GOFLAGS=-mod=vendor
,這樣執行所有 go
命令時,默認啟用 vendor 模式。
3.vendor 模式的優缺點
優點 | 缺點 |
---|---|
離線構建、部署更可靠 | 占用磁盤空間 |
防止依賴失效、倉庫被刪 | 需手動維護同步 |
方便代碼安全審計 | 依賴更新需重新執行 go mod vendor |
加速 CI/CD 構建 | – |
4.實踐建議
-
建議在企業內網、私有部署等穩定環境下使用 vendor;
-
建議將 vendor/ 目錄納入版本控制(如 Git);
-
每次更新依賴后,務必重新執行 go mod vendor,確保同步;
-
日常開發中,仍可在本地使用默認的 module 模式,避免頻繁維護 vendor。