banner
cos

cos

愿热情永存,愿热爱不灭,愿生活无憾
github
tg_channel
bilibili

Go語言初上手(二) 工程實踐 | 青訓營

並發編程#

  • 並發 是多線程程序在一個核的 cpu 上運行
    image.png

  • 並行 是多線程程序在多個核的上運行
    image.png

  • Go 可以充分發揮多核優勢,高效運行
    一個重要概念

協程#

  • 協程的開銷比線程小,可以理解為輕量級的線程,一個 Go 程序中可以創建上萬個協程。

Go 中 開啟協程 非常簡單,在函數前面增加一個 go 關鍵字就可以為一個函數開啟一個協程。

CSP 與 Channel#

CSP(Communicating Sequential Process)

Go 中提倡通過 通信共享內存 而不是通過共享內存而實現通信

那麼如何通信呢,通過 channel

Channel#

語法: make(chan 元素類型, [緩衝大小])

  • 無緩衝通道 make(chan int)
  • 有緩衝通道 make(chan int, 2)
    這個圖就非常的生動形象~
    image.png

image.png

以下是一個例子:

  • 第一個協程 作為生產者發送0~9src
  • 第二個協程 作為消費者計算 src 中每個數的平方發送到 dest
  • 主線程輸出 dest 中每個數
package main

func CalSquare() {
   src := make(chan int)     // 生產者
   dest := make(chan int, 3) // 消費者 帶緩衝解決生產者太快的問題
   go func() {               // 該線程發送0~9至src中
      defer close(src) // defer 表示延遲到函數結束時執行 用於釋放已分配的資源。
      for i := 0; i < 10; i++ {
         // <- 運算符 左側為收集數據的一方 右側為要傳的數據
         src <- i
      }
   }() // 立即執行
   go func() {
      defer close(dest)
      for i := range src {
         dest <- i * i
      }
   }()
   for i := range dest {
      // 其他複雜操作
      println(i)
   }
}
func main() {
   CalSquare()
}

可以看到每次都會是順序輸出,代表著 Go 是 並發安全的

Go 語言也保留了共享內存的做法,使用 sync 進行同步,如下

package main

import (
   "sync"
   "time"
)

var (
   x    int64
   lock sync.Mutex
)

func addWithLock() { // x加到2000 使用鎖則很安全
   for i := 0; i < 2000; i++ {
      lock.Lock() // 加鎖
      x += 3
      x -= 2
      lock.Unlock() // 解鎖
   }
}
func addWithoutLock() { // 不使用鎖
   for i := 0; i < 2000; i++ {
      x += 3
      x -= 2
   }
}
func Add() {
   x = 0
   for i := 0; i < 5; i++ {
      go addWithoutLock()
   }
   time.Sleep(time.Second) // 休眠 1s
   println("WithoutLock x =", x)
   x = 0
   for i := 0; i < 5; i++ {
      go addWithLock()
   }
   time.Sleep(time.Second) // 休眠 1s
   println("WithLock x =", x)
}
func main() {
   Add()
}

ps:試了好多次都沒衝突,樂。把運算稍微改複雜一點就有衝突了

image.png

依賴管理#

任何大型項目開發都繞不開依賴管理,Go 中的依賴主要經歷了 GOPATH -> Go Vendor -> Go Module 的演變 而現在主要採用 Go Module 的方式

  • 不同環境依賴的版本不同,所以如何控制依賴庫的版本?

GOPATH#

  • 項目代碼直接依賴 src 下的代碼
  • 通過 go get 下載最新版本的包到 src 目錄下

這樣的話,就會出現一個問題:無法實現多版本的控制(A、B 依賴於同一個包的不同版本,寄)

Go Vender#

  • 項目目錄下新增 vendor文件,所有依賴包副本形式放在其中
  • 通過 vendor => GOPATH 的方式曲線救國

ps:感覺挺像前端的 package.json…… 依賴問題真是繞不過去

這又產生了新的問題:

  • 無法控制依賴的版本
  • 更新項目時可能出現依賴衝突,從而導致編譯出錯

Go Module#

  • 通過 go.mod 文件管理依賴包版本
  • 通過 go get/go mod 指令工具管理依賴包

達成了終極目標:既能定義版本規則,又能管理項目依賴關係

可以類比一下 Java 中的 Maven

依賴配置 go.mod#

依賴標識語法:模塊路徑 + 版本來進行唯一標識

[Module Path][Version/Pseudo-version]

module example/project/app     依賴管理基本單元

go 1.16     原生庫

require (    單元依賴
    example/lib1 v1.0.2
    example/lib2 v1.0.0 // indirect
    example/lib3 v0.1.0-20190725025543-5a5fe074e612
    example/lib4 v0.0.0-20180306012644-bacd9c7ef1dd // indirect
    example/lib5/v3 v3.0.2
    example/lib6 v3.2.0+incompatible
)

如上,需要注意的是:

  • 主版本 2 + 的模塊會在路徑後增加 /vN 後綴
  • 對於沒有 go.mod 文件且主版本 2 + 的依賴,會 +incompatible
    依賴的版本規則分為語義化版本和基於 commit 的伪版本

語義化版本#

格式為:${MAJOR}.${MINOR}.${PATCH} V1.3.0、V2.3.0、 ……

  • 不同的 MAJOR 版本表示是不兼容的 API
    • 即使是同一個庫,MAJOR 版本不同也會被認為是不同的模塊
  • MINOR 版本通常是新增函數或功能向後兼容
  • PATCH 版本一般是 修復 bug

基於 commit 的版本#

格式為:${vx.0.0-yyyymmddhhmmss-abcdefgh1234}

  • 版本前綴是和語義化版本一樣的
  • 時間戳 (yyyymmddhhmmss),也就是提交 Commit 的時間
  • 校驗碼 (abcdefgh1234), 12 位的哈希前綴
    • 每次提交 commit 後 Go 都會默認生成一個伪版本號

小測試#

image.png

  1. 如果 X 項目依賴了 A、B 兩個項目,且 A、B 分別依賴了 C 項目的 v1.3、v1.4 兩個版本,依賴圖如上,最終編譯時所使用的 C 項目的版本為 []{.gap} ? {.quiz}
    • v1.3
    • v1.4 {.correct}
    • A 用到 c 時用 v1.3 編譯,B 用到 c 時用 v1.4 編譯
      {.options}

    答案為:B 選擇最低的兼容版本
    這個是 Go 進行版本選擇的算法,選擇最低的兼容版本,而 1.4 版本是向下兼容 1.3 的(語義化版本)。為什麼不選 1.3 呢?因為他又不會向上兼容 ovo,倘若還有 1.5 的話則不會選用 1.5,因為 1.4 就是滿足要求的最低兼容版本。

依賴分發#

這些依賴去哪裡下載呢?就是依賴分發

在 github 等代碼托管系統中對應倉庫上下載?

github 是比較常見給的代碼托管系統平台,而Go Modules 系統中定義的依賴,最終可以對應到多版本代碼管理系統中某一項目的特定提交或版本

對於 go.mod 中定義的依賴,可以從對應倉庫中下載指定軟件依賴,從而完成依賴分發。

問題也有:

  • 無法保證構建確定性
    • 軟件作者直接修改軟件版本,導致下次構建使用其他版本的依賴,或者找不到依賴版本
  • 無法保證依賴可用性
    • 軟件作者直接代碼平台刪除軟件,導致依賴不可用
  • 增加第三方代碼托管平台壓力

通過 Proxy 方式來解決以上問題

Go Proxy 是一個服務站點,它會緩存源站中的軟件內容,緩存的軟件版本不會改變,並且在源站軟件刪除之後依然可用

使用 Go Proxy 之後,構建時會直接從 Go Proxy 站點拉取依賴。

Go Modules 通過 GOPROXY 環境變量控制如何使用 Go Proxy

服務站點 URL 列表,direct 表示源站:GOPROXY="https://proxy1.cn, https://proxy2.cn,direct"

  • GOPROXY 是一個 Go Proxy 站點 URL 列表,可以使用 direct 表示源站。整體的依賴尋址路徑,會優先從 proxy1 下載依賴,如果 proxy1 不存在,就下到 proxy2尋找,如果proxy2 也不存在則會回源到源站直接下載依賴,緩存到 proxy 站點中。

工具#

go get example.org/pkg

後綴含義
@update默認
@none刪除依賴
@v1.1.2tag 版本,語義版本
@23dfdd5特定的 commit
master分支的最新 commit

go mod

後綴含義
init初始化,創建 go.mod 文件
download下載模炔到本地緩存
tidy增加需要的依賴,刪除不需要的依賴
go mod tidy 可以在每次提交代碼前執行一下,就可以減少構建整個項目的時間

測試#

測試一般分為回歸測試集成測試單元測試,從前到後覆蓋率逐層變大成本卻逐層降低,所以單元測試的覆蓋率一定程度上決定這代碼的質量。

  • 回歸測試一般是 QA 同學手動通過終端回歸一些固定的主流程場景
  • 集成測試是對系統功能維度做測試驗證
  • 單元測試測試開發階段,開發者對單獨的函數、模塊做功能驗證

單元測試主要包括:輸入測試單元輸出以及校對

單元的概念較廣,包括接口,函數,模塊等,用最後的校對來保證代碼的功能與我們的預期相符

單元測試有以下幾點好處

  • 保證質量
    • 整體覆蓋率足夠時下,既保證了新功能正確性,又未破壞原有代碼的正確性
  • 提升效率
    • 代碼有 bug 的情況下,通過單測,可以在一個較短週期內定位和修復問題

Go 中的單元測試有以下規則:

  • 所有測試文件以 _test.go 結尾
  • func TestXxx(testing.T)
  • 初始化邏輯放到 TestMain函數中(測試前的數據裝載配置、測試後的釋放資源等)

例子:
main.go

package main

func HelloTom() string {
   return "Jerry"
}

main_test.go

package main

import "testing"

func TestHelloTom(t *testing.T) {
   output := HelloTom()
   expectOutput := "Tom"
   if output != expectOutput {
      t.Errorf("Expect %s do not match actual %s", expectOutput, output)
   }
}

image.png

在實際項目中,單測覆蓋率

  • 一般項目的要求是 50%~60% 覆蓋率
  • 對於重要的資金型服務,覆蓋率可能要求達到 80%

單測需要保證穩定性幂等性

  • 穩定是指相互隔離,能在任何時間,任何環境,運行測試
  • 幂等是指每一次測試運行都應該產生與之前一樣的結果

而要實現這一目的就要用到mock機制。

bouk/monkey: Monkey patching in Go

monkey 是一個開源的 mock 測試庫,可以對 method,或者實例的方法進行 mock,反射,指針賦值 Mockey Patch 的作用域在 Runtime,在運行時通過 Go 的 unsafe 包,能夠將內存中函數 A 的地址替換為運行時函數 B 的地址,將待打樁函數的實現跳轉。

Go 語言還提供了基準測試框架

  • 基準測試是指測試一段程序的運行性能及耗費 CPU 的程度。

而我們在實際項目開發中,經常會遇到代碼性能瓶頸問題,為了定位問題經常要對代碼做性能分析,這就用到了基準測試。使用方法類似於單元測試

提到了fastrand,地址: bytedance/gopkg: Universal Utilities for Go

總結及心得#

本節課主要講了 Go 中的並發管理、依賴配置和測試,內容較多,需要好好消化。後面還有個項目實踐環節,等明天在進行一個實踐。

本節課內容來源於第三屆青訓營趙徵老師的課程

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。