1.鎖
????????有時候我們的代碼中可能會存在多個 goroutine 同時操作一個資源(臨界區)的情況,這種情況下就會發生競態問題(數據競態)。
? ? ? ? (1)、互斥鎖;(2)、讀寫互斥鎖;(3)、sync.WaitGroup;(4)、sync.Once;(5)、sync.Map;(6)、atomic包
var (x int64m sync.Mutex // 互斥鎖rwMutex sync.RWMutexmutex sync.Mutex
)func Mutex() {wg.Add(2)go add()go add()wg.Wait()fmt.Println(x)
}func add() {for i := 0; i < 5000; i++ {m.Lock() // 修改x前加鎖x = x + 1m.Unlock() // 改完解鎖}wg.Done()
}func RWMutex() {/*讀寫鎖分為兩種:讀鎖和寫鎖。當一個 goroutine 獲取到讀鎖之后,其他的 goroutine 如果是獲取讀鎖會繼續獲得鎖,如果是獲取寫鎖就會等待;而當一個 goroutine 獲取寫鎖之后,其他的 goroutine 無論是獲取讀鎖還是寫鎖都會等待。*/// 使用互斥鎖,10并發寫,1000并發讀do(writeWithLock, readWithLock, 10, 1000) // x:10 cost:1.466500951s// 使用讀寫互斥鎖,10并發寫,1000并發讀do(writeWithRWLock, readWithRWLock, 10, 1000) // x:10 cost:117.207592ms/*從最終的執行結果可以看出,使用讀寫互斥鎖在讀多寫少的場景下能夠極大地提高程序的性能。不過需要注意的是如果一個程序中的讀操作和寫操作數量級差別不大,那么讀寫互斥鎖的優勢就發揮不出來。*/
}// writeWithLock 使用互斥鎖的寫操作
func writeWithLock() {mutex.Lock() // 加互斥鎖x = x + 1time.Sleep(10 * time.Millisecond) // 假設讀操作耗時10毫秒mutex.Unlock() // 解互斥鎖wg.Done()
}// readWithLock 使用互斥鎖的讀操作
func readWithLock() {mutex.Lock() // 加互斥鎖time.Sleep(time.Millisecond) // 假設讀操作耗時1毫秒mutex.Unlock() // 釋放互斥鎖wg.Done()
}// writeWithLock 使用讀寫互斥鎖的寫操作
func writeWithRWLock() {rwMutex.Lock() // 加寫鎖x = x + 1time.Sleep(10 * time.Millisecond) // 假設讀操作耗時10毫秒rwMutex.Unlock() // 釋放寫鎖wg.Done()
}// readWithRWLock 使用讀寫互斥鎖的讀操作
func readWithRWLock() {rwMutex.RLock() // 加讀鎖time.Sleep(time.Millisecond) // 假設讀操作耗時1毫秒rwMutex.RUnlock() // 釋放讀鎖wg.Done()
}func do(wf, rf func(), wc, rc int) {start := time.Now()// wc個并發寫操作for i := 0; i < wc; i++ {wg.Add(1)go wf()}// rc個并發讀操作for i := 0; i < rc; i++ {wg.Add(1)go rf()}wg.Wait()cost := time.Since(start)fmt.Printf("x:%v cost:%v\n", x, cost)
}func WaitGroup() {/*WaitGroup在代碼中生硬的使用time.Sleep肯定是不合適的,Go語言中可以使用sync.WaitGroup來實現并發任務的同步。 sync.WaitGroup有以下幾個方法:方法名 功能func (wg * WaitGroup) Add(delta int) 計數器+delta(wg *WaitGroup) Done() 計數器-1(wg *WaitGroup) Wait() 阻塞直到計數器變為0sync.WaitGroup內部維護著一個計數器,計數器的值可以增加和減少。例如當我們啟動了 N 個并發任務時,就將計數器值增加N。每個任務完成時通過調用 Done 方法將計數器減1。通過調用 Wait 來等待并發任務執行完,當計數器值為 0 時,表示所有并發任務已經完成。*/wg.Add(1)go hello()wg.Wait()fmt.Println("ni hao ya !!!")
}
func hello() {defer wg.Done()fmt.Println("hello world....")
}
2.SyncOnce
? ? ?? ?在某些場景下我們需要確保某些操作即使在高并發的場景下也只會被執行一次,例如只加載一次配置文件等。
Go語言中的sync包中提供了一個針對只執行一次場景的解決方案——sync.Once,sync.Once只有一個Do方法,
延遲一個開銷很大的初始化操作到真正用到它的時候再執行是一個很好的實踐。因為預先初始化一個變量
(比如在init函數中完成初始化)會增加程序的啟動耗時,而且有可能實際執行過程中這個變量沒有用上,那么這個初始化操作就不是必須要做的。
func SyncOnce() {GetInstance() //并發安全的單例模式
}type singleton struct{}var instance *singleton
var once sync.Oncefunc GetInstance() *singleton {once.Do(func() {instance = &singleton{}})return instance
}
3.并發安全的map
????????sync.Map 是 Go 語言中提供的一個并發安全的 map 類型,它是 Go 語言中 map 的替代品,它是并發安全的,并且它是通過引入 sync.RWMutex 來實現的。
在 Go 語言中,map 是無序的,而 sync.Map 是基于 sync.RWMutex 實現的,所以它是并發安全的。
sync.Map 的底層實現是通過哈希表來實現的,哈希表的底層實現是通過數組+鏈表來實現的,所以它是通過數組+鏈表來實現的,所以它是通過數組+鏈表來實現的,所以它是通過數組+鏈表來實現的,所以它是通過數組+鏈表來實現的,所以它是通過數組+鏈表來實現的,所以它是通過數組+鏈表來實現的,所以它是通過數組+鏈表來實現的,所以它是通過數組+鏈表來實現的,所以它是通過數組+鏈表來。
var smp = sync.Map{}func SyncMap() {wg := sync.WaitGroup{}// 對m執行20個并發的讀寫操作for i := 0; i < 20; i++ {wg.Add(1)go func(n int) {key := strconv.Itoa(n)smp.Store(key, n) // 存儲key-valuevalue, _ := smp.Load(key) // 根據key取值fmt.Printf("k=:%v,v:=%v\n", key, value)wg.Done()}(i)}wg.Wait()
}
4.atomic
????????atomic包提供了底層的原子級內存操作,對于同步算法的實現很有用。這些函數必須謹慎地保證正確使用。
除了某些特殊的底層應用,使用通道或者 sync 包的函數/類型實現同步更好。
針對整數數據類型(int32、uint32、int64、uint64)我們還可以使用原子操作來保證并發安全,通常直接使用原子操作比使用鎖操作效率更高。Go語言中原子操作由內置的標準庫sync/atomic提供
func (o *TrinoQueryRunningExporter) AcquireLock() bool {
return atomic.CompareAndSwapInt64(&o.Lock, 0, 1)
}
func (o *TrinoQueryRunningExporter) ReleaseLock() {
atomic.CompareAndSwapInt64(&o.Lock, 1, 0)
}
這里的鎖就是原子操作的使用案例。
func Atomic() {c1 := CommonCounter{} // 非并發安全test(c1)c2 := MutexCounter{} // 使用互斥鎖實現并發安全test(&c2)c3 := AtomicCounter{} // 并發安全且比互斥鎖效率更高test(&c3)
}type Counter interface {Inc()Load() int64
}// 普通版
type CommonCounter struct {counter int64
}func (c CommonCounter) Inc() {c.counter++
}func (c CommonCounter) Load() int64 {return c.counter
}// 互斥鎖版
type MutexCounter struct {counter int64lock sync.Mutex
}func (m *MutexCounter) Inc() {m.lock.Lock()defer m.lock.Unlock()m.counter++
}func (m *MutexCounter) Load() int64 {m.lock.Lock()defer m.lock.Unlock()return m.counter
}// 原子操作版
type AtomicCounter struct {counter int64
}func (a *AtomicCounter) Inc() {atomic.AddInt64(&a.counter, 1)
}func (a *AtomicCounter) Load() int64 {return atomic.LoadInt64(&a.counter)
}func test(c Counter) {var wg sync.WaitGroupstart := time.Now()for i := 0; i < 1000; i++ {wg.Add(1)go func() {c.Inc()wg.Done()}()}wg.Wait()end := time.Now()fmt.Println(c.Load(), end.Sub(start))
}
5.error
????????Go 語言中把錯誤當成一種特殊的值來處理,不支持其他語言中使用try/catch捕獲異常的方式
Go 語言中使用一個名為 error 接口來表示錯誤類型。
error 接口只包含一個方法——Error,這個函數需要返回一個描述錯誤信息的字符串。當一個函數或方法需要返回錯誤時,我們通常是把錯誤作為最后一個返回值。
我們可以根據需求自定義 error,最簡單的方式是使用errors 包提供的New函數創建一個錯誤。
errors.New
當我們需要傳入格式化的錯誤描述信息時,使用fmt.Errorf是個更好的選擇。
func ErrorNew() {id := -1var err errorif id < 0 {err = errors.New("無效的id")fmt.Printf("id error:%v \n", err)}fmt.Println(fmt.Errorf("查詢數據庫失敗,v err:%v \n", err))//但是上面的方式會丟失原有的錯誤類型,只拿到錯誤描述的文本信息。//為了不丟失函數調用的錯誤鏈,使用fmt.Errorf時搭配使用特殊的格式化動詞%w,可以實現基于已有的錯誤再包裝得到一個新的錯誤。fmt.Println(fmt.Errorf("查詢數據庫失敗,w err:%w \n", err))//自定義結構體類型,可以自己定義結構體類型,實現 error`接口oper := &OpError{Op: "update",}fmt.Println(oper.Error())
}// Error OpError 類型實現error接口
func (e *OpError) Error() string {return fmt.Sprintf("無權執行%s操作", e.Op)
}// OpError 自定義結構體類型
type OpError struct {Op string
}
6.類型轉換
????????strconv包實現了基本數據類型與其字符串表示的轉換,主要有以下常用函數: Atoi()、Itoa()、parse系列、format系列、append系列。
【擴展閱讀】這是C語言遺留下的典故。C語言中沒有string類型而是用字符數組(array)表示字符串,所以Itoa對很多C系的程序員很好理解。
strconv.Atoi ?strconv.Itoa
Parse類函數用于轉換字符串為給定類型的值:ParseBool()、ParseFloat()、ParseInt()、????????ParseUint()。
Format系列函數實現了將給定類型數據格式化為string類型數據的功能
func TypeTransfer() {s1 := "100"i1, err := strconv.Atoi(s1) // Atoi()函數用于將字符串類型的整數轉換為int類型if err != nil {fmt.Println("can't convert to int")} else {fmt.Printf("type:%T value:%#v\n", i1, i1) //type:int value:100}v := int64(-42)s10 := strconv.FormatInt(v, 10)fmt.Printf("%T, %v\n", s10, s10)s16 := strconv.FormatInt(v, 16)fmt.Printf("%T, %v\n", s16, s16)
}
7.ini
????????在Go語言中,init()函數是一種特殊的函數,用于在程序啟動時自動執行一次。它的存在為我們提供了一種機制,可以在程序啟動時進行一些必要的初始化操作,為程序的正常運行做好準備
go語言中init函數用于包(package)的初始化,該函數是go語言的一個重要特性。
(1) init函數是用于程序執行前做包的初始化的函數,比如初始化包里的變量等
(2) 每個包可以擁有多個init函數
(3) 包的每個源文件也可以擁有多個init函數
(4) 同一個包中多個init函數的執行順序go語言沒有明確的定義(說明)
(5) 不同包的init函數按照包導入的依賴關系決定該初始化函數的執行順序
(6) init函數不能被其他函數調用,而是在main函數執行之前,自動被調用
init函數和main函數的異同:
相同點:
兩個函數在定義時不能有任何的參數和返回值,且Go程序自動調用。
不同點:
init可以應用于任意包中,且可以重復定義多個。
main函數只能用于main包中,且只能定義一個。
go中包的初始化順序:
首先初始化包內聲明的變量
之后調用 init 函數
最后調用 main 函數
兩個函數的執行順序:
對同一個go文件的init()調用順序是從上到下的。
對同一個package中不同文件是按文件名字符串比較“從小到大”順序調用各文件中的init()函數。
對于不同的package,如果不相互依賴的話,按照main包中"先import的后調用"的順序調用其包中的init(),如果package存在依賴,則先調用最早被依賴的package中的init(),最后調用main函數。
如果init函數中使用了println()或者print()你會發現在執行過程中這兩個不會按照你想象中的順序執行。這兩個函數官方只推薦在測試環境中使用,對于正式環境不要使用。
https://www.cnblogs.com/chenjiazhan/p/17473207.html
https://www.cnblogs.com/XiaoXiaoShuai-/p/14642055.html
init 函數的用途
(1) 初始化全局變量
(2) 執行一些必要的驗證操作
注意:
init 函數不能被顯式調用
init 函數只執行一次
避免在 init 函數中執行耗時操作
func Init() {fmt.Println("Init Test!!!")
}func init() {fmt.Println("hello world")
}
8.test
????????Go語言中的測試依賴go test命令。編寫測試代碼和編寫普通的Go代碼過程是類似的,并不需要學習新的語法、規則或工具。
go test命令是一個按照一定約定和組織的測試代碼的驅動程序。在包目錄內,所有以_test.go為后綴名的源代碼文件都是go test測試的一部分,不會被go build編譯到最終的可執行文件中。
在*_test.go文件中有三種類型的函數,單元測試函數、基準測試函數和示例函數。
類型 ?格式 作用
測試函數 ? ?函數名前綴為Test 測試程序的一些邏輯行為是否正確
基準函數 ? ?函數名前綴為Benchmark ? ?測試函數的性能
示例函數 ? ?函數名前綴為Example ?為文檔提供示例文檔
go test命令會遍歷所有的*_test.go文件中符合上述命名規則的函數,然后生成一個臨時的main包用于調用相應的測試函數,然后構建并運行、報告測試結果,最后清理測試中生成的臨時文件。
func TestChannel() {/*第一次循環時 i = 1,select 語句中包含兩個 case 分支,此時由于通道中沒有值可以接收,所以x := <-ch 這個 case 分支不滿足,而ch <- i這個分支可以執行,會把1發送到通道中,結束本次 for 循環;第二次 for 循環時,i = 2,由于通道緩沖區已滿,所以ch <- i這個分支不滿足,而x := <-ch這個分支可以執行,從通道接收值1并賦值給變量 x ,所以會在終端打印出 1;后續的 for 循環以此類推會依次打印出3、5、7、9。*/ch := make(chan int, 1)for i := 1; i <= 10; i++ {select {case x := <-ch:fmt.Println(x)case ch <- i:}}
}
9.指針
????????Go語言中的指針不能進行偏移和運算,因此Go語言中的指針操作非常簡單,我們只需要記住兩個符號:&(取地址)和*(根據地址取值)。
func Pointer() {a := 100b := &afmt.Printf("a:%v, b:%v,bp:%p \n", a, b, b)fmt.Printf("&b:%v,\n", &b)modValue(b)fmt.Printf("a:%v, b:%v,bp:%p \n", a, b, b)
}func modValue(i *int) {if i != nil {*i += 101}
}
10.context
func Context() {wg.Add(2)go func() {time.Sleep(time.Second * 2)fmt.Println("job 1 done")wg.Done()}()go func() {time.Sleep(time.Second * 1)fmt.Println("job 2 done")wg.Done()}()wg.Wait()fmt.Println("all job done")
}func Context2() {stop := make(chan bool)go func() {for {select {case <-stop:fmt.Println("got the stop channel")returndefault:fmt.Println("still working")time.Sleep(time.Second * 1)}}}()time.Sleep(time.Second * 5)fmt.Println("stop the goroutine")stop <- truetime.Sleep(time.Second * 5)
}func Context3() {ctx, cancel := context.WithCancel(context.Background())go worker(ctx, "worker1")go worker(ctx, "worker2")go worker(ctx, "worker3")time.Sleep(time.Second * 5)fmt.Println("stop the goroutine")cancel()time.Sleep(time.Second * 5)
}func worker(ctx context.Context, name string) {go func() {for {select {case <-ctx.Done():fmt.Println("got the stop channel")returndefault:fmt.Println(name, " still working")time.Sleep(time.Second * 1)}}}()
}func Context4() {// 創建一個帶有取消功能的上下文ctx, cancel := context.WithCancel(context.Background())defer cancel()// 設置一個截止時間為5秒后ctx, cancel = context.WithDeadline(ctx, time.Now().Add(5*time.Second))defer cancel()// 向上下文中添加一個值ctx = context.WithValue(ctx, "key", "value")// 啟動一個goroutine來監聽上下文的取消信號go func() {select {case <-ctx.Done():fmt.Println("Context done:", ctx.Err())}}()// 啟動一個goroutine來獲取上下文中的值go func() {time.Sleep(2 * time.Second)value := ctx.Value("key")fmt.Println("Context value:", value)}()// 模擬一個耗時操作select {case <-time.After(10 * time.Second):fmt.Println("Operation completed")case <-ctx.Done():fmt.Println("Operation canceled due to context")}// 獲取上下文的截止時間if deadline, ok := ctx.Deadline(); ok {fmt.Println("Context deadline:", deadline)} else {fmt.Println("No deadline set for context")}
}func ContextWaitGroup() {ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()var wg sync.WaitGroupwg.Add(1)go func() {defer wg.Done()for {select {case <-ctx.Done():fmt.Println("Goroutine canceled due to context")returndefault:// 模擬一些工作fmt.Println("Goroutine working...")time.Sleep(1 * time.Second)}}}()wg.Wait()fmt.Println("Main goroutine finished")
}func ContextWaitGroup1() {ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)defer cancel()go func() {for {select {case <-ctx.Done():fmt.Println("goroutine exiting...")returndefault:fmt.Println("goroutine working...")time.Sleep(500 * time.Millisecond)}}}()time.Sleep(3 * time.Second)fmt.Println("main function exiting...")
}
11.閉包
????????(Closure)是一種特殊的函數,它可以捕獲其創建時所在作用域中的變量。閉包通常與匿名函數一起使用,匿名函數可以訪問并操作不在其參數列表中的外部變量。
Go語言中的閉包有幾個特殊的用途和優勢:
狀態封裝,控制變量生命周期,函數工廠,實現回調和延后執行,模塊化和封裝,實現接口,高階函數,迭代器和生成器,避免命名沖突;
使用閉包的注意事項:內存泄漏,并發安全,循環引用.
func Closure() {/*這里這里start := time.Now() 已經運行了返回的函數再跟后面做減法*/defer TimeCost("test closure", 11)()time.Sleep(time.Second * 2)
}func TimeCost(handlerName string, req ...interface{}) func() {fmt.Printf(fmt.Sprintf("TimeCost for %s start now.", handlerName))start := time.Now()return func() {tc := time.Since(start)fmt.Printf(fmt.Sprintf("handle %s for request:%+v time cost is:%+v", handlerName, req, tc))}
}
12.http\UDP
????????????????UDP協議(User Datagram Protocol)中文名稱是用戶數據報協議,是OSI(Open System Interconnection,開放式系統互聯)參考模型中一種無連接的傳輸層協議,不需要建立連接就能直接進行數據發送和接收,屬于不可靠的、沒有時序的通信,但是UDP協議的實時性比較好,通常用于視頻直播相關領域。
func process(conn net.Conn) {defer conn.Close()for {reader := bufio.NewReader(conn)var buf [128]byten, err := reader.Read(buf[:])if err != nil {fmt.Printf("read failed,err:%v", err)break}recv := string(buf[:n])fmt.Printf("接收到的數據:%v", recv)conn.Write([]byte("ok"))}
}func HttpServer() {listen, err := net.Listen("tcp", "127.0.0.1:8801")if err != nil {fmt.Printf("listen failed,err:%v", err)return}for {conn, err := listen.Accept()if err != nil {fmt.Printf("Accept failed ,err:%v", err)continue}go process(conn)}
}func HttpClinet() {conn, err := net.Dial("tcp", "127.0.0.1:8801")if err != nil {fmt.Printf("connect failed,err:%v", err)return}input := bufio.NewReader(os.Stdin)for {s, _ := input.ReadString('\n')s = strings.TrimSpace(s)if strings.ToUpper(s) == "Q" {return}//給服務端發消息_, err := conn.Write([]byte(s))if err != nil {fmt.Printf("send failed,err:%v \n", err)return}//從服務端接收消息var buf [1023]byten, err := conn.Read(buf[:])if err != nil {fmt.Printf("read failed,err:%v \n", err)return}fmt.Printf("收到服務單回復:%v", string(buf[:n]))}
}/*
RPC就是為了解決類似遠程、跨內存空間、的函數/方法調用的。要實現RPC就需要解決以下三個問題。
如何確定要執行的函數?調用方和被調用方都需要維護一個{ function <-> ID }映射表,以確保調用正確的函數
如何表達參數? 參數或返回值需要在傳輸期間序列化并轉換成字節流,反之亦然
如何進行網絡傳輸?只要能夠完成傳輸,調用方和被調用方就不受某個網絡協議的限制
*/