go-zero微服務實戰系列(十一、大結局)

萬俊峰Kevin發表於2022-07-11

本篇是整個系列的最後一篇了,本來打算在系列的最後一兩篇寫一下關於k8s部署相關的內容,在構思的過程中覺得自己對k8s知識的掌握還很不足,在自己沒有理解掌握的前提下我覺得也很難寫出自己滿意的文章,大家看了可能也會覺得內容沒有乾貨。我最近也在學習k8s的一些最佳實踐以及閱讀k8s的原始碼,等待時機成熟的時候可能會考慮單獨寫一個k8s實戰系列文章。

內容回顧

下面列出了整個系列的每篇文章,這個系列文章的主要特點是貼近真實的開發場景,並針對高併發請求以及常見問題進行優化,文章的內容也是循序漸進的,先是介紹了專案的背景,接著進行服務的拆分,拆分完服務進行API的定義和表結構的設計,這和我們實際在公司中的開發流程是類似的,緊接著就是做一些資料庫的CRUD基本操作,後面用三篇文章來講解了快取,因為快取是高併發的基礎,沒有快取高併發系統就無從談起,快取主要是應對高併發的讀,接下來又用兩篇文章來對高併發的寫進行優化,最後通過分散式事務保證為服務間的資料一致性。如果大家能夠對每一篇文章都能理解透徹,我覺得對於工作中的絕大多數場景都能輕鬆應對。

對於文章配套的示例程式碼並沒有寫的很完善,有幾點原因,一是商城的功能點非常多,很難把所有的邏輯都覆蓋到;二是多數都是重複的業務邏輯,只要大家掌握了核心的示例程式碼,其他的業務邏輯可以自己把程式碼down下來進行補充完善,這樣我覺得才會進步。如果有不理解的地方大家可以在社群群中問我,每個社群群都可以找到我。

go-zero微服務實戰系列(一、開篇)

go-zero微服務實戰系列(二、服務拆分)

go-zero微服務實戰系列(三、API定義和表結構設計)

go-zero微服務實戰系列(四、CRUD熱身)

go-zero微服務實戰系列(五、快取程式碼怎麼寫)

go-zero微服務實戰系列(六、快取一致性保證)

go-zero微服務實戰系列(七、請求量這麼高該如何優化)

go-zero微服務實戰系列(八、如何處理每秒上萬次的下單請求)

go-zero微服務實戰系列(九、極致優化秒殺效能)

go-zero微服務實戰系列(十、分散式事務如何實現)

單元測試

軟體測試由單元測試開始(unit test)。更復雜的測試都是在單元測試之上進行的。如下所示測試的層級模型:

單元測試(unit test)是最小、最簡單的軟體測試形式、這些測試用來評估某一個獨立的軟體單元,比如一個類,或者一個函式的正確性。這些測試不考慮包含該軟體單元的整體系統的正確定。單元測試同時也是一種規範,用來保證某個函式或者模組完全符合系統對其的行為要求。單元測試經常被用來引入測試驅動開發的概念。

go test工具

go語言的測試依賴go test工具,它是一個按照一定約定和組織的測試程式碼的驅動程式。在包目錄內,所有以_test.go為字尾的原始碼檔案都是 go test 測試的一部分,不會被go build編譯到最終的可執行檔案。

*_test.go檔案中有三種型別的函式,單元測試函式、基準測試函式和示例函式:

型別 格式 作用
測試單數 函式名字首為Test 測試程式的一些邏輯行為是否正確
基準函式 函式名字首為Benchmark 測試函式的效能
示例函式 函式名字首為Example 提供示例

go test會遍歷所有*_test.go檔案中符合上述命名規則的函式,然後生成一個臨時的main包用於呼叫相應的測試函式。

單測格式

每個測試函式必須匯入testing包,測試函式的基本格式如下:

func TestName(t *testing.T) {
	// ......
}

測試函式的名字必須以Test開頭,可選的字尾名必須以大寫字母開頭,示例如下:

func TestDo(t *testing.T) { //...... }
func TestWrite(t *testing.T) { // ...... }

testing.T 用於報告測試失敗和附加的日誌資訊,擁有的主要方法如下:

Name() string
Fail()
Failed() bool
FailNow()
logDepth(s string, depth int)
Log(args ...any)
Logf(format string, args ...any)
Error(args ...any)
Errorf(format string, args ...any)
Fatal(args ...any)
Fatalf(format string, args ...any)
Skip(args ...any)
Skipf(format string, args ...any)
SkipNow()
Skipped() bool
Helper()
Cleanup(f func())
Setenv(key string, value string)

簡單示例

在這個路徑下lebron/apps/order/rpc/internal/logic/createorderlogic.go:44 有一個生成訂單id的函式,函式如下:

func genOrderID(t time.Time) string {
	s := t.Format("20060102150405")
	m := t.UnixNano()/1e6 - t.UnixNano()/1e9*1e3
	ms := sup(m, 3)
	p := os.Getpid() % 1000
	ps := sup(int64(p), 3)
	i := atomic.AddInt64(&num, 1)
	r := i % 10000
	rs := sup(r, 4)
	n := fmt.Sprintf("%s%s%s%s", s, ms, ps, rs)
	return n
}

我們建立createorderlogic_test.go檔案併為該方法編寫對應的單元測試函式,生成的訂單id長度為24,單元測試函式如下:

func TestGenOrderID(t *testing.T) {
	oid := genOrderID(time.Now())
	if len(oid) != 24 {
		t.Errorf("oid len expected 24, got: %d", len(oid))
	}
}

在當前路徑下執行 go test 命令,可以看到輸出結果如下:

PASS
ok  	github.com/zhoushuguang/lebron/apps/order/rpc/internal/logic	1.395s

還可以加上 -v 輸出更完整的結果,go test -v 輸出結果如下:

=== RUN   TestGenOrderID
--- PASS: TestGenOrderID (0.00s)
PASS
ok  	github.com/zhoushuguang/lebron/apps/order/rpc/internal/logic	1.305s

go test -run

在執行 go test 命令的時候可以新增 -run 引數,它對應一個正規表示式,又有函式名匹配上的測試函式才會被 go test 命令執行,例如我們可以使用 go test -run=TestGenOrderID 來值執行 TestGenOrderID 這個單測。

表格驅動測試

表格驅動測試不是工具,它只是編寫更清晰測試的一種方式和視角。編寫好的測試並不是一件容易的事情,但在很多情況下,表格驅動測試可以涵蓋很多方面,表格裡的每一個條目都是一個完整的測試用例,它包含輸入和預期的結果,有時還包含測試名稱等附加資訊,以使測試輸出易於閱讀。使用表格測試能夠很方便的維護多個測試用例,避免在編寫單元測試時頻繁的複製貼上。

lebron/apps/product/rpc/internal/logic/checkandupdatestocklogic.go:53 我們可以編寫如下表格驅動測試:

func TestStockKey(t *testing.T) {
	tests := []struct {
		name   string
		input  int64
		output string
	}{
		{"test one", 1, "stock:1"},
		{"test two", 2, "stock:2"},
		{"test three", 3, "stock:3"},
	}

	for _, ts := range tests {
		t.Run(ts.name, func(t *testing.T) {
			ret := stockKey(ts.input)
			if ret != ts.output {
				t.Errorf("input: %d expectd: %s got: %s", ts.input, ts.output, ret)
			}
		})
	}
}

執行命令 go test -run=TestStockKey -v 輸出如下:

=== RUN   TestStockKey
=== RUN   TestStockKey/test_one
=== RUN   TestStockKey/test_two
=== RUN   TestStockKey/test_three
--- PASS: TestStockKey (0.00s)
    --- PASS: TestStockKey/test_one (0.00s)
    --- PASS: TestStockKey/test_two (0.00s)
    --- PASS: TestStockKey/test_three (0.00s)
PASS
ok  	github.com/zhoushuguang/lebron/apps/product/rpc/internal/logic	1.353s

並行測試

表格驅動測試中通常會定義比較多的測試用例,而go語言又天生支援併發,所以很容易發揮自身優勢將表格驅動測試並行化,可以通過t.Parallel() 來實現:

func TestStockKeyParallel(t *testing.T) {
  t.Parallel()
	tests := []struct {
		name   string
		input  int64
		output string
	}{
		{"test one", 1, "stock:1"},
		{"test two", 2, "stock:2"},
		{"test three", 3, "stock:3"},
	}

	for _, ts := range tests {
		ts := ts
		t.Run(ts.name, func(t *testing.T) {
			t.Parallel()
			ret := stockKey(ts.input)
			if ret != ts.output {
				t.Errorf("input: %d expectd: %s got: %s", ts.input, ts.output, ret)
			}
		})
	}
}

測試覆蓋率

測試覆蓋率是指程式碼被測試套件覆蓋的百分比。通常我們使用的都是語句的覆蓋率,也就是在測試中至少被執行一次的程式碼佔總的程式碼的比例。go提供內建的功能來檢查程式碼覆蓋率,即使用 go test -cover 來檢視測試覆蓋率:

PASS
coverage: 0.6% of statements
ok  	github.com/zhoushuguang/lebron/apps/product/rpc/internal/logic	1.381s

可以看到我們的覆蓋率只有 0.6% ,哈哈,這是非常不合格滴,大大的不合格。go還提供了一個 -coverprofile 引數,用來將覆蓋率相關的記錄輸出到檔案 go test -cover -coverprofile=cover.out

PASS
coverage: 0.6% of statements
ok  	github.com/zhoushuguang/lebron/apps/product/rpc/internal/logic	1.459s

然後執行 go tool cover -html=cover.out,使用cover工具來處理生成的記錄資訊,該命令會開啟本地的瀏覽器視窗生成測試報告

解決依賴

對於單測中的依賴,我們一般採用mock的方式進行處理,gomock是Go官方提供的測試框架,它在內建的testing包或其他環境中都能夠很方便的使用。我們使用它對程式碼中的那些介面型別進行mock,方便編寫單元測試。對於gomock的使用請參考gomock文件

mock依賴interface,對於非interface場景下的依賴我們可以採用打樁的方式進行mock資料,monkey是一個Go單元測試中十分常用的打樁工具,它在執行時通過組合語言重寫可執行檔案,將目標函式或方法的實現跳轉到樁實現,其原理類似於熱補丁。monkey庫很強大,但是使用時需注意以下事項:

  • monkey不支援行內函數,在測試的時候需要通過命令列引數-gcflags=-l關閉Go語言的內聯優化。
  • monkey不是執行緒安全的,所以不要把它用到併發的單元測試中。

其他

畫圖工具

社群中經常有人問畫圖用的是什麼工具,本系列文章中的插圖工具主要是如下兩個

https://www.onemodel.app/

https://whimsical.com/

程式碼規範

程式碼不光是要實現功能,很重要的一點是程式碼是寫給別人看的,所以我們對程式碼的質量要有一定的要求,要遵循規範,可以參考go官方的程式碼review建議

https://github.com/golang/go/wiki/CodeReviewComments

談談感受

時間過得賊快,不知不覺間這個系列已經寫到十一篇了。按照每週更新兩篇的速度也寫了一個多月了。寫文章是個體力活且非常的耗時,又生怕有寫的不對的地方,對大家產生誤導,所以還需要反覆的檢查和查閱相關資料。平均一篇文章要寫一天左右,平時工作日比較忙,基本都是週六日來寫,因此最近一個月週六日基本沒有休息過。

但我覺得收穫也非常大,在寫文章的過程中,對於自己掌握的知識點,是一個複習的過程,可以讓自己加深對知識點的理解,對於自己沒有掌握的知識點就又是一個學習新知識的過程,讓自己掌握了新的知識,所以我和讀者也是一起在學習進步呢。大家都知道,對於自己理解的知識,想要說出來或者寫出來讓別人也理解也是不容易的,因此寫文章對自己的軟實力也是有很大的提升。

所以,我還是會繼續堅持寫文章,堅持輸出,和大家一起學習成長。同時,我也歡迎大家來 "微服務實踐" 公眾號來投稿。可能有些人覺得自己的水平不行,擔心寫的內容不高階,沒有逼格,我覺得大可不必,只要能把知識點講明白就非常棒了,可以是基礎知識,也可以是最佳實踐等等。kevin會對投稿的每一篇文章都認真稽核,寫的不對的地方他都會指出來,所有還有和kevin一對一交流學習的機會,小夥伴們抓緊行動起來呀。

結束語

非常感謝大家這一個多月以來的支援。看到每篇文章有那麼多的點贊,我十分的開心,也更加的有動力,所以,也在計劃寫下個系列的文章,目前有兩個待選的主題,分別是《go-zero原始碼系列》和《gRPC實戰原始碼系列》,歡迎小夥伴們在評論區留下你的評論,說出你更期待哪個系列,如果本篇文章點贊數超過66的話,我們就繼續開整。

程式碼倉庫: https://github.com/zhoushuguang/lebron

專案地址

https://github.com/zeromicro/go-zero

歡迎使用 go-zerostar 支援我們!

微信交流群

關注『微服務實踐』公眾號並點選 交流群 獲取社群群二維碼。

相關文章