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

本篇是整個系列的最後一篇了,本來打算在系列的最後一兩篇寫一下關於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 支持我們!

微信交流羣

關注『微服務實踐』公衆號並點擊 交流羣 獲取社區羣二維碼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章