pull/1/head
chai2010 2015-12-11 17:19:15 +08:00
parent fd6262f241
commit 643409dd27
4 changed files with 44 additions and 44 deletions

View File

@ -1,15 +1,15 @@
## 11.3. 測試覆蓋率
就其性质而言, 测试不可能是完整的. 计算机科学家 Edsger Dijkstra 曾说过: "测试可以显示存在缺陷, 但是并不是说没有BUG." 再多的测试也不能证明一个包没有BUG. 在最好的情况下, 测试可以增强我们的信息, 包在我们测试的环境是可以正常工作的.
就其性質而言, 測試不可能是完整的. 計算機科學傢 Edsger Dijkstra 曾説過: "測試可以顯示存在缺陷, 但是併不是説沒有BUG." 再多的測試也不能證明一個包沒有BUG. 在最好的情況下, 測試可以增強我們的信息, 包在我們測試的環境是可以正常工作的.
测试驱动触发运行到的被测试函数的代码数目称为测试的覆盖率. 测试覆盖率并不能量化 — 甚至连最简单的动态程序也难以精确测量 — 但是可以启发并帮助我们编写的有效的测试代码.
測試驅動觸發運行到的被測試函數的代碼數目稱爲測試的覆蓋率. 測試覆蓋率併不能量化 — 甚至連最簡單的動態程序也難以精確測量 — 但是可以啓發併幫助我們編寫的有效的測試代碼.
这些帮助信息中语句的覆盖率是最简单和最广泛使用的. 语句的覆盖率是指在测试中至少被运行一次的代码占总代码数的比例. 在本节中, 我们使用 `go test` 中集成的测试覆盖率工具, 来度量下面代码的测试覆盖率, 帮助我们识别测试和我们期望间的差距.
這些幫助信息中語句的覆蓋率是最簡單和最廣汎使用的. 語句的覆蓋率是指在測試中至少被運行一次的代碼佔總代碼數的比例. 在本節中, 我們使用 `go test` 中集成的測試覆蓋率工具, 來度量下麫代碼的測試覆蓋率, 幫助我們識彆測試和我們期望間的差距.
The code below is a table-driven test for the expression evaluator we built back in Chapter 7:
面的代码是一个表格驱动的测试, 用于测试第七章的表达式求值程序:
麫的代碼是一個錶格驅動的測試, 用於測試第七章的錶達式求值程序:
```Go
gopl.io/ch7/eval
@ -49,7 +49,7 @@ func TestCoverage(t *testing.T) {
}
```
首先, 我们要确保所有的测试都正常通过:
首先, 我們要確保所有的測試都正常通過:
```
$ go test -v -run=Coverage gopl.io/ch7/eval
@ -59,7 +59,7 @@ PASS
ok gopl.io/ch7/eval 0.011s
```
面这个命令可以显示测试覆盖率工具的用法信息:
麫這個命令可以顯示測試覆蓋率工具的用法信息:
```
$ go tool cover
@ -72,20 +72,20 @@ Open a web browser displaying annotated source code:
...
```
`go tool` 命令运行Go工具链的底层可执行程序. 这些底层可执行程序放在 $GOROOT/pkg/tool/${GOOS}_${GOARCH} 目录. 因为 `go build` 的原因, 我们很小直接调用这些底层工具.
`go tool` 命令運行Go工具鏈的底層可執行程序. 這些底層可執行程序放在 $GOROOT/pkg/tool/${GOOS}_${GOARCH} 目彔. 因爲 `go build` 的原因, 我們很小直接調用這些底層工具.
现在我们可以用 `-coverprofile` 标志参数重新运行:
現在我們可以用 `-coverprofile` 標誌蔘數重新運行:
```
$ go test -run=Coverage -coverprofile=c.out gopl.io/ch7/eval
ok gopl.io/ch7/eval 0.032s coverage: 68.5% of statements
```
这个标志参数通过插入生成钩子代码来统计覆盖率数据. 也就是说, 在运行每个测试前, 它会修改要测试代码的副本, 在每个块都会设置一个布尔标志变量. 当被修改后的被测试代码运行退出时, 将统计日志数据写入 c.out 文件, 并打印一部分执行的语句的一个总结. (如果你需要的是摘要,使用 `go test -cover`.)
這個標誌蔘數通過插入生成鉤子代碼來統計覆蓋率數據. 也就是説, 在運行每個測試前, 它會脩改要測試代碼的副本, 在每個塊都會設置一個佈爾標誌變量. 噹被脩改後的被測試代碼運行退齣時, 將統計日誌數據寫入 c.out 文件, 併打印一部分執行的語句的一個總結. (如果你需要的是摘要,使用 `go test -cover`.)
如果使用了 `-covermode=count` 标志参数, 那么将在每个代码块插入一个计数器而不是布尔标志量. 在统计结果中记录了每个块的执行次数, 这可以用于衡量哪些是被频繁执行的热点代码.
如果使用了 `-covermode=count` 標誌蔘數, 那麽將在每個代碼塊插入一個計數器而不是佈爾標誌量. 在統計結果中記彔了每個塊的執行次數, 這可以用於衡量哪些是被頻繁執行的熱點代碼.
为了收集数据, 我们运行了测试覆盖率工具, 打印了测试日志, 生成一个HTML报告, 然后在浏览器中打开(图11.3).
爲了收集數據, 我們運行了測試覆蓋率工具, 打印了測試日誌, 生成一個HTML報告, 然後在瀏覽器中打開(圖11.3).
```
$ go tool cover -html=c.out
@ -93,14 +93,14 @@ $ go tool cover -html=c.out
![](../images/ch11-03.png)
绿色的代码块被测试覆盖到了, 红色的则表示没有被覆盖到. 为了清晰起见, 我们将的背景红色文本的背景设置成了阴影效果. 我们可以马上发现 unary 操作的 Eval 方法并没有被执行到. 如果我们针对这部分未被覆盖的代码添加下面的测试, 然后重新运行上面的命令, 那么我们将会看到那个红色部分的代码也变成绿色了:
綠色的代碼塊被測試覆蓋到了, 紅色的則錶示沒有被覆蓋到. 爲了清晰起見, 我們將的背景紅色文本的背景設置成了陰影效果. 我們可以馬上發現 unary 操作的 Eval 方法併沒有被執行到. 如果我們針對這部分未被覆蓋的代碼添加下麫的測試, 然後重新運行上麫的命令, 那麽我們將會看到那個紅色部分的代碼也變成綠色了:
```
{"-x * -x", eval.Env{"x": 2}, "4"}
```
过两个 panic 语句依然是红色的. 这是没有问题的, 因为这两个语句并不会被执行到.
過兩個 panic 語句依然是紅色的. 這是沒有問題的, 因爲這兩個語句併不會被執行到.
实现 100% 的测试覆盖率听起来很好, 但是在具体实践中通常是不可行的, 也不是值得推荐的做法. 因为那只能说明代码被执行过而已, 并不意味着代码是没有BUG的; 因为对于逻辑复杂的语句需要针对不同的输入执行多次. 有一些语句, 例如上面的 panic 语句则永远都不会被执行到. 另外, 还有一些隐晦的错误在现实中很少遇到也很难编写对应的测试代码. 测试从本质上来说是一个比较务实的工作, 编写测试代码和编写应用代码的成本对比是需要考虑的. 测试覆盖率工具可以帮助我们快速识别测试薄弱的地方, 但是设计好的测试用例和编写应用代码一样需要严密的思考.
實現 100% 的測試覆蓋率聽起來很好, 但是在具體實踐中通常是不可行的, 也不是值得推薦的做法. 因爲那隻能説明代碼被執行過而已, 併不意味着代碼是沒有BUG的; 因爲對於邏輯復雜的語句需要針對不衕的輸入執行多次. 有一些語句, 例如上麫的 panic 語句則永遠都不會被執行到. 另外, 還有一些隱晦的錯誤在現實中很少遇到也很難編寫對應的測試代碼. 測試從本質上來説是一個比較務實的工作, 編寫測試代碼和編寫應用代碼的成本對比是需要考慮的. 測試覆蓋率工具可以幫助我們快速識彆測試薄弱的地方, 但是設計好的測試用例和編寫應用代碼一樣需要嚴密的思考.

View File

@ -1,22 +1,22 @@
## 11.5. 剖析
测量基准对于衡量特定操作的性能是有帮助的, 但是, 当我们视图让程序跑的更快的时候, 我们通常并不知道从哪里开始优化. 每个码农都应该知道 Donald Knuth 在1974年的 Structured Programming with go to Statements 上所说的格言. 虽然经常被解读为不重视性能的意思, 但是从原文我们可以看到不同的含义:
測量基準對於衡量特定操作的性能是有幫助的, 但是, 噹我們視圖讓程序跑的更快的時候, 我們通常併不知道從哪裡開始優化. 每個碼農都應該知道 Donald Knuth 在1974年的 Structured Programming with go to Statements 上所説的格言. 雖然經常被解讀爲不重視性能的意思, 但是從原文我們可以看到不衕的含義:
> 毫无疑问, 效率会导致各种滥用. 程序员需要浪费大量的时间思考, 或者担心, 被部分程序的速度所干扰, 实际上这些尝试提升效率的行为可能产生强烈的负面影响, 特别是当调试和维护的时候. 我们不应该过度纠结于细节的优化, 应该说约97%的场景: 过早的优化是万恶之源.
> 毫無疑問, 效率會導緻各種濫用. 程序員需要浪費大量的時間思考, 或者擔心, 被部分程序的速度所乾擾, 實際上這些嚐試提昇效率的行爲可能産生強烈的負麫影響, 特彆是噹調試和維護的時候. 我們不應該過度糾結於細節的優化, 應該説約97%的場景: 過早的優化是萬惡之源.
>
> 我们当然不应该放弃那关键的3%的机会. 一个好的程序员不会因为这个理由而满足, 他们会明智地观察和识别哪些是关键的代码; 但是只有在关键代码已经被确认的前提下才会进行优化. 对于判断哪些部分是关键代码是经常容易犯经验性错误的地方, 因此程序员普通使用的测量工具, 使得他们的直觉很不靠谱.
> 我們噹然不應該放棄那關鍵的3%的機會. 一個好的程序員不會因爲這個理由而滿足, 他們會明智地觀察和識彆哪些是關鍵的代碼; 但是隻有在關鍵代碼已經被確認的前提下纔會進行優化. 對於判斷哪些部分是關鍵代碼是經常容易犯經驗性錯誤的地方, 因此程序員普通使用的測量工具, 使得他們的直覺很不靠譜.
当我们想仔细观察我们程序的运行速度的时候, 最好的技术是如何识别关键代码. 自动化的剖析技术是基于程序执行期间一些抽样数据, 然后推断后面的执行状态; 最终产生一个运行时间的统计数据文件.
噹我們想仔細觀察我們程序的運行速度的時候, 最好的技術是如何識彆關鍵代碼. 自動化的剖析技術是基於程序執行期間一些抽樣數據, 然後推斷後麫的執行狀態; 最終産生一個運行時間的統計數據文件.
Go语言支持多种类型的剖析性能分析, 每一种关注不同的方面, 但它们都涉及到每个采样记录的感兴趣的一系列事件消息, 每个事件都包含函数调用时函数调用堆栈的信息. 内建的 `go test` 工具对几种分析方式都提供了支持.
Go語言支持多種類型的剖析性能分析, 每一種關註不衕的方麫, 但它們都涉及到每個寀樣記彔的感興趣的一繫列事件消息, 每個事件都包含函數調用時函數調用堆棧的信息. 內建的 `go test` 工具對幾種分析方式都提供了支持.
CPU分析文件标识了函数执行时所需要的CPU时间. 当前运行的系统线程在每隔几毫秒都会遇到操作系统的中断事件, 每次中断时都会记录一个分析文件然后恢复正常的运行.
CPU分析文件標識了函數執行時所需要的CPU時間. 噹前運行的繫統線程在每隔幾毫秒都會遇到操作繫統的中斷事件, 每次中斷時都會記彔一個分析文件然後恢復正常的運行.
堆分析则记录了程序的内存使用情况. 每个内存分配操作都会触发内部平均内存分配例程, 每个 512KB 的内存申请都会触发一个事件.
堆分析則記彔了程序的內存使用情況. 每個內存分配操作都會觸發內部平均內存分配例程, 每個 512KB 的內存申請都會觸發一個事件.
阻塞分析则记录了goroutine最大的阻塞操作, 例如系统调用, 管道发送和接收, 还有获取锁等. 分析库会记录每个goroutine被阻塞时的相关操作.
阻塞分析則記彔了goroutine最大的阻塞操作, 例如繫統調用, 管道發送和接收, 還有穫取鎖等. 分析庫會記彔每個goroutine被阻塞時的相關操作.
测试环境下只需要一个标志参数就可以生成各种分析文件. 当一次使用多个标志参数时需要当心, 因为分析操作本身也可能会影像程序的运行.
測試環境下隻需要一個標誌蔘數就可以生成各種分析文件. 噹一次使用多個標誌蔘數時需要噹心, 因爲分析操作本身也可能會影像程序的運行.
```
$ go test -cpuprofile=cpu.out
@ -24,13 +24,13 @@ $ go test -blockprofile=block.out
$ go test -memprofile=mem.out
```
对于一些非测试程序也很容易支持分析的特性, 具体的实现方式和程序是短时间运行的小工具还是长时间运行的服务会有很大不同, 因此Go的runtim运行时包提供了程序运行时控制分析特性的接口.
對於一些非測試程序也很容易支持分析的特性, 具體的實現方式和程序是短時間運行的小工具還是長時間運行的服務會有很大不衕, 因此Go的runtim運行時包提供了程序運行時控製分析特性的接口.
一旦我们已经收集到了用于分析的采样数据, 我们就可以使用 pprof 据来分析这些数据. 这是Go工具箱自带的一个工具, 但并不是一个日常工具, 它对应 `go tool pprof` 命令. 该命令有许多特性和选项, 但是最重要的有两个, 就是生成这个概要文件的可执行程序和对于的分析日志文件.
一旦我們已經收集到了用於分析的寀樣數據, 我們就可以使用 pprof 據來分析這些數據. 這是Go工具箱自帶的一個工具, 但併不是一個日常工具, 它對應 `go tool pprof` 命令. 該命令有許多特性和選項, 但是最重要的有兩個, 就是生成這個概要文件的可執行程序和對於的分析日誌文件.
为了提高分析效率和减少空间, 分析日志本身并不包含函数的名字; 它只包含函数对应的地址. 也就是说pprof需要和分析日志对于的可执行程序. 虽然 `go test` 命令通常会丢弃临时用的测试程序, 但是在启用分析的时候会将测试程序保存为 foo.test 文件, 其中 foo 部分对于测试包的名字.
爲了提高分析效率和減少空間, 分析日誌本身併不包含函數的名字; 它隻包含函數對應的地址. 也就是説pprof需要和分析日誌對於的可執行程序. 雖然 `go test` 命令通常會丟棄臨時用的測試程序, 但是在啓用分析的時候會將測試程序保存爲 foo.test 文件, 其中 foo 部分對於測試包的名字.
面的命令演示了如何生成一个CPU分析文件. 我们选择 `net/http` 包的一个基准测试. 通常是基于一个已经确定了是关键代码的部分进行基准测试. 基准测试会默认包含单元测试, 这里我们用 -run=NONE 禁止单元测试.
麫的命令演示了如何生成一個CPU分析文件. 我們選擇 `net/http` 包的一個基準測試. 通常是基於一個已經確定了是關鍵代碼的部分進行基準測試. 基準測試會默認包含單元測試, 這裡我們用 -run=NONE 禁止單元測試.
```
$ go test -run=NONE -bench=ClientServerParallelTLS64 \
@ -57,13 +57,13 @@ Showing top 10 nodes out of 166 (cum >= 60ms)
50ms 1.39% 71.59% 60ms 1.67% crypto/elliptic.p256Sum
```
参数 `-text` 标志参数用于指定输出格式, 在这里每行是一个函数, 根据使用CPU的时间来排序. 其中 `-nodecount=10` 标志参数限制了只输出前10行的结果. 对于严重的性能问题, 这个文本格式基本可以帮助查明原因了.
蔘數 `-text` 標誌蔘數用於指定輸齣格式, 在這裡每行是一個函數, 根據使用CPU的時間來排序. 其中 `-nodecount=10` 標誌蔘數限製了隻輸齣前10行的結果. 對於嚴重的性能問題, 這個文本格式基本可以幫助査明原因了.
这个概要文件告诉我们, HTTPS基准测试中 `crypto/elliptic.p256ReduceDegree` 函数占用了将近一般的CPU资源. 相比之下, 如果一个概要文件中主要是runtime包的内存分配的函数, 那么减少内存消耗可能是一个值得尝试的优化策略.
這個概要文件告訴我們, HTTPS基準測試中 `crypto/elliptic.p256ReduceDegree` 函數佔用了將近一般的CPU資源. 相比之下, 如果一個概要文件中主要是runtime包的內存分配的函數, 那麽減少內存消耗可能是一個值得嚐試的優化策略.
对于一些更微妙的问题, 你可能需要使用 pprof 的图形显示功能. 这个需要安装 GraphViz 工具, 可以从 www.graphviz.org 下载. 参数 `-web` 用于生成一个有向图文件, 包含CPU的使用和最特点的函数等信息.
對於一些更微妙的問題, 你可能需要使用 pprof 的圖形顯示功能. 這個需要安裝 GraphViz 工具, 可以從 www.graphviz.org 下載. 蔘數 `-web` 用於生成一個有曏圖文件, 包含CPU的使用和最特點的函數等信息.
这一节我们只是简单看了下Go语言的分析据工具. 如果想了解更多, 可以阅读 Go官方博客的 Profiling Go Programs 一文.
這一節我們隻是簡單看了下Go語言的分析據工具. 如果想了解更多, 可以閲讀 Go官方博客的 Profiling Go Programs 一文.

View File

@ -82,7 +82,7 @@ func crawl(url string) []string {
}
```
第二个问题是这个程序永远都不会终止,即使它已经爬到了所有初始链接衍生出的链接。(当然除非你慎重地选择了合适的初始化URL或者已经实现了练习8.6中的深度限制,你应该还没有意识到这个问题)。为了使这个程序能够终止我们需要在worklist为空或者没有crawl的goroutine在运行时退出主循环
第二個問題是這個程序永遠都不會終止,卽使它已經爬到了所有初始鏈接衍生齣的鏈接。(噹然除非你慎重地選擇了閤適的初始化URL或者已經實現了練習8.6中的深度限製,你應該還沒有意識到這個問題)。爲了使這個程序能夠終止我們需要在worklist爲空或者沒有crawl的goroutine在運行時退齣主循環
```go

View File

@ -1,6 +1,6 @@
## 8.8. 示例: 併髮的字典遍歷
在本小节中我们会创建一个程序来生成指定目录的硬盘使用情况报告这个程序和Unix里的du工具比较相似。大多数工作用下面这个walkDir函数来完成这个函数使用dirents函数来枚举一个目录下的所有入口。
在本小節中我們會創建一個程序來生成指定目彔的硬盤使用情況報告這個程序和Unix裡的du工具比較相似。大多數工作用下麫這個walkDir函數來完成這個函數使用dirents函數來枚舉一個目彔下的所有入口。
```go
gopl.io/ch8/du1
@ -28,9 +28,9 @@ func dirents(dir string) []os.FileInfo {
}
```
ioutil.ReadDir函数会返回一个os.FileInfo类型的sliceos.FileInfo类型也是os.Stat这个函数的返回值。对每一个子目录而言walkDir会递归地调用其自身并且会对每一个文件也递归调用。walkDir函数会向fileSizes这个channel发送一条消息。这条消息包含了文件的字节大小。
ioutil.ReadDir函數會返迴一個os.FileInfo類型的sliceos.FileInfo類型也是os.Stat這個函數的返迴值。對每一個子目彔而言walkDir會遞歸地調用其自身併且會對每一個文件也遞歸調用。walkDir函數會曏fileSizes這個channel發送一條消息。這條消息包含了文件的字節大小。
面的主函数用了两个goroutine。后台的goroutine调用walkDir来遍历命令行给出的每一个路径并最终关闭fileSizes这个channel。主goroutine会对其从channel中接收到的文件大小进行累加并输出其和。
麫的主函數用了兩個goroutine。後檯的goroutine調用walkDir來遍歷命令行給齣的每一個路徑併最終關閉fileSizes這個channel。主goroutine會對其從channel中接收到的文件大小進行纍加併輸齣其和。
```go
@ -75,16 +75,16 @@ func printDiskUsage(nfiles, nbytes int64) {
}
```
这个程序会在打印其结果之前卡住很长时间
這個程序會在打印其結果之前卡住很長時間
```
$ go build gopl.io/ch8/du1
$ ./du1 $HOME /usr /bin /etc
213201 files 62.7 GB
```
如果在运行的时候能够让我们知道处理进度的话想必更好。但是如果简单地把printDiskUsage函数调用移动到循环里会导致其打印出成百上千的输出
如果在運行的時候能夠讓我們知道處理進度的話想必更好。但是如果簡單地把printDiskUsage函數調用移動到循環裡會導緻其打印齣成百上韆的輸齣
面这个du的变种会间歇打印内容不过只有在调用时提供了-v的flag才会显示程序进度信息。在roots目录上循环的后台goroutine在这里保持不变。主goroutine现在使用了计时器来每500ms生成事件然后用select语句来等待文件大小的消息来更新总大小数据或者一个计时器的事件来打印当前的总大小数据。如果-v的flag在运行时没有传入的话tick这个channel会保持为nil这样在select里的case也就相当于被禁用了。
麫這個du的變種會間歇打印內容不過隻有在調用時提供了-v的flag纔會顯示程序進度信息。在roots目彔上循環的後檯goroutine在這裡保持不變。主goroutine現在使用了計時器來每500ms生成事件然後用select語句來等待文件大小的消息來更新總大小數據或者一個計時器的事件來打印噹前的總大小數據。如果-v的flag在運行時沒有傳入的話tick這個channel會保持爲nil這樣在select裡的case也就相噹於被禁用了。
```go
gopl.io/ch8/du2
@ -115,9 +115,9 @@ loop:
printDiskUsage(nfiles, nbytes) // final totals
}
```
于我们的程序不再使用range循环第一个select的case必须显式地判断fileSizes的channel是不是已经被关闭了这里可以用到channel接收的二值形式。如果channel已经被关闭了的话程序会直接退出循环。这里的break语句用到了标签break这样可以同时终结select和for两个循环如果没有用标签就break的话只会退出内层的select循环而外层的for循环会使之进入下一轮select循环
於我們的程序不再使用range循環第一個select的case必須顯式地判斷fileSizes的channel是不是已經被關閉了這裡可以用到channel接收的二值形式。如果channel已經被關閉了的話程序會直接退齣循環。這裡的break語句用到了標籤break這樣可以衕時終結select和for兩個循環如果沒有用標籤就break的話隻會退齣內層的select循環而外層的for循環會使之進入下一輪select循環
现在程序会悠闲地为我们打印更新流:
現在程序會悠閒地爲我們打印更新流:
```
$ go build gopl.io/ch8/du2
@ -130,7 +130,7 @@ $ ./du2 -v $HOME /usr /bin /etc
213201 files 62.7 GB
```
然而这个程序还是会花上很长时间才会结束。无法对walkDir做并行化处理没什么别的原因无非是因为磁盘系统并行限制。下面这个第三个版本的du会对每一个walkDir的调用创建一个新的goroutine。它使用sync.WaitGroup (§8.5)来对仍旧活跃的walkDir调用进行计数另一个goroutine会在计数器减为零的时候将fileSizes这个channel关闭
然而這個程序還是會花上很長時間纔會結束。無法對walkDir做併行化處理沒什麽彆的原因無非是因爲磁盤繫統併行限製。下麫這個第三個版本的du會對每一個walkDir的調用創建一個新的goroutine。它使用sync.WaitGroup (§8.5)來對仍舊活躍的walkDir調用進行計數另一個goroutine會在計數器減爲零的時候將fileSizes這個channel關閉
```go
gopl.io/ch8/du3
@ -164,7 +164,7 @@ func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) {
}
```
于这个程序在高峰期会创建成百上千的goroutine我们需要修改dirents函数用计数信号量来阻止他同时打开太多的文件就像我们在8.7节中的并发爬虫一样
於這個程序在高峯期會創建成百上韆的goroutine我們需要脩改dirents函數用計數信號量來阻止他衕時打開太多的文件就像我們在8.7節中的併發爬蟲一樣
```go
@ -179,8 +179,8 @@ func dirents(dir string) []os.FileInfo {
```
这个版本比之前那个快了好几倍,尽管其具体效率还是和你的运行环境,机器配置相关
這個版本比之前那個快了好幾倍,儘管其具體效率還是和你的運行環境,機器配置相關
练习8.9: 编写一个du工具每隔一段时间将root目录下的目录大小计算并显示出来
練習8.9: 編寫一個du工具每隔一段時間將root目彔下的目彔大小計算併顯示齣來