故事的起因
一次一個同事給我發了一段簡單的代碼,問我這段代碼有什麼問題?
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Open("/test.txt")
if err != nil {
fmt.Println(err)
}
fmt.Println(f.Name(), "opened successfully")
}
看到這段代碼後不加思索的回答,文件沒有close,他說錯,可能當時我們沒在一個頻道上,“err處理沒有return”。
又仔細的看了下代碼,發現err的處理代碼塊後使用了f.Name(),這個是存在問題的,因爲當open發生錯誤時,返回的文件句柄則爲nil,下文直接使用f.Name()。這種錯誤對於初學者經常會犯,改進的方式也很多,只要保證運行f.Name()的得到的f不爲nil即可。
可以在發生錯誤時,可以return或者os.Exit(-1) 也或下文的f.Name()放到else邏輯塊中。
具體的處理方式要根據對報錯的容忍度來處理
故事的發展
- 猜想
剛又提到,程序未對打開的文件做close,當然運行也沒問題。既然沒問題,也就沒有close的必要。但是在open後加defer close已經成爲go語言教課書級的示例。
猜想,這裏的open底層是一個I/O操作,在linux下所有的I/O操作都會轉化爲對文件的操作。如果程序對文件open後,沒有關閉,則會一直佔有資源,打開的數量越來越多,最終一定會因達到上限而導致程序出現問題。
-
猜想調查
通過谷歌找到lsof這一命令可以查看打開的文件描述符的上限。
通過改命令發現我電腦上可以支持程序最大打開的文件描述符是4864個 - 驗證
修改下代碼,看下當程序打開4865次會發生什麼情況?
package main
import (
"fmt"
"os"
)
func main() {
for i := 1; i <= 4865; i++ {
f, err := os.Open("./test.txt")
if err != nil {
fmt.Println(err)
}
fmt.Println(f.Name(), "opened successfully", i)
}
fmt.Scanln()
}
執行結果
發生了猜想中的問題,剛查看最大文件描述符是4864,這裏只打開了4861個,爲什麼少了三個?
- 再次猜想
這裏少了三個,那麼這三個應該是被系統佔用了,這裏存在兩種可能:
- 被其它程序佔用
- 被該程序佔用
- 再次驗證
先來確認第一點,被其它程序佔用
怎麼驗證呢?可以同樣的程序,一個循環數設置3000,一個設置2000,如果結論成立的話,那麼後運行的一個一定會出錯。
程序並沒有向想象中的那樣出錯
那麼就是該程序默認佔用了三個
通過lsof查下進程打開的描述符情況
發現程序會默認打開三個系統文件描述符
也就是標準輸入,標準輸出,錯誤輸出
這樣的解釋就可以自說其圓了,真的是這樣麼?
-
理論支撐
以下是維基百科對文件描述符的敘述
對文件的描述符的探索,可以畫上一個句號了。
遺留問題
在產看進程關聯的文件時,發現有多出以上四個,這些有什麼?這個問題作爲一個遺留問題拋在這裏,等待有心去探索
總結
通過以上的試驗和驗證,在程序打開文件後,記得close
完善後的最終處理代碼
package main
import (
"fmt"
"os"
)
func main() {
for i := 1; i <= 50000; i++ {
f, err := os.Open("./test.txt")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(f.Name(), "opened successfully", i)
f.Close()
}
fmt.Scanln()
}
喜歡請關注微信公衆號“雲端漫記" 持續爲你更新