說在前面:
本文是《Go學(xué)習(xí)圣經(jīng)》 的第二部分。
第一部分請參見:Go學(xué)習(xí)圣經(jīng):0基礎(chǔ)精通GO開發(fā)與高并發(fā)架構(gòu)(1)
現(xiàn)在拿到offer超級難,甚至連面試電話,一個都搞不到。
尼恩的技術(shù)社群中(50+),很多小伙伴憑借 “左手云原生+右手大數(shù)據(jù)”的絕活,拿到了offer,并且是非常優(yōu)質(zhì)的offer,據(jù)說年終獎都足足18個月。
從Java高薪崗位和就業(yè)崗位來看,云原生、K8S、GO 現(xiàn)在對于 高級工程師/架構(gòu)師來說,越來越重要。尼恩從架構(gòu)師視角出發(fā),基于自己的尼恩 3高架構(gòu)師知識體系和知識宇宙,寫一本《GO學(xué)習(xí)圣經(jīng)》

最終的學(xué)習(xí)目標(biāo)
咱們的目標(biāo),不僅僅在于 GO 應(yīng)用編程自由,更在于 GO 架構(gòu)自由。
前段時間,一個2年小伙伴希望漲薪到18K, 尼恩把GO 語言的項目架構(gòu),給他寫入了簡歷,導(dǎo)致他的簡歷金光閃閃,脫胎換股,完全可以去拿頭條、騰訊等30K的offer, 年薪可以直接多 20W。
足以說明,GO 架構(gòu)的含金量。
另外,前面尼恩的云原生是沒有涉及GO的,但是,沒有GO的云原生是不完整的。
所以, GO語言、GO架構(gòu)學(xué)習(xí)完了之后,咱們在去打個回馬槍,完成云原生的第二部分: 《Istio + K8S CRD的架構(gòu)與開發(fā)實操》 , 幫助大家徹底穿透云原生。

本文目錄
并發(fā)編程
Go 將并發(fā)結(jié)構(gòu)作為核心語言的一部分提供。
Go 協(xié)程
Go 協(xié)程(Goroutine)是 Go 語言中的一種輕量級線程實現(xiàn)。
Go 協(xié)程(Goroutine)通過在單個線程內(nèi)同時運行多個函數(shù)來實現(xiàn)并發(fā),從而避免了線程切換的開銷,并且能夠更加高效地利用系統(tǒng)資源。
與傳統(tǒng)的線程模型不同,Go 協(xié)程不是由操作系統(tǒng)內(nèi)核調(diào)度的,而是由 Go 運行時(runtime)自己調(diào)度的。
為啥是輕量級線程呢?Go 協(xié)程(Goroutine)可以避免因為線程調(diào)度引起的額外開銷,并且能夠更好地控制協(xié)程的數(shù)量和調(diào)度機制。
創(chuàng)建一個協(xié)程非常簡單,只需要在函數(shù)調(diào)用前面添加 go 關(guān)鍵字即可,例如:
func main() {
go func() {
fmt.Println("Hello, world!")
}()
}
這段代碼會創(chuàng)建一個新的協(xié)程,并在其中執(zhí)行匿名函數(shù)中的代碼。
這個協(xié)程會在后臺運行,不會阻塞主線程的執(zhí)行。
創(chuàng)建Go 協(xié)程(Goroutine)
Go 程(goroutine)是由 Go 運行時管理的輕量級線程。
創(chuàng)建一個協(xié)程非常簡單,只需要在函數(shù)調(diào)用前面添加 go 關(guān)鍵字即可
go f(x, y, z)
上面的代碼,會啟動一個新的 Go 協(xié)程(Goroutine)去執(zhí)行 f(x, y, z) 函數(shù), x
, y
和 z
的求值發(fā)生在當(dāng)前的 Go協(xié) 程中,而 f
的執(zhí)行發(fā)生在新的 Go 協(xié)程中。
下面是一個例子
package cocurrent
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Printf("字符 %s: %d \n", s, i)
}
}
func GoroutineDemo() {
go say("sync world ")
say("hello")
}
執(zhí)行的結(jié)果
Go 協(xié)程在相同的地址空間中運行,因此在訪問共享的內(nèi)存時必須進(jìn)行同步。
Go標(biāo)準(zhǔn)庫 協(xié)程同步
Go 標(biāo)準(zhǔn)庫中提供了多種同步機制,可以滿足不同場景下的需求。以下是 Go 中常用的同步機制:
- Mutex:互斥鎖,用于保護(hù)臨界區(qū)(critical section)代碼,只允許一個協(xié)程進(jìn)入臨界區(qū)執(zhí)行代碼,其他協(xié)程需要等待。使用 sync.Mutex 類型來定義互斥鎖。
- RWMutex:讀寫鎖,用于保證在讀操作時允許多個協(xié)程同時訪問資源,在寫操作時只允許一個協(xié)程進(jìn)入臨界區(qū)修改資源。使用 sync.RWMutex 類型來定義讀寫鎖。
- WaitGroup:等待組,用于等待一組并發(fā)協(xié)程執(zhí)行完成后再繼續(xù)執(zhí)行。使用 sync.WaitGroup 類型來定義等待組。
- Cond:條件變量,用于在協(xié)程之間同步和通信。使用 sync.Cond 類型來定義條件變量。
- Once:單次執(zhí)行,用于確保某個操作只會被執(zhí)行一次。使用 sync.Once 類型來定義單次執(zhí)行。
這些同步機制都可以幫助我們更好地控制協(xié)程的執(zhí)行順序和并發(fā)訪問共享資源的安全性。在實際開發(fā)中,我們需要根據(jù)具體情況選擇合適的同步機制,并且要注意避免死鎖等問題。
Mutex互斥鎖同步
這里涉及的概念叫做 互斥(mutual exclusion) ,我們通常使用互斥鎖(Mutex)這一數(shù)據(jù)結(jié)構(gòu)來提供這種機制。
Go 中的 Mutex(互斥鎖)是一種最基本的同步機制,用于保護(hù)臨界區(qū)代碼,只允許一個協(xié)程進(jìn)入臨界區(qū)執(zhí)行代碼,其他協(xié)程需要等待。在 Go 標(biāo)準(zhǔn)庫中,可以使用 sync.Mutex 類型來定義互斥鎖。
Go 標(biāo)準(zhǔn)庫中提供了 sync.Mutex
互斥鎖類型及其兩個方法:
Lock
Unlock
我們可以通過在代碼前調(diào)用 Lock
方法,在代碼后調(diào)用 Unlock
方法來保證一段代碼的互斥執(zhí)行。參見 Inc
方法。
我們也可以用 defer
語句來保證互斥鎖一定會被解鎖。參見 Value
方法。
sync.Mutex類似于java 里邊的 Lock 顯示鎖。 關(guān)于java顯示鎖,請參見 尼恩《Java 高并發(fā)核心編程 卷2 加強版》
啰嗦一下,sync.Mutex 類型包含兩個方法:
- Lock():獲得互斥鎖,如果當(dāng)前鎖已經(jīng)被其他協(xié)程獲得,就會一直等待,直到鎖被釋放為止。
- Unlock():釋放互斥鎖,允許其他協(xié)程獲得鎖并進(jìn)入臨界區(qū)。
下面是一個使用 Mutex 實現(xiàn)協(xié)程同步的例子:
import (
"fmt"
"sync"
)
var counter int
func MutexDemo() {
var wg sync.WaitGroup
var mu sync.Mutex
wg.Add(100)
for i := 0; i < 100; i++ {
go func() {
mu.Lock()
counter++
mu.Unlock()
wg.Done()
}()
}
wg.Wait()
fmt.Println("Counter:", counter)
}
在這個例子中,我們創(chuàng)建了一個計數(shù)器 counter,并啟動了 100 個協(xié)程對其進(jìn)行累加操作。由于對 counter 的訪問是并發(fā)的,因此需要使用互斥鎖 mu 來保護(hù)它,以避免不同協(xié)程之間的競爭條件。
在每個協(xié)程中,首先使用 mu.Lock() 方法獲得互斥鎖,然后對 counter 進(jìn)行加 1 操作,并最終使用 mu.Unlock() 方法釋放互斥鎖。由于只有一個協(xié)程可以同時獲得互斥鎖并進(jìn)入臨界區(qū),因此可以保證對 counter 的操作是安全的。
最后,我們使用 sync.WaitGroup 來等待所有協(xié)程執(zhí)行完畢,并輸出最終的計數(shù)器值。
WaitGroup 等待組
在 Go 中,可以使用 sync.WaitGroup 來等待一組協(xié)程完成執(zhí)行。
sync.WaitGroup 類似于java 里邊的閉鎖。 關(guān)于java閉鎖,請參見 尼恩《Java 高并發(fā)核心編程 卷2 加強版》
sync.WaitGroup 類型提供了三個方法:
- Add(delta int):將 WaitGroup 的計數(shù)器加上 delta 值。如果 delta 是負(fù)數(shù),則會 panic。
- Done():將 WaitGroup 的計數(shù)器減 1。相當(dāng)于 Add(-1)。
- Wait():阻塞當(dāng)前協(xié)程,直到 WaitGroup 的計數(shù)器為 0。
下面是一個使用 sync.WaitGroup 實現(xiàn)并發(fā)下載的例子:
import (
"fmt"
"sync"
)
func main() {
urls := []string{
"https://www.google.com",
"https://www.bing.com",
"https://www.yahoo.com",
"https://www.baidu.com",
"https://www.amazon.com",
"https://www.apple.com",
}
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(url string) {
defer wg.Done()
download(url)
}(url)
}
wg.Wait()
fmt.Println("All downloads completed.")
}
func download(url string) {
fmt.Printf("Downloading %s...\n", url)
// 模擬下載操作
}
在這個例子中,我們定義了一個 urls 列表,包含了需要下載的網(wǎng)址。
然后創(chuàng)建了一個 sync.WaitGroup 對象 wg,并通過調(diào)用 wg.Add(1) 把計數(shù)器置為 1。
接著使用 for 循環(huán)遍歷 urls 列表,對每個網(wǎng)址都啟動一個新的協(xié)程,并在協(xié)程中調(diào)用 download() 函數(shù)來下載網(wǎng)頁內(nèi)容。
在協(xié)程中,通過 defer wg.Done() 將 WaitGroup 的計數(shù)器減 1,表示當(dāng)前協(xié)程已經(jīng)完成了下載任務(wù)。
最后,主程序調(diào)用 wg.Wait() 來等待所有協(xié)程執(zhí)行完畢,并輸出提示信息表示所有下載任務(wù)都已經(jīng)完成了。
Cond(條件變量)
Go 中的 Cond(條件變量)是一種同步機制,用于在協(xié)程之間同步和通信。
Cond 是基于 Mutex 和 WaitGroup 實現(xiàn)的,它可以讓一個或多個協(xié)程等待某個條件滿足后再執(zhí)行下一步操作。
在 Go 標(biāo)準(zhǔn)庫中,可以使用 sync.Cond 類型來定義條件變量。
sync.Cond 類型包含三個方法:
- Broadcast():喚醒所有正在等待條件變量的協(xié)程。
- Signal():喚醒一個正在等待條件變量的協(xié)程。
- Wait():阻塞當(dāng)前協(xié)程,并解鎖 Mutex,直到收到 Broadcast 或 Signal 信號后才會被喚醒并重新獲得 Mutex。
下面是一個使用 Cond 實現(xiàn)生產(chǎn)者-消費者模型的例子:
import (
"fmt"
"sync"
)
const capacity = 5
var queue []int
var mu sync.Mutex
var cond = sync.NewCond(&mu)
func main() {
var wg sync.WaitGroup
wg.Add(2)
// 生產(chǎn)者協(xié)程
go func() {
defer wg.Done()
for i := 0; i < capacity*2; i++ {
mu.Lock()
for len(queue) == capacity {
cond.Wait()
}
queue = append(queue, i)
fmt.Println("Produce:", i)
if len(queue) == 1 {
cond.Signal()
}
mu.Unlock()
}
}()
// 消費者協(xié)程
go func() {
defer wg.Done()
for i := 0; i < capacity*2; i++ {
mu.Lock()
for len(queue) == 0 {
cond.Wait()
}
item := queue[0]
queue = queue[1:]
fmt.Println("Consume:", item)
if len(queue) == capacity-1 {
cond.Signal()
}
mu.Unlock()
}
}()
wg.Wait()
}
在這個例子中,我們定義了一個長度為 5 的隊列,然后創(chuàng)建了兩個協(xié)程,一個用來生產(chǎn)數(shù)據(jù),另一個用來消費數(shù)據(jù)。
在協(xié)程中,使用 sync.Mutex 和 sync.Cond 對象來保護(hù)和同步共享資源。
在生產(chǎn)者協(xié)程中,首先調(diào)用 mu.Lock() 獲取互斥鎖,然后使用 for 循環(huán)判斷隊列是否已滿,
- 如果已滿則調(diào)用 cond.Wait() 阻塞當(dāng)前協(xié)程,等待消費者協(xié)程喚醒。
- 如果隊列未滿,則將數(shù)據(jù)插入隊列并打印生產(chǎn)的數(shù)據(jù)。
在插入數(shù)據(jù)后,如果隊列原來為空,則調(diào)用 cond.Signal() 喚醒一個正在等待條件變量的協(xié)程。最后,使用 mu.Unlock() 釋放互斥鎖。
在消費者協(xié)程中,首先調(diào)用 mu.Lock() 獲取互斥鎖,然后使用 for 循環(huán)判斷隊列是否為空
- 如果為空則調(diào)用 cond.Wait() 阻塞當(dāng)前協(xié)程,等待生產(chǎn)者協(xié)程喚醒。
- 如果隊列非空,則取出隊頭元素并打印消費的數(shù)據(jù)。
在取出數(shù)據(jù)后,如果隊列原來已滿,則調(diào)用 cond.Signal() 喚醒一個正在等待條件變量的協(xié)程。
最后,使用 mu.Unlock() 釋放互斥鎖。
channel 通道
除了標(biāo)準(zhǔn)庫 sync
包提供了協(xié)程 同步能力,還可以使用channel 來實現(xiàn)。
channel 是一種特殊的數(shù)據(jù)類型,可以用來在協(xié)程之間傳遞數(shù)據(jù),并且能夠?qū)崿F(xiàn)阻塞式等待和喚醒功能。
channel 通道(/信道)的兩個基本操作
和映射與切片一樣,channel 通道在使用前必須創(chuàng)建:
ch := make(chan int)
使用 make 函數(shù)創(chuàng)建 channel 時,第一個參數(shù)為 channel 類型,第二個參數(shù)為緩沖區(qū)大?。蛇x)。注意,第二個參數(shù)是可選的。
channel 通道在創(chuàng)建的時候, 類型參數(shù)表示 通道里邊 值的類型。所以,通道是帶有類型的管道,你可以通過它用信道操作符 <-
來發(fā)送或者接收值。
ch <- v // 將 v 發(fā)送至信道 ch。
v := <-ch // 從 ch 接收值并賦予 v。
“箭頭” <- 就是數(shù)據(jù)流的方向。默認(rèn)情況下,發(fā)送和接收操作在另一端準(zhǔn)備好之前都會阻塞。這使得 Go 程可以在沒有顯式的鎖或競態(tài)變量的情況下進(jìn)行同步。
在使用 channel 進(jìn)行同步時,一般有兩種基本的操作:
- 發(fā)送數(shù)據(jù)到 channel:通過 channel 的 <- 操作符向其中發(fā)送一個值,例如:
ch <- "hello"
- 從 channel 接收數(shù)據(jù):通過 channel 的 <- 操作符從其中接收一個值,例如:
msg := <- ch
當(dāng)調(diào)用 <- 操作符時,如果 channel 中沒有數(shù)據(jù)可用,則當(dāng)前協(xié)程會被阻塞,直到有數(shù)據(jù)可用為止。
下面是一個使用 channel 實現(xiàn)協(xié)程同步的例子:
func main() {
ch := make(chan string)
go func() {
fmt.Println("Sending message...")
ch <- "Hello, world!"
fmt.Println("Message sent!")
}()
msg := <- ch
fmt.Println("Received message:", msg)
}
在這個例子中,我們創(chuàng)建了一個字符串類型的 channel,然后啟動了一個新的協(xié)程。
在協(xié)程中,先打印一條信息表示正在發(fā)送消息,然后將消息發(fā)送到 channel 中。發(fā)送完成后,再打印一條信息表示消息已經(jīng)發(fā)送完畢。
在主程序中,我們等待從 channel 中接收到消息,并將其保存到變量 msg 中。接收到消息后,再打印一條信息表示已經(jīng)接收到了消息,并輸出這個消息的內(nèi)容。
注意,在這個例子中,主程序會被阻塞,直到從 channel 中接收到了消息為止。就是這句:
msg := <- ch
這是因為主程序使用 <- ch 操作符從 channel 中接收數(shù)據(jù)時,如果 channel 中沒有數(shù)據(jù)可用,它會一直阻塞等待,直到有數(shù)據(jù)可用為止。
附錄:make 函數(shù)如何使用?
在 Go 中,make 函數(shù)用于創(chuàng)建一個類型為 slice、map 或 channel 的對象,并返回其引用。make 函數(shù)的語法如下:
make(Type, size)
其中 Type 表示要創(chuàng)建的對象類型,size 則表示對象大小或緩沖區(qū)大?。▋H適用于 channel)。具體來說,make 函數(shù)有以下三種用法:
1.創(chuàng)建 slice:使用 make 函數(shù)創(chuàng)建 slice 時,第一個參數(shù)為 slice 類型,第二個參數(shù)為 slice 的長度(數(shù)量),第三個參數(shù)為 slice 的容量(可選)。例如:
// 創(chuàng)建長度為 10,容量為 20 的 int 類型 slice
s := make([]int, 10, 20)
2.創(chuàng)建 map:使用 make 函數(shù)創(chuàng)建 map 時,第一個參數(shù)為 map 類型,不需要指定大小。例如:
// 創(chuàng)建 string 到 int 的映射表
m := make(map[string]int)
3.創(chuàng)建 channel:使用 make 函數(shù)創(chuàng)建 channel 時,第一個參數(shù)為 channel 類型,第二個參數(shù)為緩沖區(qū)大?。蛇x)。例如:
// 創(chuàng)建一個無緩沖的 channel
ch := make(chan string)
// 創(chuàng)建一個可以緩存 10 個字符串的 channel
ch := make(chan string, 10)
除此之外,make 函數(shù)還可以用于創(chuàng)建一些類型的值,例如 string、array 和 struct 等。
但是,在這些情況下,通常更推薦使用字面量語法來創(chuàng)建相應(yīng)的值。
range遍歷 和 通道關(guān)閉 close
在 Go 中,可以使用 close
函數(shù)來關(guān)閉通道。關(guān)閉通道后,發(fā)送方不能再向通道中發(fā)送數(shù)據(jù),但是接收方仍然可以從通道中接收數(shù)據(jù),直到通道中所有的數(shù)據(jù)都被讀取完畢。
如果要關(guān)閉通道,生產(chǎn)者/發(fā)送者可通過 close
函數(shù)關(guān)閉一個信道,來表示沒有需要發(fā)送的值了。
close函數(shù)的使用方法,非常簡單,具體如下:
close(ch)
消費者/接收者如何判定呢?
在消費的時候, 可以通過接收表達(dá)式返回的第二個參數(shù),來測試信道是否被關(guān)閉, 兩個返回值版本的接收表達(dá)式如下:
v, ok := <-ch
若兩個返回值中,如果沒有值可以接收、且信道已被關(guān)閉,第一個值為0值,第二個值 ok
會被設(shè)置為 false
。
其中 ok 是一個 bool 類型,可以通過它來判斷 channel 是否已經(jīng)關(guān)閉,如果 channel 關(guān)閉該值為 false ,此時 v 接收到的是 channel 類型的零值。比如:channel 是傳遞的 int, 那么 v 就是 0 ;如果是結(jié)構(gòu)體,那么 v 就是結(jié)構(gòu)體內(nèi)部對應(yīng)字段的零值。
注意:
- 只有發(fā)送者才能關(guān)閉信道,而接收者不能。
- 向一個已經(jīng)關(guān)閉的信道發(fā)送數(shù)據(jù)會引發(fā)程序恐慌(panic)。
在 Go 中,可以使用 range
來遍歷通道中的數(shù)據(jù)。使用 range
遍歷通道時,會一直等待通道中有新的數(shù)據(jù)可讀取,直到通道被關(guān)閉或者顯式地使用 break
終止循環(huán)。
簡單來說,循環(huán) for i := range c
會不斷從信道接收值,直到它被關(guān)閉。
還要注意: 信道與文件不同,通常情況下無需關(guān)閉它們。只有在必須告訴接收者不再有需要發(fā)送的值時才有必要關(guān)閉,例如終止一個 range
循環(huán)。
下面是一個使用 range
遍歷通道的示例:
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
time.Sleep(time.Second)
}
close(ch)
}()
for x := range ch {
fmt.Println("Received:", x)
}
fmt.Println("Done")
}
在這個例子中,我們創(chuàng)建了一個無緩沖的 channel ch
,并啟動了一個協(xié)程向 ch
中發(fā)送數(shù)據(jù)。
在主程序中,使用 range
遍歷 ch
中的數(shù)據(jù),并打印接收到的數(shù)據(jù)。
當(dāng)協(xié)程向 ch
中發(fā)送完數(shù)據(jù)后,通過 close
函數(shù)關(guān)閉 ch
。
在使用 range
遍歷通道時,如果通道未關(guān)閉,則循環(huán)會一直等待直到通道被關(guān)閉。當(dāng)通道被關(guān)閉后,循環(huán)會自動終止,無需使用其他方式來判斷通道是否已經(jīng)關(guān)閉。同時,如果在循環(huán)中使用 break
終止循環(huán),則需要注意在終止前將通道關(guān)閉,否則可能會導(dǎo)致死鎖等問題。
需要注意的是,使用 range
遍歷通道時,如果通道中已經(jīng)沒有數(shù)據(jù)可讀取,則循環(huán)會被阻塞,直到有新的數(shù)據(jù)可讀取或者通道被關(guān)閉。因此,在使用 range
遍歷通道時,需要確保在發(fā)送方將所有數(shù)據(jù)發(fā)送完畢后及時關(guān)閉通道,否則可能會導(dǎo)致循環(huán)一直阻塞等待。
close Channel 的一些說明
channel 不需要通過 close 來釋放資源,這個是它與 socket、file 等不一樣的地方,對于 channel 而言,唯一需要 close 的就是我們想通過 close 觸發(fā) channel 讀事件。
- close channel對 channel阻塞無效,寫了數(shù)據(jù)不讀,直接 close,還是會阻塞的。
- 如果 channel 已經(jīng)被關(guān)閉,繼續(xù)往它發(fā)送數(shù)據(jù)會導(dǎo)致 panic
send on closed channel
- closed 的 channel,再次關(guān)閉 close 會 panic
- close channel 的推薦使用姿勢是在發(fā)送方來執(zhí)行,因為 channel 的關(guān)閉只有接收端能感知到,但是發(fā)送端感知不到,因此一般只能在發(fā)送端主動關(guān)閉。而且大部分時候可以不執(zhí)行 close,只需要讀寫即可。
- 從一個已經(jīng) close 的 channel中讀取數(shù)據(jù),是可以讀取的,讀到的數(shù)據(jù)為 0
- 讀取的 channel 如果被關(guān)閉,并不會影響正在讀的數(shù)據(jù),它會將所有數(shù)據(jù)讀取完畢,在讀取完已發(fā)送的數(shù)據(jù)后會返回元素類型的零值(zero value)。
多通道查詢select 語句/通道的多路復(fù)用
select
語句使一個 Go 程可以等待多個channel通信操作。
select
會阻塞到某個分支可以繼續(xù)執(zhí)行為止,這時就會執(zhí)行該分支。當(dāng)多個分支都準(zhǔn)備好時會隨機選擇一個執(zhí)行。
在 Go 中,可以使用 select
語句來等待多個 channel 中的數(shù)據(jù),并執(zhí)行相應(yīng)的操作。
當(dāng)有多個 channel 中的數(shù)據(jù)可讀取時,select
語句會隨機選擇一個可用的 channel,并執(zhí)行對應(yīng)的操作。
下面是一個示例代碼,演示如何使用 select
語句查詢多個 channel 中的數(shù)據(jù):
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan string)
go func() {
for i := 0; i < 5; i++ {
ch1 <- i
time.Sleep(time.Second)
}
}()
go func() {
for i := 0; i < 5; i++ {
ch2 <- fmt.Sprintf("Message %d", i)
time.Sleep(time.Second)
}
}()
for i := 0; i < 10; i++ {
select {
case x := <-ch1:
fmt.Println("Received from ch1:", x)
case x := <-ch2:
fmt.Println("Received from ch2:", x)
}
}
fmt.Println("Done")
}
在這個示例代碼中,我們創(chuàng)建了兩個 channel ch1
和 ch2
,分別用于發(fā)送 int
類型和 string
類型的數(shù)據(jù)。
在兩個協(xié)程中,分別向 ch1
和 ch2
中發(fā)送數(shù)據(jù),并間隔一秒鐘。
在主函數(shù)中,使用 select
語句查詢 ch1
和 ch2
中的數(shù)據(jù),并打印接收到的數(shù)據(jù)。在循環(huán)中共查詢 10 次,由于兩個協(xié)程的間隔時間不同,因此可能會先從 ch1
中接收到數(shù)據(jù),也可能會先從 ch2
中接收到數(shù)據(jù)。最后,當(dāng)所有數(shù)據(jù)被讀取完畢后,程序輸出 Done
。
需要注意的是,在使用 select
語句查詢多個 channel 時,如果多個 channel 同時有數(shù)據(jù)可讀取,則隨機選擇一個 channel,并執(zhí)行對應(yīng)的操作。
因此,在設(shè)計程序邏輯時,需要考慮到 channel 的使用順序可能會發(fā)生變化。此外,如果在 select
語句中同時等待多個 channel,而其中一個 channel 被關(guān)閉了,則程序仍然會等待其它的 channel,并在有數(shù)據(jù)可讀取時執(zhí)行相應(yīng)的操作。
Go的select 和 OS的select 對比
Go語言中的select 和操作系統(tǒng)中的系統(tǒng)調(diào)用select比較相似。
C語言的select系統(tǒng)調(diào)用可以同時監(jiān)聽多個文件描述符的可讀或者可寫的狀態(tài),Go 語言的select可以讓Goroutine同時等待多個Channel可讀或可寫,在多個文件或Channel狀態(tài)改變之前,select會一直阻塞當(dāng)前線程或Goroutine。
select是與switch相似的控制結(jié)構(gòu),不過select的case中的表達(dá)式必須都是channel的收發(fā)操作。當(dāng)select中的多個case同時被觸發(fā)時,會隨機執(zhí)行其中一個。
通常情況下,select語言會阻塞goroutine并等待多個Channel中的一個達(dá)到可以收發(fā)的狀態(tài)。但如果有default語句,可以實現(xiàn)非阻塞,就是當(dāng)多個channel都不能執(zhí)行的時候,運行default。
非阻塞查詢
select 默認(rèn)是阻塞的,如果所有的通道都沒有數(shù)據(jù),那么 函數(shù)就會被阻塞。
如何不進(jìn)行阻塞呢? 在 Go 中,select
語句還可以使用 default
分支,用于在沒有任何 channel 可讀取時執(zhí)行默認(rèn)操作。當(dāng)所有被查詢的 channel 都沒有數(shù)據(jù)可讀取時,select
會立即執(zhí)行 default
分支,從而實現(xiàn)不會被阻塞。
換句話來說,當(dāng) select
中的其它分支都沒有準(zhǔn)備好時,default
分支就會執(zhí)行。所以,為了在嘗試在接收時不發(fā)生阻塞,可使用 default
分支, 使用的方式如下:
select {
case i := <-c:
// 使用 i
default:
// 從 c 中接收會阻塞時執(zhí)行
}
下面是一個示例代碼,演示如何使用 default
分支:
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go func() {
time.Sleep(time.Second * 3)
close(ch)
}()
for {
select {
case x, ok := <-ch:
if !ok {
fmt.Println("Channel closed")
return
}
fmt.Println("Received:", x)
default:
fmt.Println("No data received")
time.Sleep(time.Second)
}
}
fmt.Println("Done")
}
在這個示例代碼中,我們創(chuàng)建了一個無緩沖的通道 ch
,并在一個協(xié)程中等待 3 秒鐘后關(guān)閉通道。
在主函數(shù)中,使用 select
語句監(jiān)聽 ch
中的數(shù)據(jù),并打印接收到的數(shù)據(jù)。
由于 ch
一開始并沒有數(shù)據(jù)可讀取,因此 select
會立即執(zhí)行 default
分支,并打印提示信息。在 ch
被關(guān)閉后,通過判斷第二個返回值 ok
的值來確定通道是否已經(jīng)關(guān)閉。如果通道已經(jīng)關(guān)閉,則跳出循環(huán)并輸出結(jié)束信息。
需要注意的是,在使用 default
分支時,需要考慮到程序的實際需求,并合理設(shè)置等待時長。
如果等待時間過短,則可能會頻繁地執(zhí)行 default
分支,導(dǎo)致性能損失;如果等待時間過長,則可能會導(dǎo)致數(shù)據(jù)延遲等問題。
此外,在使用 default
分支時,需要注意區(qū)分通道中的零值和通道已經(jīng)關(guān)閉兩種情況,以避免出現(xiàn)不必要的錯誤。
帶緩沖的通道
在 Go 中,可以使用帶緩沖的 channel 來實現(xiàn)協(xié)程之間的同步和通信。channel 可以是 帶緩沖的。
如何創(chuàng)建帶緩沖通道呢? 將緩沖長度作為第二個參數(shù)提供給 make
來初始化一個帶緩沖的信道
在創(chuàng)建帶緩沖的 channel 時,需要在 channel 類型后面添加一個整數(shù),表示緩沖區(qū)大小。例如:
// 創(chuàng)建一個可以緩存 10 個字符串的 channel
ch := make(chan string, 10)
在這個例子中,我們創(chuàng)建了一個可以緩存 10 個字符串的 channel ch。
- 當(dāng)有協(xié)程向 ch 發(fā)送數(shù)據(jù)時,如果緩沖區(qū)未滿,則可以直接將數(shù)據(jù)寫入緩沖區(qū);否則,發(fā)送操作會被阻塞,直到有協(xié)程從 ch 中讀取數(shù)據(jù)為止。
- 同樣地,當(dāng)有協(xié)程從 ch 中讀取數(shù)據(jù)時,如果緩沖區(qū)非空,則可以直接從緩沖區(qū)讀取數(shù)據(jù);否則,接收操作會被阻塞,直到有協(xié)程向 ch 中發(fā)送數(shù)據(jù)為止。
帶緩沖的通道的特點是:
- 僅當(dāng)信道的緩沖區(qū)填滿后,向其發(fā)送數(shù)據(jù)時才會阻塞。
- 當(dāng)緩沖區(qū)為空時,接受方會阻塞。
帶緩沖的 channel 是一種有固定緩沖區(qū)大小的 channel,當(dāng)緩沖區(qū)滿時,向 channel 發(fā)送數(shù)據(jù)會被阻塞,直到有協(xié)程從 channel 中接收數(shù)據(jù)為止。相反,當(dāng)緩沖區(qū)為空時,從 channel 接收數(shù)據(jù)也會被阻塞,直到有協(xié)程向 channel 中發(fā)送數(shù)據(jù)為止。
下面是一個使用帶緩沖的 channel 實現(xiàn)生產(chǎn)者-消費者模型的例子:
import (
"fmt"
)
const capacity = 5
func main() {
ch := make(chan int, capacity)
done := make(chan bool)
// 生產(chǎn)者協(xié)程
go func() {
for i := 0; i < capacity*2; i++ {
ch <- i
fmt.Println("Produce:", i)
}
done <- true
}()
// 消費者協(xié)程
go func() {
for i := 0; i < capacity*2; i++ {
item := <-ch
fmt.Println("Consume:", item)
}
done <- true
}()
<-done
<-done
}
在這個例子中,我們創(chuàng)建了一個緩沖區(qū)大小為 5 的 channel ch,然后創(chuàng)建了兩個協(xié)程,一個用來生產(chǎn)數(shù)據(jù)(向 ch 中發(fā)送數(shù)據(jù)),另一個用來消費數(shù)據(jù)(從 ch 中接收數(shù)據(jù))。
當(dāng)所有數(shù)據(jù)都被生產(chǎn)和消費完畢后,使用兩個 done channel 來通知主程序結(jié)束。
在生產(chǎn)者協(xié)程中,首先向 ch 中發(fā)送數(shù)據(jù),并打印生產(chǎn)的數(shù)據(jù)。如果緩沖區(qū)已滿,則發(fā)送操作會被阻塞,等待消費者協(xié)程從 ch 中讀取數(shù)據(jù)。在最后一個數(shù)據(jù)被生產(chǎn)和發(fā)送完畢后,通過 done channel 向主程序發(fā)送結(jié)束信號。
在消費者協(xié)程中,首先從 ch 中接收數(shù)據(jù),并打印消費的數(shù)據(jù)。如果緩沖區(qū)為空,則接收操作會被阻塞,等待生產(chǎn)者協(xié)程向 ch 中發(fā)送數(shù)據(jù)。在最后一個數(shù)據(jù)被消費完畢后,通過 done channel 向主程序發(fā)送結(jié)束信號。
Java BlockingQueue 和 Go channel 對比學(xué)習(xí)
Java 中的 BlockingQueue
和 Go 中的 channel 都是用于實現(xiàn)線程之間的通信的工具,但是它們在一些方面存在差異 , 主要有3點:
- 1:實現(xiàn)方式
Java 中的 BlockingQueue
是一個接口,它有多個不同的實現(xiàn)類,如 ArrayBlockingQueue
、LinkedBlockingQueue
等。這些實現(xiàn)類都是基于數(shù)組或鏈表等數(shù)據(jù)結(jié)構(gòu)實現(xiàn)的,提供了一些阻塞式的隊列操作方法。
Go 中的 channel 是語言內(nèi)置的類型,直接由編譯器實現(xiàn)。在底層,channel 是使用 waitgroup、mutex、cond 等同步原語實現(xiàn)的,而不是基于數(shù)據(jù)結(jié)構(gòu)實現(xiàn)的。
- 2:緩存機制
Java 的 BlockingQueue
有兩種類型:有界阻塞隊列和無界阻塞隊列。有界阻塞隊列的大小是固定的,當(dāng)隊列元素數(shù)量達(dá)到上限時,生產(chǎn)者線程會被阻塞,直到隊列中有空位。無界阻塞隊列沒有容量限制,在添加元素時不會被阻塞,但是獲取元素時可能會被阻塞。
Go 的 channel 也可以分為兩種類型:帶緩存的 channel 和非緩存的 channel。帶緩存的 channel 可以緩存一定數(shù)量的元素,當(dāng)緩沖區(qū)滿時,發(fā)送操作會被阻塞。非緩存的 channel 不允許緩存元素,每個元素只能被發(fā)送和接收一次。
- 3:阻塞機制
Java 的 BlockingQueue
提供了多種阻塞式隊列操作方法,如 put
和 take
等。其中,put
方法會在隊列已滿時阻塞直到有空位,而 take
方法會在隊列為空時阻塞直到有元素可取。
Go 的 channel 通過阻塞操作實現(xiàn)協(xié)程之間的同步和通信。當(dāng)發(fā)送或接收操作無法進(jìn)行時,協(xié)程會被阻塞,并暫停執(zhí)行,直到對應(yīng)的操作可以進(jìn)行為止。
Java 的 BlockingQueue
和 Go 的 channel 在實現(xiàn)方式和應(yīng)用場景不同,但是它們也有一些相同點。主要有4點:
- 1:用途相同
Java 的 BlockingQueue
和 Go 的 channel 都是用于協(xié)程之間的通信和同步。它們允許多個協(xié)程在不同的時間段進(jìn)行讀寫操作,并提供了阻塞式的方法來確保線程安全和正確性。
- 2:阻塞機制相同
Java 的 BlockingQueue
和 Go 的 channel 都通過阻塞操作來實現(xiàn)協(xié)程之間的同步。當(dāng)隊列為空或已滿時,生產(chǎn)者線程和消費者線程都會被阻塞,直到對應(yīng)的條件得到滿足為止。
- 3:線程安全性相同
Java 的 BlockingQueue
和 Go 的 channel 都是線程安全的。它們都提供了阻塞式的方法,可以確保多個協(xié)程在不同的時間段進(jìn)行讀寫操作時不會發(fā)生競態(tài)條件等問題。
- 4:可靠性相同
Java 的 BlockingQueue
和 Go 的 channel 都是可靠的。它們都能夠確保協(xié)程之間的通信和同步。同時,在使用過程中也可以通過異常捕獲等方法來處理潛在的錯誤,并保證程序的正確性和健壯性。
綜上所述,Java 的 BlockingQueue
和 Go 的 channel 在用途、阻塞機制、線程安全性和可靠性等方面存在相同點,這些共同點也是它們成為編寫多線程程序時的優(yōu)秀工具的原因之一。
SynchronousQueue VS 無緩沖channel
go 中channel 分為緩沖通道和非緩沖通道(容量為0)。
Go 語言的無緩沖channel,只有在發(fā)送操作和接收操作配對上了,發(fā)送方和接收方才能得以繼續(xù)執(zhí)行,否則將會阻塞在發(fā)送或者接收操作。
Go 語言的無緩沖channel,本質(zhì)上就是以同步的方式來傳遞數(shù)據(jù)。
所以, Go 語言的無緩沖channel 正是 Java 中的 SynchronousQueue 具有的特性。
零容量 無緩沖 | 有限容量 | |
---|---|---|
Go | unbuffered channel | buffered channel |
Java | SynchronousQueue | LinkedBlockingQueue |
LinkedBlockingQueue VS 緩沖通道 buffered channel
緩沖通道,顧名思義,就是能起到緩沖作用的數(shù)據(jù)類型。
相對于非緩沖通道發(fā)送操作如果沒有配對的接收操作則會阻塞的情況,緩沖通道在容量未滿的時候允許發(fā)送操作發(fā)送成功之后立即執(zhí)行后續(xù)的操作而不阻塞。
Java 中的 LinkedBlockingQueue 也具有這一特性,從命名來看就是底層基于鏈表的阻塞隊列。
操作對比
Go中,可以使用 len 獲取通道的 長度,cap 函數(shù) 獲取通道的 容量,下面是一個例子:
unbufChan := make(chan int) // 創(chuàng)建一個非緩沖通道
fmt.Printf("容量為%d\n", cap(unbufChan)) // 容量為0
fmt.Printf("長度為%d\n", len(unbufChan)) // 長度為0
bufChan := make(chan int, 8) // 創(chuàng)建一個緩沖通道
fmt.Printf("容量為%d\n", cap(bufChan)) // 容量為8
fmt.Printf("長度為%d\n", len(bufChan)) // 長度為0
bufChan <- 1
fmt.Printf("容量為%d\n", cap(bufChan)) // 容量為8
fmt.Printf("長度為%d\n", len(bufChan)) // 長度為1
對于 Go 語言的非緩沖通道,其容量也總是為0
其中隊列(或通道)的長度代表它當(dāng)前包含的元素值的個數(shù)。當(dāng)隊列(或通道)已滿時,其長度與容量相同。
SynchronousQueue VS 無緩沖channel 的長度和 容量比較:
容量 | 長度 | 剩余容量 | |
---|---|---|---|
SynchonousQueue | 0 | 0 | 0 |
unbuffered channel | 0 | 0 | 0 |
LinkedBlockingQueue VS 緩沖通道 buffered channel 的長度和 容量比較:
容量 | 長度 | 剩余容量 | |
---|---|---|---|
LinkedBlockingQueue | 構(gòu)造函數(shù)指定的capacity | size() | remainingCapacity() |
buffered channel | cap(ch) | len(ch) | cap(ch) - len(ch) |
其中隊列(或通道)的長度代表它當(dāng)前包含的元素值的個數(shù)。當(dāng)隊列(或通道)已滿時,其長度與容量相同。
go rocketmq 編程
Apache RocketMQ 是一個開源的、分布式的消息中間件系統(tǒng),支持高吞吐量和高可用性的消息傳遞。
在 Go 編程中,可以使用 Apache RocketMQ 的 Go 客戶端來實現(xiàn)與 RocketMQ 的交互。
golang 模塊安裝
go get github.com/apache/rocketmq-client-go/v2
尼恩提示:
在 goland 工具中的 模塊安裝過程,請參考后面的附錄。
實例:使用 RocketMQ 的 Go 客戶端來發(fā)送和接收消息
下面是一個簡單的示例代碼,演示如何使用 RocketMQ 的 Go 客戶端來發(fā)送和接收消息:
package batchprocess
import (
"context"
"fmt"
"github.com/apache/rocketmq-client-go/v2"
"log"
"time"
"github.com/apache/rocketmq-client-go/v2/consumer"
"github.com/apache/rocketmq-client-go/v2/primitive"
"github.com/apache/rocketmq-client-go/v2/producer"
)
// 創(chuàng)建生產(chǎn)者
const NAME_NODE = "192.168.56.121:9876"
const TOPIC = "test"
func RocketMQDemo() {
producer, err := rocketmq.NewProducer(
producer.WithNameServer([]string{NAME_NODE}),
producer.WithRetry(2),
)
if err != nil {
fmt.Println("create producer error:", err)
return
}
err = producer.Start()
if err != nil {
fmt.Println("start producer error:", err)
return
}
defer producer.Shutdown()
// 發(fā)送消息
for i := 0; i < 10; i++ {
msg := &primitive.Message{
Topic: TOPIC,
Body: []byte("Hello RocketMQ"),
}
res, err := producer.SendSync(context.Background(), msg)
if err != nil {
log.Printf("send message error: %v\n", err)
} else {
log.Printf("send message success: %v\n", res)
}
time.Sleep(time.Second)
}
// 創(chuàng)建消費者
c, err := rocketmq.NewPushConsumer(
consumer.WithNameServer([]string{NAME_NODE}),
consumer.WithGroupName("test-group"),
)
if err != nil {
fmt.Println("create consumer error:", err)
return
}
err = c.Subscribe(TOPIC, consumer.MessageSelector{},
func(ctx context.Context, msgs ...*primitive.MessageExt) (consumer.ConsumeResult, error) {
for _, msg := range msgs {
log.Printf("receive message: topic=%s, body=%s\n",
msg.Topic, string(msg.Body))
}
return consumer.ConsumeSuccess, nil
})
if err != nil {
fmt.Println("subscribe error:", err)
return
}
err = c.Start()
if err != nil {
fmt.Println("start consumer error:", err)
return
}
defer c.Shutdown()
time.Sleep(time.Second * 10)
}
在這段代碼中,首先生產(chǎn)者并用其向 RocketMQ 中的 test
主題發(fā)送了一條消息。然后創(chuàng)建了一個消費者,訂閱了 test
主題,并在回調(diào)函數(shù)中處理接收到的消息。
在這個示例中,首先創(chuàng)建了一個 RocketMQ 生產(chǎn)者,并通過 WithNameServer
和 WithRetry
分別設(shè)置了 NameServer 地址和重試次數(shù)等配置項。
然后,在循環(huán)中創(chuàng)建了一個消息對象,并調(diào)用 SendSync
方法發(fā)送同步消息。該方法的第一個參數(shù)是上下文對象,可以使用 context.Background() 創(chuàng)建;第二個參數(shù)是消息對象。如果發(fā)送成功,則返回一個 SendResult 對象,否則返回一個非空的錯誤對象。
最后,使用 time.Sleep() 方法等待一秒鐘,以便觀察發(fā)送結(jié)果。在真實的應(yīng)用程序中,可以根據(jù)需要調(diào)整等待時間。
綜上所述,在 RocketMQ 的 Go 版本客戶端 rocketmq-client-go 中,可以使用 SendSync
方法發(fā)送同步消息,并通過返回值和錯誤對象獲取發(fā)送結(jié)果。
需要注意的是: 在使用 RocketMQ 的 Go 客戶端時,必須先安裝和配置好 RocketMQ 的服務(wù)端,并將 Go 客戶端庫引入到項目中。同時,也需要根據(jù)實際情況進(jìn)行配置和參數(shù)設(shè)置,以確保程序能夠正常運行。
消息發(fā)送和接受的驗證
啟動 rocketmq
使用尼恩的一鍵啟動環(huán)境
啟動之后的效果
啟動 go 實例
package main
import (
"crazymakercircle.com/awesomeProject/batchprocess"
"fmt"
)
func main() {
fmt.Println("\tcocurrent RocketMQDemo :")
//cocurrent.GoroutineDemo()
fmt.Println("\tcocurrent MutexDemo :")
batchprocess.RocketMQDemo()
}
使用goland 直接執(zhí)行
發(fā)送消息效果
消費消息效果
附錄:Go 模塊的安裝和使用
Go 模塊是 Go 語言1.11版本后引入的官方包管理工具,可以自動管理依賴項和版本。
一個模塊是一些以版本作為單元相關(guān)的包的集合。模塊記錄精確的依賴要求并創(chuàng)建可復(fù)制的構(gòu)建。
通常,版本控制存儲庫僅包含在存儲庫根目錄中定義的一個模塊。(單個存儲庫中支持多個模塊,但是通常,與每個存儲庫中的單個模塊相比,這將導(dǎo)致正在進(jìn)行的工作更多)。
總結(jié)存儲庫,模塊和軟件包之間的關(guān)系:
- 一個存儲庫包含一個或多個Go模塊。
- 每個模塊包含一個或多個Go軟件包。
- 每個軟件包都在一個目錄中包含一個或多個Go源文件。
下面是使用 Go 模塊安裝和管理第三方庫的步驟:
啟用 Go 模塊
在使用 Go 模塊之前,需要先啟用 Go 模塊功能。
可以通過設(shè)置 GO111MODULE
環(huán)境變量來控制 Go 是否使用模塊。要啟用模塊,請將該環(huán)境變量設(shè)置為 on
,例如:
$ export GO111MODULE=on
創(chuàng)建新項目
在開始開發(fā)項目之前,需要創(chuàng)建一個新的項目目錄,并在其中初始化 Go 模塊。
可以使用 go mod init
命令來完成初始化操作,例如:
$ go mod init crazymakercircle.com/awesomeProject
這個命令會創(chuàng)建一個新的 Go 模塊,并在當(dāng)前目錄中生成一個名為 go.mod
的文件。
打開看看
go.mod
模塊由Go源文件樹定義,該go.mod
文件在樹的根目錄中。模塊源代碼可能位于GOPATH之外。
在 Go 1.11 版本之后,Go 引入了官方的包管理工具 Go modules。使用 Go modules 可以更好地管理項目中的依賴項和版本,避免了 GOPATH 和 vendor 目錄等傳統(tǒng)的包管理方式中存在的一些問題。
在使用 Go modules 時,需要在項目根目錄中創(chuàng)建一個名為 go.mod
的文件,并在其中定義模塊路徑和依賴項等信息。
下面是一個示例的 go.mod
文件:
module example.com/myproject
go 1.16
require (
github.com/gin-gonic/gin v1.7.4
github.com/go-sql-driver/mysql v1.6.0
)
在這個文件中,第一行指定了當(dāng)前模塊的名稱,即 example.com/myproject
。
注意,這個名稱應(yīng)該是唯一的,以便其他項目可以引用該模塊。
第二行指定了所使用的 Go 版本,即 go 1.16
。
下面的 require
塊定義了所有依賴項及其版本信息。
每個依賴項都由一個完整的包名稱和版本號組成,例如 github.com/gin-gonic/gin v1.7.4
。這個版本號表示需要使用的確切版本,也可以使用語義化版本號范圍來指定版本,例如 github.com/gin-gonic/gin v1.7.x
。
mod文件 有四種指令:module
,require
,replace
,exclude
。
在 Go modules 中,一個模塊可以包含多個軟件包,每個軟件包都有一個唯一的導(dǎo)入路徑。這個導(dǎo)入路徑由模塊路徑和從 go.mod
到軟件包目錄的相對路徑共同確定。
假設(shè)有一個名為 example.com/myproject
的模塊,其中包含兩個軟件包 foo
和 bar
,它們的目錄結(jié)構(gòu)如下:
myproject/
|- go.mod
|- foo/
|- foo.go
|- bar/
|- bar.go
在這個例子中,軟件包 foo
的導(dǎo)入路徑為 example.com/myproject/foo
,軟件包 bar
的導(dǎo)入路徑為 example.com/myproject/bar
。
這個導(dǎo)入路徑由模塊路徑 example.com/myproject
和相對路徑 foo
或 bar
共同組成。
注意,在 Go modules 中,所有軟件包的導(dǎo)入路徑都將模塊路徑共享為公共前綴。這個公共前綴可以幫助防止命名沖突和混淆。
總之,模塊中所有軟件包的導(dǎo)入路徑是由模塊路徑和從 go.mod
到軟件包目錄的相對路徑共同決定的。對于不同的軟件包,它們的相對路徑是不同的,但它們共享相同的模塊路徑前綴。
安裝第三方庫
在 Go 模塊中安裝第三方庫與在傳統(tǒng)的 GOPATH 中安裝方式略有不同??梢允褂?go get
命令來安裝第三方庫并將其添加到當(dāng)前項目的依賴項中,例如:
$ go get github.com/gin-gonic/gin@v1.7.4
這個命令會下載指定版本的 gin 庫,并將其添加到當(dāng)前項目的依賴項中。
go get github.com/apache/rocketmq-client-go/v2
此外,還可以使用 go get
命令下載最新版本的庫,并將其添加到依賴項中,例如:
$ go get github.com/gin-gonic/gin
這個命令會下載指定版本的 rocketmq-client-go庫,并將其添加到當(dāng)前項目的依賴項中。
比如,安裝 RocketMQ client 依賴
go get github.com/apache/rocketmq-client-go/v2
如果下載不來,或者設(shè)置代理試試,打開你的終端并執(zhí)行(Go 1.13 及以上)
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
下載依賴項
當(dāng)安裝了第三方庫后,還需要將其下載到本地計算機上。
可以使用 go mod download
命令來下載所有依賴項,例如:
$ go mod download
這個命令會下載當(dāng)前項目依賴的所有庫及其版本。
管理依賴項
在開發(fā)過程中,可能需要升級或刪除某些依賴項。可以使用 go mod tidy
命令來清理不再使用的依賴項,例如:
$ go mod tidy
這個命令會分析項目代碼并移除未使用的庫。
同時,還可以使用 go get -u
命令來升級依賴項到最新版本,例如:
$ go get -u github.com/gin-gonic/gin
這個命令會下載并安裝 gin 庫的最新版本,并更新 go.mod 文件中的版本號。
綜上所述,使用 Go 模塊安裝和管理第三方庫非常方便,可以自動解決依賴關(guān)系和版本問題,大大簡化了項目的依賴管理。
GoLand 中使用 Go 模塊(go mod)管理依賴項
在 GoLand 中使用 Go 模塊(go mod)管理依賴項,可以通過以下步驟進(jìn)行操作:
打開或創(chuàng)建一個 Go 項目
在 GoLand 中打開或創(chuàng)建一個 Go 項目,并確保該項目啟用了 Go 模塊功能。
要啟用 Go modules,可以通過菜單欄中的 File > Settings > Go > Go Modules 來啟用 Go modules。
在這個對話框中,可以選擇全局或項目級別的 Go modules 設(shè)置。建議選擇項目級別的設(shè)置,以避免影響其他項目。
初始化 Go modules
在啟用 Go modules 后,需要初始化 Go modules??梢栽诮K端中切換到項目目錄,然后執(zhí)行以下命令來初始化 Go modules:
go mod init crazymakercircle.com/awesomeProject
這個命令會創(chuàng)建一個新的 Go 模塊,并在當(dāng)前目錄中生成一個名為 go.mod
的文件。
添加依賴項
在 GoLand 中添加依賴項非常簡單??梢允褂?go get
命令來安裝第三方庫并將其添加到當(dāng)前項目的依賴項中。例如,在 GoLand 的終端窗口中輸入以下命令:
$ go get github.com/gin-gonic/gin
這個命令會下載并安裝 Gin HTTP 框架,并將其添加到 go.mod 文件中。在此之后,即可在代碼中引用 gin 庫。
比如,安裝 RocketMQ client 依賴
go get github.com/apache/rocketmq-client-go/v2
解決require內(nèi)依賴全部飄紅問題
解決go.mod文件中require內(nèi)依賴全部飄紅
設(shè)置 go 模塊化,并設(shè)置環(huán)境變量 GOPROXY=https://goproxy.cn,direct
ok了
管理依賴關(guān)系
在開發(fā)過程中,可能需要升級或刪除某些依賴項??梢允褂?go get -u
命令來升級依賴項到最新版本,并更新 go.mod 文件中的版本號,例如:
$ go get -u github.com/gin-gonic/gin
除此之外,還可以使用 GoLand 自帶的依賴關(guān)系管理工具,包括自動生成和維護(hù) go.mod 和 go.sum 文件、自動提示缺失的依賴項以及檢查依賴項的版本等。
比如:
go get -u github.com/apache/rocketmq-client-go/v2
構(gòu)建和運行項目
在完成依賴項的添加和管理后,即可構(gòu)建和運行項目。可以使用 GoLand 的集成工具來構(gòu)建和運行項目,例如:
- 點擊菜單欄中的 Run 按鈕或使用快捷鍵 Shift + F10 來運行程序;
- 在編輯器窗口中右鍵單擊并選擇 Run ‘main’ 選項來運行程序;
- 在終端中輸入
go build
命令來編譯項目,并使用./<executable>
命令來運行可執(zhí)行文件。
綜上所述,GoLand 提供了便捷的工具來支持使用 Go 模塊管理依賴項,包括自動化生成和維護(hù) go.mod 和 go.sum 文件、自動提示缺失的依賴項以及檢查依賴項的版本等,大大簡化了項目的依賴管理。
gorm 操作mysql
什么是ORM?
ORM框架操作數(shù)據(jù)庫都需要預(yù)先定義模型,模型可以理解成數(shù)據(jù)模型,作為操作數(shù)據(jù)庫的媒介。
例如:
- 從數(shù)據(jù)庫讀取的數(shù)據(jù)會先保存到預(yù)先定義的模型對象,然后我們就可以從模型對象得到我們想要的數(shù)據(jù)。
- 插入數(shù)據(jù)到數(shù)據(jù)庫也是先新建一個模型對象,然后把想要保存的數(shù)據(jù)先保存到模型對象,然后把模型對象保存到數(shù)據(jù)庫。
在golang中g(shù)orm模型定義是通過struct實現(xiàn)的,這樣我們就可以通過gorm庫實現(xiàn)struct類型和mysql表數(shù)據(jù)的映射。
提示:gorm負(fù)責(zé)將對模型的讀寫操作翻譯成sql語句,然后gorm再把數(shù)據(jù)庫執(zhí)行sql語句后返回的結(jié)果轉(zhuǎn)化為我們定義的模型對象。
gorm介紹
GORM是Golang目前比較熱門的數(shù)據(jù)庫ORM操作庫,對開發(fā)者也比較友好,使用非常方便簡單,使用上主要就是把struct類型和數(shù)據(jù)庫表記錄進(jìn)行映射,操作數(shù)據(jù)庫的時候不需要直接手寫Sql代碼,
GORM庫github地址: https://github.com/go-gorm/gorm
gorm安裝
操作MySQL需要安裝兩個包:
- MySQL驅(qū)動包
- GORM包 使用go get命令安裝依賴包
//安裝MySQL驅(qū)動
go get -u gorm.io/driver/mysql
//安裝gorm包
go get -u gorm.io/gorm
go.md里邊,加了依賴
導(dǎo)入包
import (
"bytes"
"fmt"
_ "gorm.io/driver/mysql"
_ "gorm.io/gorm"
"sync"
"time"
)
gorm模型定義
gorm模型定義主要就是在struct類型定義的基礎(chǔ)上增加字段標(biāo)簽說明實現(xiàn),下面看個完整的例子。
假如有個sample表,表結(jié)構(gòu)如下
CREATE TABLE `sample` (
`id` int(11) NOT NULL COMMENT '主鍵' ,
`title` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '標(biāo)題' ,
`create_time` datetime NULL DEFAULT NULL COMMENT '創(chuàng)建時間' ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8 COLLATE=utf8_bin
ROW_FORMAT=DYNAMIC
;
模型定義如下
//字段注釋說明了gorm庫把struct字段轉(zhuǎn)換為表字段名長什么樣子。
type Sample struct {
Id int64 //表字段名為:id
Title string //表字段名為:title
//字段定義后面使用兩個反引號``包裹起來的字符串部分叫做標(biāo)簽定義,這個是golang的基礎(chǔ)語法,不同的庫會定義不同的標(biāo)簽,有不同的含義
CreateTime int64 `gorm:"column:create_time"` //表字段名為:create_time
}
默認(rèn)gorm對struct字段名使用Snake Case命名風(fēng)格轉(zhuǎn)換成mysql表字段名(需要轉(zhuǎn)換成小寫字母)。
根據(jù)gorm的默認(rèn)約定,上面例子只需要使用gorm:"column:create_time"標(biāo)簽定義為CreateTime字段指定表字段名,其他使用默認(rèn)值即可。
提示:Snake Case命名風(fēng)格,就是各個單詞之間用下劃線(_)分隔,例如: CreateTime的Snake Case風(fēng)格命名為create_time
3、gorm模型標(biāo)簽
通過上面的例子,大家看到可以通過類似gorm:"column:createtime"這樣的標(biāo)簽定義語法,定義struct字段的列名(表字段名)。
gorm標(biāo)簽語法:gorm:"標(biāo)簽定義"
標(biāo)簽定義部分,多個標(biāo)簽定義可以使用分號(;)分隔,例如定義列名:
gorm:"column:列名"
gorm常用標(biāo)簽如下:
標(biāo)簽 | 說明 | 例子 |
---|---|---|
column | 指定列名 | gorm:“column:createtime” |
primaryKey | 指定主鍵 | gorm:“column:id; PRIMARY_KEY” |
- | 忽略字段 | gorm:“-” 可以忽略struct字段,被忽略的字段不參與gorm的讀寫操作 |
定義表名
可以通過定義struct類型的TableName函數(shù)實現(xiàn)定義模型的表名
接上面的例子:
//設(shè)置表名,可以通過給Food struct類型定義 TableName函數(shù),返回一個字符串作為表名
func (v Sample) TableName() string {
return "sample"
}
建議:
默認(rèn)情況下都給模型定義表名,有時候定義模型只是單純的用于接收手寫sql查詢的結(jié)果,這個時候是不需要定義表名;手動通過gorm函數(shù)Table()指定表名,也不需要給模型定義TableName函數(shù)。
gorm.Model
GORM 定義一個 gorm.Model 結(jié)構(gòu)體,其包括字段 ID、CreatedAt、UpdatedAt、DeletedAt。
// gorm.Model 的定義
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
以將它嵌入到我們的結(jié)構(gòu)體中,就以包含這幾個字段,類似繼承的效果。
type User struct {
gorm.Model // 嵌入gorm.Model的字段
Name string
}
自動更新時間
GORM 約定使用 CreatedAt、UpdatedAt 追蹤創(chuàng)建/更新時間。
如果定義了這種字段,GORM 在創(chuàng)建、更新時會自動填充當(dāng)前時間。
要使用不同名稱的字段,您可以配置 autoCreateTime、autoUpdateTime 標(biāo)簽
如果想要保存 UNIX(毫/納)秒時間戳,而不是 time,只需簡單地將 time.Time 修改為 int 即可。
例子:
type User struct {
CreatedAt time.Time // 默認(rèn)創(chuàng)建時間字段, 在創(chuàng)建時,如果該字段值為零值,則使用當(dāng)前時間填充
UpdatedAt int // 默認(rèn)更新時間字段, 在創(chuàng)建時該字段值為零值或者在更新時,使用當(dāng)前時間戳秒數(shù)填充
Updated int64 `gorm:"autoUpdateTime:nano"` // 自定義字段, 使用時間戳填納秒數(shù)充更新時間
Updated int64 `gorm:"autoUpdateTime:milli"` //自定義字段, 使用時間戳毫秒數(shù)填充更新時間
Created int64 `gorm:"autoCreateTime"` //自定義字段, 使用時間戳秒數(shù)填充創(chuàng)建時間
}
gorm連接數(shù)據(jù)庫
gorm支持多種數(shù)據(jù)庫,這里主要介紹mysql,連接mysql主要有兩個步驟:
1)配置DSN (Data Source Name)
2)使用gorm.Open連接數(shù)據(jù)庫
1、配置DSN (Data Source Name)
gorm庫使用dsn作為連接數(shù)據(jù)庫的參數(shù),dsn翻譯過來就叫數(shù)據(jù)源名稱,用來描述數(shù)據(jù)庫連接信息。
一般都包含數(shù)據(jù)庫連接地址,賬號,密碼之類的信息。
DSN格式:
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]
mysql連接dsn例子:
//mysql dsn格式
//涉及參數(shù):
//username 數(shù)據(jù)庫賬號
//password 數(shù)據(jù)庫密碼
//host 數(shù)據(jù)庫連接地址,可以是Ip或者域名
//port 數(shù)據(jù)庫端口
//Dbname 數(shù)據(jù)庫名
username:password@tcp(host:port)/Dbname?charset=utf8&parseTime=True&loc=Local
//填上參數(shù)后的例子
//username = root
//password = 123456
//host = localhost
//port = 3306
//Dbname = tizi365
//后面K/V鍵值對參數(shù)含義為:
// charset=utf8 客戶端字符集為utf8
// parseTime=true 支持把數(shù)據(jù)庫datetime和date類型轉(zhuǎn)換為golang的time.Time類型
// loc=Local 使用系統(tǒng)本地時區(qū)
root:123456@tcp(localhost:3306)/tizi365?charset=utf8&parseTime=True&loc=Local
//gorm 設(shè)置mysql連接超時參數(shù)
//開發(fā)的時候經(jīng)常需要設(shè)置數(shù)據(jù)庫連接超時參數(shù),gorm是通過dsn的timeout參數(shù)配置
//例如,設(shè)置10秒后連接超時,timeout=10s
//下面是完成的例子
root:123456@tcp(localhost:3306)/tizi365?charset=utf8&parseTime=True&loc=Local&timeout=10s
//設(shè)置讀寫超時時間
// readTimeout - 讀超時時間,0代表不限制
// writeTimeout - 寫超時時間,0代表不限制
root:123456@tcp(localhost:3306)/tizi365?charset=utf8&parseTime=True&loc=Local&timeout=10s&readTimeout=30s&writeTimeout=60s
2、使用gorm.Open連接數(shù)據(jù)庫
有了上面配置的dsn參數(shù),就可以使用gorm連接數(shù)據(jù)庫,下面是連接數(shù)據(jù)庫的例子
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
//配置MySQL連接參數(shù)
username := "root" //賬號
password := "123456" //密碼
host := "127.0.0.1" //數(shù)據(jù)庫地址,可以是Ip或者域名
port := 3306 //數(shù)據(jù)庫端口
Dbname := "tizi365" //數(shù)據(jù)庫名
timeout := "10s" //連接超時,10秒
//拼接下dsn參數(shù), dsn格式可以參考上面的語法,這里使用Sprintf動態(tài)拼接dsn參數(shù),因為一般數(shù)據(jù)庫連接參數(shù),我們都是保存在配置文件里面,需要從配置文件加載參數(shù),然后拼接dsn。
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local&timeout=%s", username, password, host, port, Dbname, timeout)
//連接MYSQL, 獲得DB類型實例,用于后面的數(shù)據(jù)庫讀寫操作。
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("連接數(shù)據(jù)庫失敗, error=" + err.Error())
}
//延時關(guān)閉數(shù)據(jù)庫連接
defer db.Close()
}
3、gorm調(diào)試模式
為了方便調(diào)試,了解gorm操作到底執(zhí)行了怎么樣的sql語句,開發(fā)的時候需要打開調(diào)試日志,這樣gorm會打印出執(zhí)行的每一條sql語句。
使用Debug函數(shù)執(zhí)行查詢即可
result := db.Debug().Where("username = ?", "tizi365").First(&u)
4、gorm連接池
在高并發(fā)實踐中,為了提高數(shù)據(jù)庫連接的使用率,避免重復(fù)建立數(shù)據(jù)庫連接帶來的性能消耗,會經(jīng)常使用數(shù)據(jù)庫連接池技術(shù)來維護(hù)數(shù)據(jù)庫連接。
gorm自帶了數(shù)據(jù)庫連接池使用非常簡單只要設(shè)置下數(shù)據(jù)庫連接池參數(shù)即可。
數(shù)據(jù)庫連接池使用例子:
定義tools包,負(fù)責(zé)數(shù)據(jù)庫初始化工作(備注:借助連接池說明,一般在操作數(shù)據(jù)庫時,可以將數(shù)據(jù)庫連接單獨封裝成一個包)
// 定義一個工具包,用來管理gorm數(shù)據(jù)庫連接池的初始化工作。
package tools
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// 定義全局的db對象,我們執(zhí)行數(shù)據(jù)庫操作主要通過他實現(xiàn)。
var _db *gorm.DB
// 包初始化函數(shù),golang特性,每個包初始化的時候會自動執(zhí)行init函數(shù),這里用來初始化gorm。
func init() {
//配置MySQL連接參數(shù)
host := "192.168.56.121" //數(shù)據(jù)庫地址,可以是Ip或者域名
username := "root" //賬號
password := "123456" //密碼
port := 3306 //數(shù)據(jù)庫端口
Dbname := "store" //數(shù)據(jù)庫名
timeout := "10s" //連接超時,10秒
//拼接下dsn參數(shù), dsn格式可以參考上面的語法,這里使用Sprintf動態(tài)拼接dsn參數(shù),因為一般數(shù)據(jù)庫連接參數(shù),我們都是保存在配置文件里面,需要從配置文件加載參數(shù),然后拼接dsn。
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local&timeout=%s", username, password, host, port, Dbname, timeout)
// 聲明err變量,下面不能使用:=賦值運算符,否則_db變量會當(dāng)成局部變量,導(dǎo)致外部無法訪問_db變量
var err error
//連接MYSQL, 獲得DB類型實例,用于后面的數(shù)據(jù)庫讀寫操作。
_db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("連接數(shù)據(jù)庫失敗, error=" + err.Error())
}
sqlDB, _ := _db.DB()
//設(shè)置數(shù)據(jù)庫連接池參數(shù)
sqlDB.SetMaxOpenConns(100) //設(shè)置數(shù)據(jù)庫連接池最大連接數(shù)
sqlDB.SetMaxIdleConns(20) //連接池最大允許的空閑連接數(shù),如果沒有sql任務(wù)需要執(zhí)行的連接數(shù)大于20,超過的連接會被連接池關(guān)閉。
}
// 獲取gorm db對象,其他包需要執(zhí)行數(shù)據(jù)庫查詢的時候,只要通過tools.getDB()獲取db對象即可。
// 不用擔(dān)心協(xié)程并發(fā)使用同樣的db對象會共用同一個連接,db對象在調(diào)用他的方法的時候會從數(shù)據(jù)庫連接池中獲取新的連接
func GetDB() *gorm.DB {
return _db
}
使用例子:
package main
//導(dǎo)入tools包
import tools
func main() {
//獲取DB
db := tools.GetDB()
//執(zhí)行數(shù)據(jù)庫查詢操作
u := User{}
//自動生成sql: SELECT * FROM `users` WHERE (username = 'tizi365') LIMIT 1
db.Where("username = ?", "tizi365").First(&u)
}
注意:使用連接池技術(shù)后,千萬不要使用完db后調(diào)用db.Close關(guān)閉數(shù)據(jù)庫連接,這樣會導(dǎo)致整個數(shù)據(jù)庫連接池關(guān)閉,導(dǎo)致連接池沒有可用的連接。
CRUD操作
gorm 是一個 Go 語言的 ORM(Object Relational Mapping)庫,可以方便地操作數(shù)據(jù)庫。下面是 gorm 模塊的使用步驟:
插入數(shù)據(jù)
func InsertDemo() {
// 創(chuàng)建 Sample 實例
Sample := Sample{Id: 1, Title: "張三", CreateTime: time.Now()}
//獲取DB
MysqlDB := tools.GetDB()
// 添加數(shù)據(jù)
MysqlDB.Create(&Sample)
// 獲取添加后的自增 ID
fmt.Println(Sample.Id)
}
執(zhí)行結(jié)果
查詢數(shù)據(jù)
func SearchDemo() {
//獲取DB
db := tools.GetDB()
//獲取第一個 Sample 記錄
var firstSample Sample
db.First(&firstSample)
fmt.Println("('%d','%s','%s');", firstSample.Id, firstSample.Title, firstSample.CreateTime)
// 條件查詢
var sample Sample
db.Where("title = ?", "張三").First(&sample)
fmt.Println("('%d','%s','%s');", firstSample.Id, firstSample.Title, firstSample.CreateTime)
// 查詢所有 Sample 記錄
var samples []Sample
db.Find(&samples)
}
更新數(shù)據(jù)
func UpdateDemo() {
//獲取DB
db := tools.GetDB()
//獲取第一個 Sample 記錄
var sample Sample
db.Where("title = ?", "張三").First(&sample)
fmt.Println("('%d','%s','%s');", sample.Id, sample.Title, sample.CreateTime)
// 更新指定字段
db.Model(&sample).Update("age", 20)
// 更新多個字段
db.Model(&sample).Updates(Sample{Id: 20, Title: "李四"})
}
刪除數(shù)據(jù)
func DeleteDemo() {
//獲取DB
db := tools.GetDB()
//獲取第一個 Sample 記錄
var sample Sample
db.Where("title = ?", "張三").First(&sample)
fmt.Println("('%d','%s','%s');", sample.Id, sample.Title, sample.CreateTime)
// 刪除 sample 記錄
db.Delete(&sample)
// 根據(jù)條件刪除多個記錄
db.Where("title = ?", "張三").Delete(&Sample{})
}
這些是 gorm 模塊的基本使用方法,可以根據(jù)實際需求進(jìn)行調(diào)整和擴展。
go與mysql數(shù)據(jù)類型關(guān)系
mysql日期時間格式
go 存儲 mysql TIMESTAMP格式
存:
type TestTime struct{
CreatedAt time.Time
}
m:=new(TestTime)
m.CreatedAt:=time.Now()
取:
go orm 取TestTime結(jié)構(gòu)體數(shù)據(jù)
str:=orm_data.CreatedAt.Format("2006-01-02 15:04:05")
str == "2019-08-27 09:35:13"
高并發(fā)實操: 消息隊列削峰解耦+ 批量寫入DB
為什么要使用消息隊列
三個最主要的應(yīng)用場景:解耦、異步、削峰
- 削峰填谷(最主要的作用)可以削去到達(dá)系統(tǒng)的峰值流量,讓業(yè)務(wù)邏輯的處理更加緩和;但是會造成請求處理的延遲
- 異步處理可以簡化業(yè)務(wù)流程中的步驟,提升系統(tǒng)性能;
- 需要分清同步流程和異步流程的邊界
- 消息存在著丟失的風(fēng)險
- 解耦合可以將系統(tǒng)和系統(tǒng)解耦開,這樣兩個系統(tǒng)的任何變更都不會影響到另一個系統(tǒng)
削峰
傳統(tǒng)模式:并發(fā)量大的時候,所有的請求直接懟到數(shù)據(jù)庫,造成數(shù)據(jù)庫連接異常
消息隊列模式:系統(tǒng)A慢慢的按照數(shù)據(jù)庫能處理的并發(fā)量,從消息隊列中慢慢拉取消息。
在生產(chǎn)中,這個短暫的高峰期積壓是允許的。
解耦
傳統(tǒng)模式:系統(tǒng)間耦合性太強,如下圖所示,
系統(tǒng)A在代碼中直接調(diào)用系統(tǒng)B和系統(tǒng)C的代碼,如果將來D系統(tǒng)接入,系統(tǒng)A還需要修改代碼,過于麻煩!
消息隊列模式:將消息寫入消息隊列,需要消息的系統(tǒng)自己從消息隊列中訂閱,從而系統(tǒng)A不需要做任何修改
異步
傳統(tǒng)模式:一些非必要的業(yè)務(wù)邏輯以同步的方式運行,太耗費時間。
消息隊列模式: 將消息寫入消息隊列,非必要的業(yè)務(wù)邏輯以異步的方式運行,加快相應(yīng)速度。
實操:用GO實現(xiàn)消息隊列削峰解耦
用GO實現(xiàn)消息隊列削峰解耦,參考代碼如下:
package batchprocess
// 創(chuàng)建生產(chǎn)者
const SAMPLE_TOPIC = "sample"
func ProducerStart() {
producer, err := rocketmq.NewProducer(
producer.WithNameServer([]string{NAME_NODE}),
producer.WithRetry(2),
)
...
// 發(fā)送消息,無限循環(huán)
for i := 0; ; i++ {
sample := Sample{Id: int64(i + 100), Title: "張三", CreateTime: time.Now()}
//序列化
json, err := json.Marshal(&sample)
msg := &primitive.Message{
Topic: SAMPLE_TOPIC,
Body: []byte(json),
}
res, err := producer.SendSync(context.Background(), msg)
if err != nil {
log.Printf("send Sample error: %v\n", err)
} else {
log.Printf("send Sample success: %v\n", res)
}
time.Sleep(time.Second)
}
}
func ConsumerStart() {
// 創(chuàng)建消費者
c, err := rocketmq.NewPushConsumer(
consumer.WithNameServer([]string{NAME_NODE}),
consumer.WithGroupName("test-group"),
)
err = c.Subscribe(SAMPLE_TOPIC, consumer.MessageSelector{},
func(ctx context.Context, msgs ...*primitive.MessageExt) (consumer.ConsumeResult, error) {
for _, msg := range msgs {
var sample Sample
err := json.Unmarshal(msg.Body, &sample)
if err != nil {
fmt.Println(err)
}
fmt.Println(sample)
messageMysqlChan <- sample // 加入通道
log.Printf("receive Sample: topic=%s, body=%s\n",
msg.Topic, string(msg.Body))
}
return consumer.ConsumeSuccess, nil
})
wg.Wait()
}
執(zhí)行的效果如下(后面尼恩會在 穿透云原生視頻中,進(jìn)行詳細(xì)介紹):
為什么應(yīng)該使用批量插入來提高M(jìn)ySQL性能?
MySQL是一種常用的開源關(guān)系數(shù)據(jù)庫管理系統(tǒng)(RDBMS),常用于建立網(wǎng)站和應(yīng)用程序后端的數(shù)據(jù)存儲和管理系統(tǒng)。但隨著數(shù)據(jù)量的增大,MySQL的性能也會逐漸下降,此時需要使用批量插入來提高M(jìn)ySQL性能。
批量插入是指一次性向MySQL數(shù)據(jù)庫中插入多條記錄,相對于逐個插入單條記錄,批量插入可以大大提高M(jìn)ySQL的性能。那么,為什么應(yīng)該使用批量插入呢?以下是幾個原因。
- 減少網(wǎng)絡(luò)往返次數(shù)
MySQL是一種客戶端/服務(wù)器模式的數(shù)據(jù)庫,在客戶端插入一條記錄時,需要與MySQL服務(wù)器建立一次網(wǎng)絡(luò)連接,而這個過程將耗費時間和帶寬。如果每插入一條記錄就要建立一次網(wǎng)絡(luò)連接,那么對于大批量的數(shù)據(jù)插入將會非常低效。通過批量插入,可以減少網(wǎng)絡(luò)連接次數(shù),從而提高M(jìn)ySQL的性能。
- 減少SQL語句的解析次數(shù)
MySQL中,每條SQL語句都需要進(jìn)行解析并編譯成執(zhí)行計劃,這個過程也需要耗費時間。如果逐個插入單條記錄,那么每條SQL語句都需要解析和編譯,而使用批量插入,只需要解析和編譯一次SQL語句即可,從而減少了SQL語句的解析次數(shù),提高M(jìn)ySQL的性能。
- 減少磁盤I/O操作
MySQL將數(shù)據(jù)存儲在磁盤上,每次向磁盤寫入一條記錄都將會進(jìn)行一次磁盤I/O操作。如果逐個插入單條記錄,那么每次插入都將會進(jìn)行一次磁盤I/O操作,而使用批量插入,多條記錄將會一起寫入磁盤,從而減少了磁盤I/O操作,提高了MySQL的性能。
- 減少鎖的競爭
在MySQL中,插入一條記錄時需要獲取表級鎖或行級鎖,如果逐個插入單條記錄,那么每次插入都將會競爭鎖資源,從而影響MySQL的性能。使用批量插入時,多條記錄被看做一個事務(wù),只需要獲取一次鎖,從而減少了鎖的競爭,提高了MySQL的性能。
以上是使用批量插入來提高M(jìn)ySQL性能的幾個原因。但是,批量插入也存在一些缺點,例如批量插入一起錯誤時很難進(jìn)行回滾操作,可能導(dǎo)致數(shù)據(jù)的不一致性。因此,在使用批量插入時,需謹(jǐn)慎考慮。
總而言之,使用批量插入是提高M(jìn)ySQL性能的有效方式,可以減少網(wǎng)絡(luò)連接次數(shù)、SQL語句的解析次數(shù)、磁盤I/O操作和鎖的競爭,從而提高M(jìn)ySQL的性能。但是,在使用批量插入時也需要注意一些可能的缺陷。
實操:用GO實現(xiàn)批量寫入
package batchprocess
func StartBatchWriter() {
messageMysqlChan = make(chan Sample, 100)
insertedFlags = make(map[int64]bool)
go batchMessageReceive()
go batchStartTimer()
}
/*
接收消息的邏輯,只負(fù)責(zé)接收消息
*/
func batchMessageReceive() {
for {
select {
case oneMessage := <-messageMysqlChan:
mesLock.Lock()
tmpMessage = append(tmpMessage, oneMessage)
mesLock.Unlock()
}
}
}
func batch(batchMessage []Sample) {
if len(batchMessage) == 0 {
fmt.Print(">>>>>>>>> 空消息")
return
}
var buffer bytes.Buffer
sql := "insert into `sample` (`id`,`title`,`create_time`) values"
if _, err := buffer.WriteString(sql); err != nil {
fmt.Print(err.Error())
}
for index, value := range batchMessage {
/*查看元素在集合中是否存在 */
_, ok := insertedFlags[value.Id] /*如果確定是處理過 */
if ok {
continue
} else {
insertedFlags[value.Id] = true
}
if index == len(batchMessage)-1 {
buffer.WriteString(fmt.Sprintf("('%d','%s','%s');", value.Id, value.Title, value.CreateTime.Format("2006-01-02 15:04:05")))
} else {
buffer.WriteString(fmt.Sprintf("('%d','%s','%s'),", value.Id, value.Title, value.CreateTime.Format("2006-01-02 15:04:05")))
}
}
//獲取DB
MysqlDB := tools.GetDB()
err := MysqlDB.Exec(buffer.String()).Error
if err != nil {
fmt.Println("插入數(shù)據(jù)庫失?。?, err.Error())
} else {
fmt.Printf("插入數(shù)據(jù)庫成功,一共插入的條數(shù): %d:", len(batchMessage))
fmt.Println("祝賀")
}
return
}
執(zhí)行效果
執(zhí)行的效果如下(后面尼恩會在 穿透云原生視頻中,進(jìn)行詳細(xì)介紹):
Golang GC垃圾回收器
Cache 和 Buffer的區(qū)別
在理解垃圾回收之前,我們先理解一下Cache 和 Buffer,這兩個都是緩存,這兩者之間有什么區(qū)別呢?
buffer:緩沖
用于存儲速度不同步的設(shè)備或優(yōu)先級不同的設(shè)備之間傳輸數(shù)據(jù);通過buffer可以減少進(jìn)程間通信需要等待的時間,當(dāng)存儲速度快的設(shè)備與存儲速度慢的設(shè)備進(jìn)行通信時,存儲慢的數(shù)據(jù)先把數(shù)據(jù)存放到buffer,達(dá)到一定程度存儲快的設(shè)備再讀取buffer的數(shù)據(jù),在此期間存儲快的設(shè)備CPU可以做其他的事情。
A buffer is something that has yet to be "written" to disk.
cache:緩存
是高速緩存,是位于CPU和主內(nèi)存之間的容量較小但速度很快的存儲器,因為CPU的速度遠(yuǎn)遠(yuǎn)高于主內(nèi)存的速度,CPU從內(nèi)存中讀取數(shù)據(jù)需等待很長的時間,而 Cache保存著CPU剛用過的數(shù)據(jù)或循環(huán)使用的部分?jǐn)?shù)據(jù),這時從Cache中讀取數(shù)據(jù)會更快,減少了CPU等待的時間,提高了系統(tǒng)的性能。
A cache is something that has been "read" from the disk and stored for later use.
buffer是用于存放將要輸出到disk(塊設(shè)備)的數(shù)據(jù),進(jìn)行流量整形,把突發(fā)的大數(shù)量較小規(guī)模的 I/O 整理成平穩(wěn)的小數(shù)量較大規(guī)模的 I/O,以減少響應(yīng)次數(shù),而cache是存放從disk上讀出的數(shù)據(jù),為了彌補高速設(shè)備和低速設(shè)備的鴻溝而引入的中間層,最終起到加快訪問速度的作用。。二者都是為提高IO性能而設(shè)計的。
而Go標(biāo)準(zhǔn)庫Buffer是一個可變大小的字節(jié)緩沖區(qū),可以用Wirte和Read方法操作它.
Golang GC發(fā)展史
通常在編程中的垃圾指內(nèi)存中不再使用的內(nèi)存區(qū)域,自動發(fā)現(xiàn)與釋放這種內(nèi)存區(qū)域的過程就是垃圾回收。
內(nèi)存資源是有限的,而垃圾回收可以讓內(nèi)存重復(fù)使用,并且減輕開發(fā)者對內(nèi)存管理的負(fù)擔(dān),減少程序中的內(nèi)存問題。
我們透過這個來看下Go垃圾回收發(fā)展史:
- go1.1,提高效率和垃圾回收精確度。
- go1.3,提高了垃圾回收的精確度。
- go1.4,之前版本的runtime大部分是使用C寫的,這個版本大量使用Go進(jìn)行了重寫,讓GC有了掃描stack的能力,進(jìn)一步提高了垃圾回收的精確度。
- go1.5,目標(biāo)是降低GC延遲,采用了并發(fā)標(biāo)記和并發(fā)清除,三色標(biāo)記,write barrier,以及實現(xiàn)了更好的回收器調(diào)度,設(shè)計文檔1,文檔2,以及這個版本的[Go talk]。
- go1.6,小優(yōu)化,當(dāng)程序使用大量內(nèi)存時,GC暫停時間有所降低。
- go1.7,小優(yōu)化,當(dāng)程序有大量空閑goroutine,stack大小波動比較大時,GC暫停時間有顯著降低。
- go1.8,write barrier切換到hybrid write barrier,以消除STW中的re-scan,把STW的最差情況降低到50us,設(shè)計文檔。
混合屏障的優(yōu)勢在于它允許堆棧掃描永久地使堆棧變黑(沒有STW并且沒有寫入堆棧的障礙),這完全消除了堆棧重新掃描的需要,從而消除了對堆棧屏障的需求。重新掃描列表。特別是堆棧障礙在整個運行時引入了顯著的復(fù)雜性,并且干擾了來自外部工具(如GDB和基于內(nèi)核的分析器)的堆棧遍歷。
此外,與Dijkstra風(fēng)格的寫屏障一樣,混合屏障不需要讀屏障,因此指針讀取是常規(guī)的內(nèi)存讀取; 它確保了進(jìn)步,因為物體單調(diào)地從白色到灰色再到黑色。
混合屏障的缺點很小。它可能會導(dǎo)致更多的浮動垃圾,因為它會在標(biāo)記階段的任何時刻保留從根(堆棧除外)可到達(dá)的所有內(nèi)容。然而,在實踐中,當(dāng)前的Dijkstra障礙可能幾乎保留不變?;旌掀琳线€禁止某些優(yōu)化:特別是,如果Go編譯器可以靜態(tài)地顯示指針是nil,則Go編譯器當(dāng)前省略寫屏障,但是在這種情況下混合屏障需要寫屏障。這可能會略微增加二進(jìn)制大小。
- go1.9,提升指標(biāo)主要是:
- 過去
runtime.GC
,debug.SetGCPercent
, 和debug.FreeOSMemory
都不能觸發(fā)并發(fā)GC,他們觸發(fā)的GC都是阻塞的,go1.9可以了,變成了在垃圾回收之前只阻塞調(diào)用GC的goroutine。 -
debug.SetGCPercent
只在有必要的情況下才會觸發(fā)GC。
- go.1.10,小優(yōu)化,加速了GC,程序應(yīng)當(dāng)運行更快一點點。
- go1.12,顯著提高了堆內(nèi)存存在大碎片情況下的sweeping性能,能夠降低GC后立即分配內(nèi)存的延遲。
還有 5W字待發(fā)布
本文,僅僅是《Golang 圣經(jīng)》 的第一部分。
《Golang 圣經(jīng)》后面的內(nèi)容 更加精彩,涉及到高并發(fā)、分布式微服務(wù)架構(gòu)、 WEB開發(fā)架構(gòu),具體請關(guān)注進(jìn)展,請關(guān)注《技術(shù)自由圈》 公眾號。
如果需要領(lǐng)取 《Golang 圣經(jīng)》, 請關(guān)注《技術(shù)自由圈》 公眾號,發(fā)送暗號 “領(lǐng)電子書” 。
最后,如果學(xué)習(xí)過程中遇到問題,可以來尼恩的 萬人高并發(fā)社群中交流。
參考資料
- Tracing Garbage Collection - wikipedia
- On-the-fly Garbage Collection: an exercise in cooperation.
- Garbage Collection
- Tracing Garbage Collection
- Copying Garbage Collection
- Generational Garbage Collection
- Golang Gc Talk
- Eliminate Rescan
技術(shù)自由的實現(xiàn)路徑 PDF:
實現(xiàn)你的 架構(gòu)自由:
《吃透8圖1模板,人人可以做架構(gòu)》
《10Wqps評論中臺,如何架構(gòu)?B站是這么做的?。?!》
《阿里二面:千萬級、億級數(shù)據(jù),如何性能優(yōu)化? 教科書級 答案來了》
《峰值21WQps、億級DAU,小游戲《羊了個羊》是怎么架構(gòu)的?》
《100億級訂單怎么調(diào)度,來一個大廠的極品方案》
《2個大廠 100億級 超大流量 紅包 架構(gòu)方案》
… 更多架構(gòu)文章,正在添加中
實現(xiàn)你的 響應(yīng)式 自由:
《響應(yīng)式圣經(jīng):10W字,實現(xiàn)Spring響應(yīng)式編程自由》
這是老版本 《Flux、Mono、Reactor 實戰(zhàn)(史上最全)》
實現(xiàn)你的 spring cloud 自由:
《Spring cloud Alibaba 學(xué)習(xí)圣經(jīng)》
《分庫分表 Sharding-JDBC 底層原理、核心實戰(zhàn)(史上最全)》
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之間混亂關(guān)系(史上最全)》
實現(xiàn)你的 linux 自由:
《Linux命令大全:2W多字,一次實現(xiàn)Linux自由》
實現(xiàn)你的 網(wǎng)絡(luò) 自由:
《TCP協(xié)議詳解 (史上最全)》
《網(wǎng)絡(luò)三張表:ARP表, MAC表, 路由表,實現(xiàn)你的網(wǎng)絡(luò)自由?。 ?/p>
實現(xiàn)你的 分布式鎖 自由:
《Redis分布式鎖(圖解 - 秒懂 - 史上最全)》
《Zookeeper 分布式鎖 - 圖解 - 秒懂》
實現(xiàn)你的 王者組件 自由:
《隊列之王: Disruptor 原理、架構(gòu)、源碼 一文穿透》
《緩存之王:Caffeine 源碼、架構(gòu)、原理(史上最全,10W字 超級長文)》
《緩存之王:Caffeine 的使用(史上最全)》
《Java Agent 探針、字節(jié)碼增強 ByteBuddy(史上最全)》
實現(xiàn)你的 面試題 自由:
4000頁《尼恩Java面試寶典 》 40個專題文章來源:http://www.zghlxwxcb.cn/news/detail-458274.html
以上尼恩 架構(gòu)筆記、面試題 的PDF文件更新,請到下面《技術(shù)自由圈》公號取↓↓↓文章來源地址http://www.zghlxwxcb.cn/news/detail-458274.html
到了這里,關(guān)于Go學(xué)習(xí)圣經(jīng):隊列削峰+批量寫入 超高并發(fā)原理和實操的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!