Go 語言并發編程 及 進階與依賴管理_軟工菜雞的博客-CSDN博客
03 測試
回歸測試一般是QA(質量保證)同學手動通過終端回歸一些固定的主流程場景
集成測試是對系統功能維度做測試驗證,通過服務暴露的某個接口,進行自動化測試
而單元測試開發階段,開發者對單獨的函數、模塊做功能驗證
層級從上至下,測試成本逐漸減低,而測試覆蓋率確逐步上升,所以單元測試的覆蓋率一定程度上決定著代碼的質量。
3.1.0 單元測試
單元測試主要包括:輸入,測試單元,輸出,以及校對
單元的概念比較廣:包括接口,函數,模塊等;用最后的校對來保證代碼的功能與我們的預期相符;
單側一方面可以保證質量,在整體覆蓋率足夠的情況下,一定程度上既保證了新功能本身的正確性,又未破壞原有代碼的正確性。 另一方面可以提升效率,在代碼有bug的情況下,通過編寫單測,可以在一個較短周期內定位和修復問題。
3.1.1 單元測試-規則
基本規范,以 _test.go結尾這樣從文件上就很好了區分源碼和測試代碼
以Test開頭,且連接的第一個字母大寫
TestMain的m.run()會跑這個package下的所有單測;
3.1.2 單元測試-例子
HelloTom() 由于代碼邏輯的錯誤沒有輸出Tom;
接下來進行測試:TestHelloTom()
3.1.3 單元測試-運行
輸出結果錯誤
3.1.4 單元測試-assert
import 了testify/assert 的Equal函數
3.1.5 單元測試-覆蓋率
這是一個判斷是否及格的函數,超過60分,返回true,否則返回false
右邊是對輸入為70 的單元測試,我們執行右邊的單側
通過指定go test [] --cover參數,我們看輸出了 覆蓋率為66.7。
一共三行,我們的單測試執行了2行,所以為66.7
下一步就是提升覆蓋率,我們可以增加一個不及格的測試case,重新執行所有單側,最終覆蓋率為100%。
也就是說,我們通過不斷對各個分支的測試,保證了覆蓋率和完備性。
3.1.6 單元測試-Tips
在實際項目中,一般的要求是50%~60% 覆蓋率,而對于資金型服務,覆蓋率可能要求達到80%;
我們做單元測試,測試分支相互獨立,全面覆蓋,則要求函數體足夠小,這樣就比較簡單的提升覆蓋率,也符合函數設計的單一職責。
3.2 單元測試-依賴
工程中復雜的項目,一般會依賴數據庫、緩存等,而我們的單測需要保證穩定性和冪等性
穩定是指相互隔離,能在任何時間,任何環境,運行測試。
冪等是指每一次測試運行都應該產生與之前一樣的結果。而要實現這一目的就要用到mock機制。
3.3 單元測試-文件處理
下面舉個例子,打開文件讀入 將文件中的第一行字符串中的11替換成00,執行單測,測試通過,而我們的單測需要依賴本地的文件,如果文件被修改或者刪除測試就會fail。
為了保證測試case的穩定性,我們對讀取文件函數進行mock,屏蔽對于文件的依賴。
3.4 單元測試-Mock
monkey是一個開源的mock測試庫,可以對method,或者實例方法的方法進行mock(打樁)
打樁指的是:用一個函數A 替換函數B,B作為原函數;A作為打樁函數
Mock Patch(原函數,需要打樁的函數) 的作用域在 Runtime;
Unpatch(打樁函數):打樁結束后把原函數卸載掉;
monkey主要是在運行時通過 Go 的 unsafe 包,能夠將內存中函數的地址替換為運行時函數的地址,在測試中調用的就是運行時的打樁函數B地址。
下面是一個mock的使用樣例,通過patch對Readfineline進行打樁mock,默認返回line110,這里通過defer卸載mock,這樣整個測試函數就擺脫了本地文件的束縛和依賴。
3.5 基準測試
Go 語言還提供了基準測試框架,基準測試是指測試一段程序的運行性能及耗費 CPU 的程度。
而我們在實際項目開發中,經常會遇到代碼性能瓶頸,為了定位問題經常要對代碼做性能分析,這就用到了基準測試。使用方法類似于單元測試,
3.5.1 基準測試-例子
這里舉一個服務器負載均衡的例子,首先我們有10個服務器列表,每次隨機(rand包)執行select函數隨機選擇一個執行。
3.5.2 基準測試-運行
基準測試以Benchmark開頭,入參是testing.B, 用b中的N值反復遞增循環測試(對一個測試用例的默認測試時間是 1 秒,當測試用例函數返回時還不到 1 秒,那么 testing.B 中的 N 值將按 1、2、5、10、20、50……遞增,并以遞增后的值重新進行用例函數測試)
Resttimer重置計時器,我們在reset之前做了init或其他的準備操作,這些操作不應該作為基準測試的范圍;
runparallel()函數是多協程并發測試;執行 2個基準測試,發現代碼在并發情況下存在劣化,主要原因是rand為了保證全局的隨機性和并發安全,持有了一把全局鎖。
3.5.3 基準測試-優化
GitHub - bytedance/gopkg: Universal Utilities for Go
而為了解決這一隨機函數性能問題,開源了一個高性能隨機數方法fastrand,上面有開源地址;我們這邊再做一下基準測試,性能提升百倍。主要的思路是犧牲了一定的數列的一致性,在大多數場景是適用的,同學在后面遇到隨機的場景可以嘗試用一下。
4 項目實踐
4.0需求背景
大家應該都是從掘金的 社區話題入口報名的,都看到過這個頁面,頁面的功能包括話題詳情,回帖列表,支持回帖,點贊,和回帖回復
我們今天就以此為需求模型,開發一個該頁面交涉及的服務端小功能。
4.1 需求描述
????????????????下面是需求的詳細描述;Ok,如果該功能由同學自己實現,如何去做模型設計,可以拿筆和紙簡單畫一下,然后打開ide,爭取我們一起完成這個實現,后面跟不上也不要著急,代碼已經托管到github,大家可以自行下載查看GitHub - Moonlight-Zhao/go-project-example
4.2 需求用例
我們從用例分析一步步拆解實現,主要涉及的功能點,用戶瀏覽消費,涉及頁面的展示,包括話題內容和回帖的列表,其實從圖中我們應該會抽出2個實體的,而實體的屬性有哪些,他們之間的聯系又如何? 大家想一下,可以先定義一下結構體
4.3 ER 圖
4.4 分層結構
整體分為三層
repository數據層,service邏輯層,controoler視圖層
數據層關聯底層數據模型,也就是這里的model,封裝外部數據的增刪改查,我們的數據存儲在本地文件,通過文件操作拉取話題,帖子數據;
數據層面向邏輯層,對service層透明,數據層屏蔽下游數據差異,也就是不管下游是文件,還是數據庫,還是微服務等,對service層的接口模型是不變的。
Servcie邏輯層處理核心業務邏輯,接收數據層數據 計算打包業務實體entiy,對應我們的需求,就是話題頁面,包括話題和回帖列表,并上送給視圖層;
Controller視圖層負責處理和外部的交互邏輯,以view視圖的形式返回給客戶端,對于我們需求,封裝json格式化的請求結果,api形式訪問就好
4.5 組件工具
下面介紹下開發涉及的基礎組件和工具
首先是gin,高性能開源的go web框架,我們基于gin 搭建web服務器,在課程手冊應該提到了,這里我們只是簡單的使用,主要涉及路由分發,不會涉及其他復雜的概念。
因為我們引入了web框架,所以就涉及go module依賴管理,如前面依賴管理課程內容講解,我們首先通過go mod是初始化go mod管理配置文件,然后go get下載gin依賴,這里顯示用了V1.3.0版本。
有了框架依賴,我們只需要關注業務本身的實現,
從下至上:reposity-->service-->controller,我們一步步實現。希望大家能跟上我的節奏,從0~1 實現這個項目,如果時間問題,大家可以一步步copy一下,主要是走一下開發思路。
4.6 Repository數據層
首先是reposity,我們可以根據之前的er圖先定義struct文件中每行的格式如圖所示
那如何實現QueryTopicById(話題),QueryPostsByParentId(帖子)?
4.6 Repository-index
一方面查詢我們可以用全掃描遍歷的方式,但是這雖然能達到我們的目的,但是并非高效的方式;
所以這里引出索引的概念,索引就像書的目錄,可以引導我們快速查找定位我們需要的結果;
這里我們用map實現內存索引,在服務對外暴露前,利用文件元數據初始化全局內存索引,這樣就可以實現0(1)的時間復雜度查找操作。
下面是具體的實現,首先是os.Open打開文件,基于file 初始化scanner,通過迭代器方式遍歷scanner數據行,轉化為結構體Topic存儲至內存map,這就是初始化話題內存索引。
4.6 Repository-查詢
有了內存索引,下一步就是實現查詢操作就比較簡單了
直接根據查詢key獲得map中的value就好了,這里用到了sync.once,主要適用高并發的場景下只執行一次的場景,這里的基于once的實現模式就是我們平常說的單例模式,減少存儲的浪費。
有了topic的查詢代碼,大家可以照貓畫虎 自行實現一下根據話題id查詢回帖列表的查詢方法
func (*PostDao) QueryPostByParentId(parentId int64) ([]*Post, error) {var posts []*Posterr := db.Where("parent_id = ?", parentId).Find(&posts).Errorif err != nil {util.Logger.Error("find posts by parent_id err:" + err.Error())return nil, err}return posts, nil
}
4.7 Service邏輯層
有了reposity層以后,下面我們開始實現service層,首先我們定義servcie層實體PageInfo,包括:Topic和PostList
下面是具體的servcie流程編排 通過err控制流程退出,正常會返回頁面信息,err為nil
func (f *QueryPageInfoFlow) Do() (*PageInfo, error) {if err := f.checkParam(); err != nil {//簡單的id校驗return nil, err}if err := f.prepareInfo(); err != nil {return nil, err}if err := f.packPageInfo(); err != nil {return nil, err}return f.pageInfo, nil
}
關于prepareInfo方法,話題和回帖信息的獲取都依賴topicid,這樣2個流程沒有相互依賴;
這就可以并行執行,提高執行效率。
WaitGroup Add(2);(2個并行的協程);wait等待兩個協程的信息從數據層返回
大家在后期做項目開發中,一定要思考流程是否可以并行,通過壓榨CPU,降低接口耗時,不要一味的串行實現,浪費多核cpu的資源
Paramcheck 和pack這里就不講了,給大家一點時間 編碼 ,copy代碼
4.8 Controller視圖層
這里我們定義一個view對象,通過code msg打包業務狀態信息,用data承載業務實體信息,輸入
4.9 Router
最后是web服務的引擎配置,path映射到具體的controller。通過path變量傳遞話題id
4.10 運行
最后執行go run 本地啟動web服務,通過curl命令請求服務暴露的接口,當然平時寫代碼不可能像我講的這么順暢,難免有bug,大家要做好完備的單元測試,快速定位問題,解決問題。
鼠鼠出bug了捏!
好的,以上就是對社區話題頁面需求的整個實現流程,這樣我們從項目拆解,代碼設計落地,最后測試運行就跑通了整個的項目流程,為大家后期實現項目提供了一定的開發思路。當然實際項目較我們實現的需求會復雜很多,不過大家也不必擔心,可以通過大拆小的思路,將大需求拆解為小需求的思路來分析解決,遇到問題,各個擊破,同時做好充分的測試。 課后作業
非常感謝您閱讀到這里,如果這篇文章對您有幫助,希望能留下您的點贊👍 關注💖 收藏 💕評論💬感謝支持!!!