Go語(yǔ)言單元測(cè)試基礎(chǔ)從入門到放棄
Go語(yǔ)言測(cè)試
這是Go單測(cè)從入門到放棄系列教程的第0篇,主要講解在Go語(yǔ)言中如何做單元測(cè)試以及介紹了表格驅(qū)動(dòng)測(cè)試、回歸測(cè)試,并且介紹了常用的斷言工具。
go test工具
Go語(yǔ)言中的測(cè)試依賴go test
命令。編寫測(cè)試代碼和編寫普通的Go代碼過程是類似的,并不需要學(xué)習(xí)新的語(yǔ)法、規(guī)則或工具。
go test命令是一個(gè)按照一定約定和組織的測(cè)試代碼的驅(qū)動(dòng)程序。在包目錄內(nèi),所有以_test.go
為后綴名的源代碼文件都是go test
測(cè)試的一部分,不會(huì)被go build
編譯到最終的可執(zhí)行文件中。
在*_test.go
文件中有三種類型的函數(shù),單元測(cè)試函數(shù)、基準(zhǔn)測(cè)試函數(shù)和示例函數(shù)。
類型 | 格式 | 作用 |
---|---|---|
測(cè)試函數(shù) | 函數(shù)名前綴為Test | 測(cè)試程序的一些邏輯行為是否正確 |
基準(zhǔn)函數(shù) | 函數(shù)名前綴為Benchmark | 測(cè)試函數(shù)的性能 |
示例函數(shù) | 函數(shù)名前綴為Example | 為文檔提供示例文檔 |
go test
命令會(huì)遍歷所有的*_test.go
文件中符合上述命名規(guī)則的函數(shù),然后生成一個(gè)臨時(shí)的main包用于調(diào)用相應(yīng)的測(cè)試函數(shù),然后構(gòu)建并運(yùn)行、報(bào)告測(cè)試結(jié)果,最后清理測(cè)試中生成的臨時(shí)文件。
單元測(cè)試函數(shù)
格式
每個(gè)測(cè)試函數(shù)必須導(dǎo)入testing
包,測(cè)試函數(shù)的基本格式(簽名)如下:
func?TestName(t?*testing.T){ ????//?... }
測(cè)試函數(shù)的名字必須以Test
開頭,可選的后綴名必須以大寫字母開頭,舉幾個(gè)例子:
func?TestAdd(t?*testing.T){?...?} func?TestSum(t?*testing.T){?...?} func?TestLog(t?*testing.T){?...?}
其中參數(shù)t
用于報(bào)告測(cè)試失敗和附加的日志信息。testing.T
的擁有的方法如下:
func?(c?*T)?Cleanup(func()) func?(c?*T)?Error(args?...interface{}) func?(c?*T)?Errorf(format?string,?args?...interface{}) func?(c?*T)?Fail() func?(c?*T)?FailNow() func?(c?*T)?Failed()?bool func?(c?*T)?Fatal(args?...interface{}) func?(c?*T)?Fatalf(format?string,?args?...interface{}) func?(c?*T)?Helper() func?(c?*T)?Log(args?...interface{}) func?(c?*T)?Logf(format?string,?args?...interface{}) func?(c?*T)?Name()?string func?(c?*T)?Skip(args?...interface{}) func?(c?*T)?SkipNow() func?(c?*T)?Skipf(format?string,?args?...interface{}) func?(c?*T)?Skipped()?bool func?(c?*T)?TempDir()?string
單元測(cè)試示例
就像細(xì)胞是構(gòu)成我們身體的基本單位,一個(gè)軟件程序也是由很多單元組件構(gòu)成的。單元組件可以是函數(shù)、結(jié)構(gòu)體、方法和最終用戶可能依賴的任意東西。總之我們需要確保這些組件是能夠正常運(yùn)行的。單元測(cè)試是一些利用各種方法測(cè)試單元組件的程序,它會(huì)將結(jié)果與預(yù)期輸出進(jìn)行比較。
接下來(lái),我們?cè)?code>base_demo包中定義了一個(gè)Split
函數(shù),具體實(shí)現(xiàn)如下:
//?base_demo/split.go package?base_demo import?"strings" //?Split?把字符串s按照給定的分隔符sep進(jìn)行分割返回字符串切片 func?Split(s,?sep?string)?(result?[]string)?{ ?i?:=?strings.Index(s,?sep) ?for?i?>?-1?{ ??result?=?append(result,?s[:i]) ??s?=?s[i+1:] ??i?=?strings.Index(s,?sep) ?} ?result?=?append(result,?s) ?return }
在當(dāng)前目錄下,我們創(chuàng)建一個(gè)split_test.go
的測(cè)試文件,并定義一個(gè)測(cè)試函數(shù)如下:
//?split/split_test.go package?split import?( ?"reflect" ?"testing" ) func?TestSplit(t?*testing.T)?{?//?測(cè)試函數(shù)名必須以Test開頭,必須接收一個(gè)*testing.T類型參數(shù) ?got?:=?Split("a:b:c",?":")?????????//?程序輸出的結(jié)果 ?want?:=?[]string{"a",?"b",?"c"}????//?期望的結(jié)果 ?if?!reflect.DeepEqual(want,?got)?{?//?因?yàn)閟lice不能比較直接,借助反射包中的方法比較 ??t.Errorf("expected:%v,?got:%v",?want,?got)?//?測(cè)試失敗輸出錯(cuò)誤提示 ?} }
此時(shí)split
這個(gè)包中的文件如下:
??ls?-l total?16 -rw-r--r--??1?liwenzhou??staff??408??4?29?15:50?split.go -rw-r--r--??1?liwenzhou??staff??466??4?29?16:04?split_test.go
在當(dāng)前路徑下執(zhí)行go test
命令,可以看到輸出結(jié)果如下:
? go test
PASS
ok golang-unit-test-demo/base_demo 0.005s
go test -v
一個(gè)測(cè)試用例有點(diǎn)單薄,我們?cè)倬帉懸粋€(gè)測(cè)試使用多個(gè)字符切割字符串的例子,在split_test.go
中添加如下測(cè)試函數(shù):
func?TestSplitWithComplexSep(t?*testing.T)?{ ?got?:=?Split("abcd",?"bc") ?want?:=?[]string{"a",?"d"} ?if?!reflect.DeepEqual(want,?got)?{ ??t.Errorf("expected:%v,?got:%v",?want,?got) ?} }
現(xiàn)在我們有多個(gè)測(cè)試用例了,為了能更好的在輸出結(jié)果中看到每個(gè)測(cè)試用例的執(zhí)行情況,我們可以為go test
命令添加-v
參數(shù),讓它輸出完整的測(cè)試結(jié)果。
? go test -v
=== RUN TestSplit
--- PASS: TestSplit (0.00s)
=== RUN TestSplitWithComplexSep
split_test.go:20: expected:[a d], got:[a cd]
--- FAIL: TestSplitWithComplexSep (0.00s)
FAIL
exit status 1
FAIL golang-unit-test-demo/base_demo 0.009s
從上面的輸出結(jié)果我們能清楚的看到是TestSplitWithComplexSep
這個(gè)測(cè)試用例沒有測(cè)試通過。
go test -run
單元測(cè)試的結(jié)果表明split
函數(shù)的實(shí)現(xiàn)并不可靠,沒有考慮到傳入的sep參數(shù)是多個(gè)字符的情況,下面我們來(lái)修復(fù)下這個(gè)Bug:
package?base_demo import?"strings" //?Split?把字符串s按照給定的分隔符sep進(jìn)行分割返回字符串切片 func?Split(s,?sep?string)?(result?[]string)?{ ?i?:=?strings.Index(s,?sep) ?for?i?>?-1?{ ??result?=?append(result,?s[:i]) ??s?=?s[i+len(sep):]?//?這里使用len(sep)獲取sep的長(zhǎng)度 ??i?=?strings.Index(s,?sep) ?} ?result?=?append(result,?s) ?return }
在執(zhí)行go test
命令的時(shí)候可以添加-run
參數(shù),它對(duì)應(yīng)一個(gè)正則表達(dá)式,只有函數(shù)名匹配上的測(cè)試函數(shù)才會(huì)被go test
命令執(zhí)行。
例如通過給go test
添加-run=Sep
參數(shù)來(lái)告訴它本次測(cè)試只運(yùn)行TestSplitWithComplexSep
這個(gè)測(cè)試用例:
??go?test?-run=Sep?-v ===?RUN???TestSplitWithComplexSep ---?PASS:?TestSplitWithComplexSep?(0.00s) PASS ok??????golang-unit-test-demo/base_demo?0.010s
最終的測(cè)試結(jié)果表情我們成功修復(fù)了之前的Bug。
回歸測(cè)試
我們修改了代碼之后僅僅執(zhí)行那些失敗的測(cè)試用例或新引入的測(cè)試用例是錯(cuò)誤且危險(xiǎn)的,正確的做法應(yīng)該是完整運(yùn)行所有的測(cè)試用例,保證不會(huì)因?yàn)樾薷拇a而引入新的問題。
??go?test?-v ===?RUN???TestSplit ---?PASS:?TestSplit?(0.00s) ===?RUN???TestSplitWithComplexSep ---?PASS:?TestSplitWithComplexSep?(0.00s) PASS ok??????golang-unit-test-demo/base_demo?0.011s
測(cè)試結(jié)果表明我們的單元測(cè)試全部通過。
通過這個(gè)示例我們可以看到,有了單元測(cè)試就能夠在代碼改動(dòng)后快速進(jìn)行回歸測(cè)試,極大地提高開發(fā)效率并保證代碼的質(zhì)量。
跳過某些測(cè)試用例
為了節(jié)省時(shí)間支持在單元測(cè)試時(shí)跳過某些耗時(shí)的測(cè)試用例。
func?TestTimeConsuming(t?*testing.T)?{ ????if?testing.Short()?{ ????????t.Skip("short模式下會(huì)跳過該測(cè)試用例") ????} ????... }
當(dāng)執(zhí)行go test -short
時(shí)就不會(huì)執(zhí)行上面的TestTimeConsuming
測(cè)試用例。
子測(cè)試
在上面的示例中我們?yōu)槊恳粋€(gè)測(cè)試數(shù)據(jù)編寫了一個(gè)測(cè)試函數(shù),而通常單元測(cè)試中需要多組測(cè)試數(shù)據(jù)保證測(cè)試的效果。Go1.7+ 中新增了子測(cè)試,支持在測(cè)試函數(shù)中使用t.Run
執(zhí)行一組測(cè)試用例,這樣就不需要為不同的測(cè)試數(shù)據(jù)定義多個(gè)測(cè)試函數(shù)了。
func?TestXXX(t?*testing.T){ ??t.Run("case1",?func(t?*testing.T){...}) ??t.Run("case2",?func(t?*testing.T){...}) ??t.Run("case3",?func(t?*testing.T){...}) }
表格驅(qū)動(dòng)測(cè)試
介紹
編寫好的測(cè)試并非易事,但在許多情況下,表格驅(qū)動(dòng)測(cè)試可以涵蓋很多方面:表格里的每一個(gè)條目都是一個(gè)完整的測(cè)試用例,包含輸入和預(yù)期結(jié)果,有時(shí)還包含測(cè)試名稱等附加信息,以使測(cè)試輸出易于閱讀。
使用表格驅(qū)動(dòng)測(cè)試能夠很方便的維護(hù)多個(gè)測(cè)試用例,避免在編寫單元測(cè)試時(shí)頻繁的復(fù)制粘貼。
表格驅(qū)動(dòng)測(cè)試的步驟通常是定義一個(gè)測(cè)試用例表格,然后遍歷表格,并使用t.Run
對(duì)每個(gè)條目執(zhí)行必要的測(cè)試。
表格驅(qū)動(dòng)測(cè)試不是工具、包或其他任何東西,它只是編寫更清晰測(cè)試的一種方式和視角。
示例
官方標(biāo)準(zhǔn)庫(kù)中有很多表格驅(qū)動(dòng)測(cè)試的示例,例如fmt包中的測(cè)試代碼:
var?flagtests?=?[]struct?{ ?in??string ?out?string }{ ?{"%a",?"[%a]"}, ?{"%-a",?"[%-a]"}, ?{"%+a",?"[%+a]"}, ?{"%#a",?"[%#a]"}, ?{"%?a",?"[%?a]"}, ?{"%0a",?"[%0a]"}, ?{"%1.2a",?"[%1.2a]"}, ?{"%-1.2a",?"[%-1.2a]"}, ?{"%+1.2a",?"[%+1.2a]"}, ?{"%-+1.2a",?"[%+-1.2a]"}, ?{"%-+1.2abc",?"[%+-1.2a]bc"}, ?{"%-1.2abc",?"[%-1.2a]bc"}, } func?TestFlagParser(t?*testing.T)?{ ?var?flagprinter?flagPrinter ?for?_,?tt?:=?range?flagtests?{ ??t.Run(tt.in,?func(t?*testing.T)?{ ???s?:=?Sprintf(tt.in,?&flagprinter) ???if?s?!=?tt.out?{ ????t.Errorf("got?%q,?want?%q",?s,?tt.out) ???} ??}) ?} }
通常表格是匿名結(jié)構(gòu)體數(shù)組切片,可以定義結(jié)構(gòu)體或使用已經(jīng)存在的結(jié)構(gòu)進(jìn)行結(jié)構(gòu)體數(shù)組聲明。name屬性用來(lái)描述特定的測(cè)試用例。
接下來(lái)讓我們?cè)囍约壕帉懕砀耱?qū)動(dòng)測(cè)試:
func?TestSplitAll(t?*testing.T)?{ ?//?定義測(cè)試表格 ?//?這里使用匿名結(jié)構(gòu)體定義了若干個(gè)測(cè)試用例 ?//?并且為每個(gè)測(cè)試用例設(shè)置了一個(gè)名稱 ?tests?:=?[]struct?{ ??name??string ??input?string ??sep???string ??want??[]string ?}{ ??{"base?case",?"a:b:c",?":",?[]string{"a",?"b",?"c"}}, ??{"wrong?sep",?"a:b:c",?",",?[]string{"a:b:c"}}, ??{"more?sep",?"abcd",?"bc",?[]string{"a",?"d"}}, ??{"leading?sep",?"沙河有沙又有河",?"沙",?[]string{"",?"河有",?"又有河"}}, ?} ?//?遍歷測(cè)試用例 ?for?_,?tt?:=?range?tests?{ ??t.Run(tt.name,?func(t?*testing.T)?{?//?使用t.Run()執(zhí)行子測(cè)試 ???got?:=?Split(tt.input,?tt.sep) ???if?!reflect.DeepEqual(got,?tt.want)?{ ????t.Errorf("expected:%#v,?got:%#v",?tt.want,?got) ???} ??}) ?} }
在終端執(zhí)行go test -v
,會(huì)得到如下測(cè)試輸出結(jié)果:
? go test -v
=== RUN TestSplit
--- PASS: TestSplit (0.00s)
=== RUN TestSplitWithComplexSep
--- PASS: TestSplitWithComplexSep (0.00s)
=== RUN TestSplitAll
=== RUN TestSplitAll/base_case
=== RUN TestSplitAll/wrong_sep
=== RUN TestSplitAll/more_sep
=== RUN TestSplitAll/leading_sep
--- PASS: TestSplitAll (0.00s)
--- PASS: TestSplitAll/base_case (0.00s)
--- PASS: TestSplitAll/wrong_sep (0.00s)
--- PASS: TestSplitAll/more_sep (0.00s)
--- PASS: TestSplitAll/leading_sep (0.00s)
PASS
ok golang-unit-test-demo/base_demo 0.010s
并行測(cè)試
表格驅(qū)動(dòng)測(cè)試中通常會(huì)定義比較多的測(cè)試case,在Go語(yǔ)言中很容易發(fā)揮自身并發(fā)優(yōu)勢(shì)將表格驅(qū)動(dòng)測(cè)試并行化,可以查看下面的代碼示例。
func?TestSplitAll(t?*testing.T)?{ ?t.Parallel()??//?將?TLog?標(biāo)記為能夠與其他測(cè)試并行運(yùn)行 ?//?定義測(cè)試表格 ?//?這里使用匿名結(jié)構(gòu)體定義了若干個(gè)測(cè)試用例 ?//?并且為每個(gè)測(cè)試用例設(shè)置了一個(gè)名稱 ?tests?:=?[]struct?{ ??name??string ??input?string ??sep???string ??want??[]string ?}{ ??{"base?case",?"a:b:c",?":",?[]string{"a",?"b",?"c"}}, ??{"wrong?sep",?"a:b:c",?",",?[]string{"a:b:c"}}, ??{"more?sep",?"abcd",?"bc",?[]string{"a",?"d"}}, ??{"leading?sep",?"沙河有沙又有河",?"沙",?[]string{"",?"河有",?"又有河"}}, ?} ?//?遍歷測(cè)試用例 ?for?_,?tt?:=?range?tests?{ ??tt?:=?tt??//?注意這里重新聲明tt變量(避免多個(gè)goroutine中使用了相同的變量) ??t.Run(tt.name,?func(t?*testing.T)?{?//?使用t.Run()執(zhí)行子測(cè)試 ???t.Parallel()??//?將每個(gè)測(cè)試用例標(biāo)記為能夠彼此并行運(yùn)行 ???got?:=?Split(tt.input,?tt.sep) ???if?!reflect.DeepEqual(got,?tt.want)?{ ????t.Errorf("expected:%#v,?got:%#v",?tt.want,?got) ???} ??}) ?} }
使用工具生成測(cè)試代碼
社區(qū)里有很多自動(dòng)生成表格驅(qū)動(dòng)測(cè)試函數(shù)的工具,比如gotests等,很多編輯器如Goland也支持快速生成測(cè)試文件。這里簡(jiǎn)單演示一下gotests
的使用。
安裝
go?get?-u?github.com/cweill/gotests/...
執(zhí)行
gotests?-all?-w?split.go
上面的命令表示,為split.go
文件的所有函數(shù)生成測(cè)試代碼至split_test.go
文件(目錄下如果事先存在這個(gè)文件就不再生成)。
生成的測(cè)試代碼大致如下:
package?base_demo import?( ?"reflect" ?"testing" ) func?TestSplit(t?*testing.T)?{ ?type?args?struct?{ ??s???string ??sep?string ?} ?tests?:=?[]struct?{ ??name???????string ??args???????args ??wantResult?[]string ?}{ ??//?TODO:?Add?test?cases. ?} ?for?_,?tt?:=?range?tests?{ ??t.Run(tt.name,?func(t?*testing.T)?{ ???if?gotResult?:=?Split(tt.args.s,?tt.args.sep);?!reflect.DeepEqual(gotResult,?tt.wantResult)?{ ????t.Errorf("Split()?=?%v,?want?%v",?gotResult,?tt.wantResult) ???} ??}) ?} }
代碼格式與我們上面的類似,只需要在TODO位置添加我們的測(cè)試邏輯就可以了。
測(cè)試覆蓋率
測(cè)試覆蓋率是指代碼被測(cè)試套件覆蓋的百分比。通常我們使用的都是語(yǔ)句的覆蓋率,也就是在測(cè)試中至少被運(yùn)行一次的代碼占總代碼的比例。在公司內(nèi)部一般會(huì)要求測(cè)試覆蓋率達(dá)到80%左右。
Go提供內(nèi)置功能來(lái)檢查你的代碼覆蓋率。我們可以使用go test -cover
來(lái)查看測(cè)試覆蓋率。例如:
??go?test?-cover PASS coverage:?100.0%?of?statements ok??????golang-unit-test-demo/base_demo?0.009s
從上面的結(jié)果可以看到我們的測(cè)試用例覆蓋了100%的代碼。
Go還提供了一個(gè)額外的-coverprofile
參數(shù),用來(lái)將覆蓋率相關(guān)的記錄信息輸出到一個(gè)文件。例如:
??go?test?-cover?-coverprofile=c.out PASS coverage:?100.0%?of?statements ok??????golang-unit-test-demo/base_demo?0.009s
上面的命令會(huì)將覆蓋率相關(guān)的信息輸出到當(dāng)前文件夾下面的c.out
文件中。
??tree?. . ├──?c.out ├──?split.go └──?split_test.go
然后我們執(zhí)行go tool cover -html=c.out
,使用cover
工具來(lái)處理生成的記錄信息,該命令會(huì)打開本地的瀏覽器窗口生成一個(gè)HTML報(bào)告。
上圖中每個(gè)用綠色標(biāo)記的語(yǔ)句塊表示被覆蓋了,而紅色的表示沒有被覆蓋。
testify/assert
testify是一個(gè)社區(qū)非常流行的Go單元測(cè)試工具包,其中使用最多的功能就是它提供的斷言工具——testify/assert
或testify/require
。
安裝
go?get?github.com/stretchr/testify
使用示例
我們?cè)趯憜卧獪y(cè)試的時(shí)候,通常需要使用斷言來(lái)校驗(yàn)測(cè)試結(jié)果,但是由于Go語(yǔ)言中沒有提供斷言,所以我們會(huì)寫出很多的if...else...
語(yǔ)句。而testify/assert
為我們提供了很多常用的斷言函數(shù),并且能夠輸出友好、易于閱讀的錯(cuò)誤描述信息。
比如我們之前在TestSplit
測(cè)試函數(shù)中就使用了reflect.DeepEqual
來(lái)判斷期望結(jié)果與實(shí)際結(jié)果是否一致。
t.Run(tt.name,?func(t?*testing.T)?{?//?使用t.Run()執(zhí)行子測(cè)試 ?got?:=?Split(tt.input,?tt.sep) ?if?!reflect.DeepEqual(got,?tt.want)?{ ??t.Errorf("expected:%#v,?got:%#v",?tt.want,?got) ?} })
使用testify/assert
之后就能將上述判斷過程簡(jiǎn)化如下:
t.Run(tt.name,?func(t?*testing.T)?{?//?使用t.Run()執(zhí)行子測(cè)試 ?got?:=?Split(tt.input,?tt.sep) ?assert.Equal(t,?got,?tt.want)??//?使用assert提供的斷言函數(shù) })
當(dāng)我們有多個(gè)斷言語(yǔ)句時(shí),還可以使用assert := assert.New(t)創(chuàng)建一個(gè)assert對(duì)象,它擁有前面所有的斷言方法,只是不需要再傳入Testing.T
參數(shù)了。
func?TestSomething(t?*testing.T)?{ ??assert?:=?assert.New(t) ??//?assert?equality ??assert.Equal(123,?123,?"they?should?be?equal") ??//?assert?inequality ??assert.NotEqual(123,?456,?"they?should?not?be?equal") ??//?assert?for?nil?(good?for?errors) ??assert.Nil(object) ??//?assert?for?not?nil?(good?when?you?expect?something) ??if?assert.NotNil(object)?{ ????//?now?we?know?that?object?isn't?nil,?we?are?safe?to?make ????//?further?assertions?without?causing?any?errors ????assert.Equal("Something",?object.Value) ??} }
testify/assert
提供了非常多的斷言函數(shù),這里沒辦法一一列舉出來(lái),大家可以查看官方文檔了解。
testify/require
擁有testify/assert
所有斷言函數(shù),它們的唯一區(qū)別就是——testify/require
遇到失敗的用例會(huì)立即終止本次測(cè)試。
此外,testify
包還提供了mock、http等其他測(cè)試工具,篇幅所限這里就不詳細(xì)介紹了,有興趣的同學(xué)可以自己了解一下。
總結(jié)
本文介紹了Go語(yǔ)言單元測(cè)試的基本用法,通過為Split函數(shù)編寫單元測(cè)試的真實(shí)案例,模擬了日常開發(fā)過程中的場(chǎng)景,一步一步詳細(xì)介紹了表格驅(qū)動(dòng)測(cè)試、回歸測(cè)試和常用的斷言工具testify/assert的使用。在下一篇中,我們將更進(jìn)一步,詳細(xì)介紹如何使用httptest和gock工具進(jìn)行網(wǎng)絡(luò)測(cè)試,更多關(guān)于Go語(yǔ)言單元測(cè)試基礎(chǔ)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
解決golang post文件時(shí)Content-Type出現(xiàn)的問題
這篇文章主要介紹了解決golang post文件時(shí)Content-Type出現(xiàn)的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來(lái)看看吧2021-05-05Golang標(biāo)準(zhǔn)庫(kù)container/list的用法圖文詳解
提到單向鏈表,大家應(yīng)該是比較熟悉的了,這篇文章主要為大家詳細(xì)介紹了Golang標(biāo)準(zhǔn)庫(kù)container/list的用法相關(guān)知識(shí),感興趣的小伙伴可以了解下2024-01-01詳解golang channel有無(wú)緩沖區(qū)的區(qū)別
這篇文章主要給大家介紹了golang channel有無(wú)緩沖區(qū)的區(qū)別,無(wú)緩沖是同步的,有緩沖是異步的,文中通過代碼示例給大家講解的非常詳細(xì),需要的朋友可以參考下2024-01-01