Golang單元測試、go協(xié)程和管道示例詳解
1.單元測試
1.1基本介紹
Go語言中自帶有一個輕量級的測試框架testing和自帶的go test命令來實現(xiàn)單元測試和性能測試,testing框架和其他語言中的測試框架類似,可以基于這個框架寫針對相應(yīng)函數(shù)的測試用例,也可以基于該框架寫相應(yīng)的壓力測試用例。
通過單元測試,可以解決如下問題:
- 確保每個函數(shù)是可運行,并且運行結(jié)果是正確的
- 確保寫出來的代碼性能是好的
- 單元測試能及時的發(fā)現(xiàn)程序設(shè)計或?qū)崿F(xiàn)的邏輯錯誤,使問題及早暴露,便于問題的定位解決,而性能測試的重點在于發(fā)現(xiàn)程序設(shè)計上的一些問題,讓程序能夠在高并發(fā)的情況下還能保持穩(wěn)定
1.2單元測試編寫步驟
(1)創(chuàng)建測試文件
測試文件以*test.go結(jié)尾,例如:math_test.go
(2)編寫測試函數(shù)
測試函數(shù)以Test開頭,接受*testing.T參數(shù)
func TestAdd(t *testing.T){
//測試邏輯
}(3)表格驅(qū)動測試
使用結(jié)構(gòu)體切片定義多個測試用例,循環(huán)遍歷執(zhí)行
func TestAdd(t *testing.T){
//使用結(jié)構(gòu)體切片定義多個測試用例
tests:=[]struct{
a,b,want int
}{
{1,2,3},
{3,4,7},
{5,6,11},
}
//循環(huán)遍歷執(zhí)行
for _,v:=range tests{
sum:=Add(v.a,v.b)
if sum != v.want{
t.Errorf("Add(%d,%d)=%d;want %d\n",v.a,v.b,sum,v.want)
}
}
}
(4)子測試
使用t.Run()分組測試用例,提升可讀性和選擇性運行
func TestAdd(t *testing.T){
//使用結(jié)構(gòu)體切片定義多個測試用例
tests:=[]struct{
name string
a,b,want int
}{
//分組
{"正數(shù)",1,2,3},
{"負數(shù)",-3,-4,-7},
{"零值",0,0,0},
}
//循環(huán)遍歷執(zhí)行
for _,v:=range tests{
sum:=Add(v.a,v.b)
if sum != v.want{
t.Errorf("Add(%d,%d)=%d;want %d\n",v.a,v.b,sum,v.want)
}
}
}(5)錯誤測試
驗證函數(shù)是否返回預(yù)期錯誤
func TestDivide(t *testing.T){
_,err:=Divide(6,0)
if err ==nil{
t.Fatal("預(yù)期錯誤,并未返回")
}
if err.Error()!="除零錯誤"{
t.Error("錯誤消息不符:%s",err)
}
}(6)測試覆蓋率
生成并查看覆蓋率報告
go test -coverprofile=coverage.out go tool cover -html=coverage.out
(7)初始化和清理
使用TestMain進行全局設(shè)置
func TestMain(m *testing.M){
setup()
code:=m.Run()
teardown()
os.Exit(code)
}(8)使用t.Cleanup
注冊清理函數(shù)
func TestDB(t *testing.T){
db:=setupDB()
t.Cleanup(func(){
teardownDB(db)
})
//測試邏輯
}(9)模擬依賴
通過接口實現(xiàn)Mock()
type Storage interface{
Get(id int) string
}
type MockStorage struct{}
func (m*MockStorage)Get(id int)string{
return "mock"
}
func TestService(t*testing.T){
s:=&Service{storage:&MockStorage{}}
//測試邏輯
}(10)跳過測試
func TestNetwork(t *testing.T){
if testing.Short(){
t.Skip("短模式下跳過")
}
//網(wǎng)絡(luò)相關(guān)測試
}
(11)并行測試
使用t.Parallel()加速測試
func TestParallel(t *testing.T){
t.Parallel()
//并發(fā)安全測試邏輯
}1.3總結(jié)
- 測試用例文件名必須以 test.go結(jié)尾。比如 cal tert.go,cal 不是固定的。
- 測試用例函數(shù)必須以Test開頭,一般來說就是Test+被測試的函數(shù)名,比如TestAddUpper。TestAddUpper(t *tesing.T)的形參類型必須是 *testing.T【看一下手冊】
- 一個測試用例文件中,可以有多個測試用例函數(shù),比如 TestAddUpper
- Testsub運行測試用例指令 (1)cmd>go test[如果運行正確,無日志,錯誤時,會輸出日志] (2)cmd>gotest-v[運行正確或是錯誤,都輸出日志]
- 當(dāng)出現(xiàn)錯誤時,可以使用t.Fatalf來格式化輸出錯誤信息,并退出程序
- t.Logf方法可以輸出相應(yīng)的日志
- 測試用例函數(shù),并沒有放在main函數(shù)中,也執(zhí)行了,這就是測試用例的方便之處
- PASS表示測試用例運行成功,F(xiàn)AIL表示測試用例運行失敗
1.4單元測試綜合案例
被測代碼文件:mathutil.go
package main
// 計算兩個數(shù)的和
func Add(a, b int) int {
return a + b
}
// 計算階乘
func Factorial(n int) int {
if n < 0 {
return -1
}
if n == 0 {
return 1
}
return n * Factorial(n-1)
}
// 判斷是否為素數(shù)
func IsPrime(n int) bool {
if n < 2 {
return false
}
for i := 2; i*i <= n; i++ {
if n%i == 0 {
return false
}
}
return true
}測試文件:mathutil_test.go
package main
import (
"testing"
)
func TestAdd(t *testing.T) {
tests := []struct {
a, b, expected int
}{
{1, 2, 3},
{-1, 1, 0},
{0, 0, 0},
{100, 200, 300},
}
for _, tt := range tests {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; expected %d", tt.a, tt.b, result, tt.expected)
}
}
}
func TestFactorial(t *testing.T) {
tests := []struct {
n, expected int
}{
{0, 1},
{1, 1},
{5, 120},
{10, 3628800},
{-1, -1},
}
for _, tt := range tests {
result := Factorial(tt.n)
if result != tt.expected {
t.Errorf("Factorial(%d) = %d; expected %d", tt.n, result, tt.expected)
}
}
}
func TestIsPrime(t *testing.T) {
tests := []struct {
n int
expected bool
}{
{2, true},
{3, true},
{4, false},
{17, true},
{1, false},
{0, false},
{-1, false},
}
for _, tt := range tests {
result := IsPrime(tt.n)
if result != tt.expected {
t.Errorf("IsPrime(%d) = %v; expected %v", tt.n, result, tt.expected)
}
}
}運行:在bash下運行
go test -v
2.goroutine
2.1進程和線程說明
- 進程就是程序程序在操作系統(tǒng)中的一次執(zhí)行過程,是系統(tǒng)進行資源分配和調(diào)度的基本單位
- 線程是進程的一個執(zhí)行實例,是程序執(zhí)行的最小單元,它是比進程更小的能獨立運行的基本單位。
- 一個進程可以創(chuàng)建核銷毀多個線程,同一個進程中的多個線程可以并發(fā)執(zhí)行。
- 一個程序至少有一個進程,一個進程至少有一個線程
2.2進程線程關(guān)系示意圖

2.3并發(fā)和并行
- 多線程程序在單核上運行,就是并發(fā)
- 多線程程序在多核上運行,就是并行
- 并發(fā):因為是在一個cpu上,比如有10個線程,每個線程執(zhí)行10毫秒(進行輪詢操作),從人的角度看,好像這10個線程都在運行,但是從微觀上看,在某一個時間點看,其實只有一個線程在執(zhí)行,這就是并發(fā)。
- 并行:因為是在多個cpu上(比如有10個cpu),比如有10個線程,每個線程執(zhí)行10毫秒(各自在不同cpu上執(zhí)行),從人的角度看,這10個線程都在運行,但是從微觀上看,在某一個時間點看,也同時有10個線程在執(zhí)行,這就是并行


2.4Go協(xié)程和Go主線程
Go主線程(有程序員直接稱為線程/也可以理解成進程):一個Go線程上,可以起多個協(xié)程,你可以這樣理解,協(xié)程是輕量級的線程[編譯器做優(yōu)化]。
Go協(xié)程的特點
- 有獨立的棧空間
- 共享程序
- 堆空間調(diào)度由用戶控制
- 協(xié)程是輕量級的線程
2.5 goroutine使用案例
使用 go 關(guān)鍵字啟動協(xié)程
package main
import(
"fmt"
"time"
)
func test(){
for i:=0;i<10;i++{
fmt.Printf("test()~%v\n",i)
time.Sleep(1000*time.Millisecond)//休眠一秒
}
}
func main(){
//啟動協(xié)程
go test()
for i:=0;i<5;i++{
fmt.Printf("main()~%v\n",i)
time.Sleep(1000*time.Millisecond)//休眠一秒
}
}說明:
- 主線程是一個物理線程,直接作用在cpu上的。是重量級的,非常耗費cpu資源
- 協(xié)程從主線程開啟的,是輕量級的線程,是邏輯態(tài)。對資源消耗相對小。
- Golang的協(xié)程機制是重要的特點,可以輕松的開啟上萬個協(xié)程。其它編程語言的并發(fā)機制是一般基于線程的,開啟過多的線程,資源耗費大,這里就突顯Golang在并發(fā)上的優(yōu)勢了
- 當(dāng)主線程終止時,其他的協(xié)程也將終止
2.6MPG基本介紹
- M 代表著一個內(nèi)核線程,也可以稱為一個工作線程。goroutine就是跑在M之上的
- P 代表著(Processor)處理器 它的主要用途就是用來執(zhí)行g(shù)oroutine的,一個P代表執(zhí)行一個Go代碼片段的基礎(chǔ)(可以理解為上下文環(huán)境),所以它也維護了一個可運行的goroutine隊列,和自由的goroutine隊列,里面存儲了所有需要它來執(zhí)行的goroutine。
- G 代表著goroutine 實際的數(shù)據(jù)結(jié)構(gòu)(就是你封裝的那個方法),并維護者goroutine 需要的棧、程序計數(shù)器以及它所在的M等信息。
- Seched 代表著一個調(diào)度器 它維護有存儲空閑的M隊列和空閑的P隊列,可運行的G隊列,自由的G隊列以及調(diào)度器的一些狀態(tài)信息等。

2.7設(shè)置Go運行cpu的個數(shù)
所用到的方法:

案例:
package main
import (
"fmt"
"runtime"
)
func main(){
//獲取當(dāng)前(邏輯)cpu的數(shù)量
num:=runtime.NumCPU()
//設(shè)置num-1的cpu運行g(shù)o程序
runtime.GOMAXPROCS(num)
fmt.Println(num)
}3.channel(管道)
3.1全局變量+互斥鎖解決資源競爭

案例: 啟動20個協(xié)程求1~20的階乘
package main
import (
"fmt"
"sync"
"time"
)
var (
myMap =make(map[int]int,10)
//聲明一個全局的互斥鎖
lock sync.Mutex
)
func test(n int){
res:=1
for i:=1;i<=n;i++{
res*=i
}
//這里我們將res放入myMap中
//加鎖
lock.Lock()
myMap[n]=res
//解鎖
lock.Unlock()
}
func main(){
//開啟多個協(xié)程完成求階乘
for i:=1;i<=20;i++{
go test(i)
}
//休眠5秒
time.Sleep(5*time.Second)
//加鎖
lock.Lock()
for i,v:=range myMap{
fmt.Printf("map[%d]=%d\n",i,v)
}
//解鎖
lock.Unlock()
}3.2channel的介紹
- channel本質(zhì)就是一個數(shù)據(jù)結(jié)構(gòu)-隊列
- 數(shù)據(jù)是先進先出[FIFO]
- 線程安全,多goroutine訪問時,不需要加鎖,就是說channel本身就是線程安全的
- channel是類型安全的,只能發(fā)送和接收指定類型的數(shù)據(jù),例如:一個string的channel只能存放string類型數(shù)據(jù)
- 無緩沖channel是同步的,發(fā)送和接收操作會阻塞直到另一端準(zhǔn)備好
- 緩沖channel允許在沒有接收者的情況下發(fā)送有限數(shù)量的數(shù)據(jù)
- channel可以通過 close() 函數(shù)關(guān)閉,關(guān)閉后不能再發(fā)送數(shù)據(jù)
(1)定義channel
var 名字 chan 類型
(2)創(chuàng)建channel
ch := make(chan int) // 創(chuàng)建一個無緩沖的int類型channel ch := make(chan string, 10) // 創(chuàng)建一個緩沖大小為10的string類型channel
(3)發(fā)送和接受數(shù)據(jù)
ch <- 42 // 發(fā)送數(shù)據(jù)到channel value := <-ch // 從channel接收數(shù)據(jù) <-ch //也可以不接受數(shù)據(jù),將數(shù)據(jù)推出
3.3channel的使用案例
(1)基本數(shù)據(jù)類型Chan
package main
import (
"fmt"
)
func main(){
//定義一個接受int類型的channel
var intChan chan int
//使用channel前需要make
intChan=make(chan int,4)
//進channel
intChan<-1
intChan<-2
intChan<-3
intChan<-4
// intChan<-5 注意,當(dāng)我們給channel寫入數(shù)據(jù)時,不能超過其容量
//看看intChan是什么
fmt.Printf("intChan 的值是%v,地址為%p,長度為%v,容量為%v\n",intChan,&intChan,len(intChan),cap(intChan))
//從channel中取數(shù)據(jù)
var num int
num=<-intChan
fmt.Println("num=",num)
fmt.Printf("intChan 的長度為%v,容量為%v\n",len(intChan),cap(intChan))
//注意:在沒有使用協(xié)程的情況下,如果我們的管道數(shù)據(jù)已經(jīng)全部取出,繼續(xù)取數(shù)據(jù)就會報deadlock
num2:=<-intChan
<-intChan //數(shù)據(jù)出channel 也可以不接受,相當(dāng)于丟棄
num3:=<-intChan
fmt.Printf("num2=%d , num3=%d\n",num2,num3)
//num2=2 , num3=4
//數(shù)據(jù)出channel時先進先出的
}(2)struct管道和map管道:
package main
import (
"fmt"
)
type Cat struct{
Name string
Age int
}
func main(){
//structChan
cat1:=Cat{"jav",12}
cat2:=Cat{"mimi",3}
var catChan chan Cat
catChan=make(chan Cat,3)
catChan<-cat1
catChan<-cat2
fmt.Println(<-catChan)
cat3:=<-catChan
fmt.Println(cat3)
//mapChan
var mapChan chan map[string]string
mapChan=make(chan map[string]string,5)
m1:=make(map[string]string,10)
m1["功法1"]="九幽玄天"
m1["功法2"]="天罡決"
mapChan<-m1
m2:=make(map[string]string)
m2["神通1"]="古神決"
m2["神通2"]="呼風(fēng)喚雨"
mapChan<-m2
//輸出
fmt.Println(<-mapChan)
fmt.Println(<-mapChan)
}(3)interface{}類型的chan
package main
import(
"fmt"
)
type Person struct{
Name string
Age int
}
type Dog struct{
Name string
Age int
}
func main(){
//能接受任意類型的管道
var allChan chan interface{}
allChan=make(chan interface{},10)//不指定空間10則無法使用
//定義變量
num1:=2
str1:="channel is interesting"
person1:=Person{"李星云",21}
dog1:=Dog{"小紅",4}
//chan接受數(shù)據(jù)
allChan<-num1
allChan<-str1
allChan<-person1
allChan<-dog1
//取數(shù)據(jù)
num2:=<-allChan
str2:=<-allChan
person2:=<-allChan
dog2:=<-allChan
//打印
fmt.Printf("num2的類型為%T,值為%v\n",num2,num2)
fmt.Printf("str2的類型為%T,值為%v\n",str2,str2)
fmt.Printf("person2的類型為%T,值為%v\n",person2,person2)
fmt.Printf("dog2的類型為%T,值為%v\n",dog2,dog2)
//驗證
// fmt.Println(person2.Name)
//會報錯,此時是interface{}類型,不能由屬性
//需類型斷言解決該問題
person3,ok:=person2.(Person)
if ok==false{
fmt.Println("類型斷言失敗")
}
fmt.Printf("person3的類型為%T,值為%v\n",person3,person3)
fmt.Println(person3.Name)
}3.4chan的關(guān)閉和遍歷
3.4.1chan的關(guān)閉
用到的函數(shù):

使用內(nèi)置函數(shù)close可以關(guān)閉channel,當(dāng)channel關(guān)閉后,就不能再向channel寫數(shù)據(jù)了,但是仍然可以從該channel讀取數(shù)據(jù)
案例:
package main
import (
"fmt"
)
func main(){
var intChan chan int
intChan=make(chan int,5)
intChan<-1
intChan<-2
intChan<-3
//關(guān)閉管道
close(intChan)
//這時不能再寫數(shù)據(jù)到intChan
//intChan<-4
//panic: send on closed channel
//但是可以讀數(shù)據(jù)
fmt.Println(<-intChan)
}3.4.3chan的遍歷
channel支持for--range的方式進行遍歷,
請注意兩個細節(jié):
- 在遍歷時,如果channel沒有關(guān)閉,則回出現(xiàn)deadlock的錯誤
- 在遍歷時,如果channel已經(jīng)關(guān)閉,則會正常遍歷數(shù)據(jù),遍歷完后,就會退出遍歷。
案例1:沒有close
package main
import (
"fmt"
)
func main(){
var intChan chan int =make(chan int,100)
//遍歷chan,接受數(shù)據(jù)
for i:=0;i<100;i++{
intChan<-i*2
}
//在遍歷讀數(shù)據(jù)時,如果channel沒有關(guān)閉,
//則會出現(xiàn)fatal error: all goroutines are asleep - deadlock!
for v:=range intChan{
fmt.Println(v)
}
fmt.Println("遍歷完成~")//不會輸出
}案例2:close
package main
import (
"fmt"
)
func main(){
var intChan chan int =make(chan int,100)
//遍歷chan,接受數(shù)據(jù)
for i:=0;i<100;i++{
intChan<-i*2
}
//在遍歷讀數(shù)據(jù)時,如果channel已經(jīng)關(guān)閉,
//則會正常遍歷數(shù)據(jù),遍歷完數(shù)據(jù)后就會退出遍歷
close(intChan)
for v:=range intChan{
fmt.Println(v)
}
fmt.Println("遍歷完成~")//正常輸出
}3.5goroutine和channel結(jié)合
package main
import (
"fmt"
_ "time"
)
//writer Data
func writeData(intChan chan int){
for i:=1;i<=200;i++{
//放入數(shù)據(jù)
intChan<-i
fmt.Println("writeData.............寫入數(shù)據(jù):",i)
}
//關(guān)閉
close(intChan)
}
//read Data
func readData(intChan chan int,exitChan chan bool){
for{
v,ok:=<-intChan
if !ok{
break
}
fmt.Printf("readData 讀到數(shù)據(jù)=%v\n",v)
}
//讀完數(shù)據(jù)
exitChan<-true
close(exitChan)
}
func main(){
//創(chuàng)建兩個管道
intChan := make(chan int,200)
exitChan := make(chan bool,1)
go writeData(intChan)
go readData(intChan,exitChan)
// time.Sleep(5*time.Second)
for {
_,ok:=<-exitChan
if !ok{
break
}
}
}3.6管道阻塞的機制
(1)無緩沖管道 :
- 發(fā)送操作會阻塞,直到有另一個goroutine執(zhí)行接收操作
- 接收操作會阻塞,直到有另一個goroutine執(zhí)行發(fā)送操作
- 這種機制實現(xiàn)了goroutine之間的同步
(2)有緩沖管道 :
- 當(dāng)緩沖區(qū)未滿時,發(fā)送操作不會阻塞
- 當(dāng)緩沖區(qū)為空時,接收操作會阻塞
- 當(dāng)緩沖區(qū)滿時,發(fā)送操作會阻塞
- 這種機制允許一定程度的異步操作
案例:
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int) // 無緩沖管道
go func() {
time.Sleep(2 * time.Second)
fmt.Println("準(zhǔn)備發(fā)送數(shù)據(jù)")
ch <- 42
fmt.Println("數(shù)據(jù)已發(fā)送")
}()
fmt.Println("等待接收數(shù)據(jù)")
value := <-ch
fmt.Println("接收到數(shù)據(jù):", value)
}解除阻塞的方式 :
- 對于發(fā)送操作:有g(shù)oroutine執(zhí)行接收操作
- 對于接收操作:有g(shù)oroutine執(zhí)行發(fā)送操作
- 使用context或timeout機制取消阻塞
- 關(guān)閉管道(關(guān)閉后接收操作不會阻塞,會立即返回零值)
解除阻塞案例:
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
// 啟動一個goroutine,延遲發(fā)送數(shù)據(jù)
go func() {
time.Sleep(2 * time.Second)
ch <- 42
fmt.Println("數(shù)據(jù)已發(fā)送")
}()
// 主goroutine嘗試接收數(shù)據(jù),會阻塞直到有數(shù)據(jù)
fmt.Println("等待接收數(shù)據(jù)...")
value := <-ch
fmt.Println("接收到數(shù)據(jù):", value)
// 使用select實現(xiàn)超時解除阻塞
select {
case v := <-ch:
fmt.Println("接收到數(shù)據(jù):", v)
case <-time.After(1 * time.Second):
fmt.Println("超時,解除阻塞")
}
// 關(guān)閉管道解除阻塞
close(ch)
v, ok := <-ch
fmt.Println("從關(guān)閉的管道接收:", v, "是否成功:", ok)
}3.7channel使用細節(jié)
- 避免向已關(guān)閉的channel發(fā)送數(shù)據(jù),這會導(dǎo)致panic
- 重復(fù)關(guān)閉channel會導(dǎo)致panic
- 從已關(guān)閉的channel接收數(shù)據(jù)會立即返回零值
- 使用nil channel會永久阻塞
- 注意channel的緩沖區(qū)大小,過大會占用過多內(nèi)存
- 使用 select 語句可以同時處理多個channel操作
- 對于只讀或只寫channel,可以使用類型約束( <-chan 和 chan<- )
- 使用 range 可以遍歷channel直到它被關(guān)閉
結(jié)語
到此這篇關(guān)于Golang單元測試、go協(xié)程和管道的文章就介紹到這了,更多相關(guān)Go單元測試、go協(xié)程和管道內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
go語言在請求http時加入自定義http header的方法
這篇文章主要介紹了go語言在請求http時加入自定義http header的方法,實例分析了Go語言http請求的原理與操作技巧,需要的朋友可以參考下2015-03-03
Go語言字典(map)用法實例分析【創(chuàng)建,填充,遍歷,查找,修改,刪除】
這篇文章主要介紹了Go語言字典(map)用法,結(jié)合實例形式較為詳細的分析了Go語言字典的創(chuàng)建、填充、遍歷、查找、修改、刪除等操作相關(guān)實現(xiàn)技巧,需要的朋友可以參考下2017-02-02
GoLand 2020.3 正式發(fā)布有不少新功能(支持泛型)
這是 2020 年第 3 個版本,也是最后一個版本,你還將發(fā)現(xiàn)許多新的代碼編輯功能,具體內(nèi)容詳情跟隨小編看看有哪些新特性2020-12-12

