同事寫的一段代,碼業務場景:需要多次GET請求一個三方服務的http 接口,獲取數據后寫入文件。發現有部分文件沒有寫入。查看日志出現了報錯“socket: too many open files”、“too many open files”。
在此記錄一下解決辦法。這也是新寫Go的人很常見的問題。
示例代碼:
package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
)
func main() {
requestAndWriteFile()
}
// 從接口獲取數據並且寫入文件
func requestAndWriteFile() {
// 約有5千個元素
params := []string{
"001",
"002",
"003",
"004",
}
hostname := "https://test.com/api"
for _, val := range params {
url := hostname + "?=code" + val
byteData, err := Get(url)
if err != nil{
fmt.Println(err)
continue
}
fileFullPath := "/var/" + val + ".txt"
path := filepath.Dir(fileFullPath)
_, err = os.Stat(path)
// 不存在則創建
if err != nil {
err = os.MkdirAll(path, 0755)
if err != nil {
fmt.Println("創建目錄錯誤", err)
continue
}
}
file, e := os.OpenFile(fileFullPath, os.O_RDWR|os.O_CREATE, 0766)
// 關閉文件
defer file.Close()
if e != nil {
fmt.Println("打開目錄錯誤", e)
}
_, er := file.Write(byteData)
if er != nil {
fmt.Println("寫入文件錯誤", er)
}
}
}
// 發送GET請求
func Get(url string) ([]byte, error) {
response, err := http.Get(url)
if err != nil {
return nil, err
}
// 關閉響應
defer response.Body.Close()
return ioutil.ReadAll(response.Body)
}
上面的代碼中使用了“defer response.Body.Close()” 關閉了http響應體,打開的文件也用“defer file.Close()”關閉了。乍一看似乎沒有問題。但是Go的HTTP請求本身是有坑的,釋放不及時,會造成同時有多個socket連接。第二個問題就是“defer file.Close()” 寫在for 循環中,那么按照defer的特性,將在函數requestAndWriteFile return之前執行多個defer,越先出現的defer越后執行。多次循環后打開的文件數就超過了系統限制,就會報錯“too many open files”。
解決辦法是:對於http請求導致“socket: too many open files”,采用公用的 http.Transport;對於“too many open files”,寫入文件的操作,封裝成函數,在函數中打開關閉文件,就可以避免。修改后的示例代碼:
package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
)
// 全局 transport
var globalTransport *http.Transport
func init() {
globalTransport = &http.Transport{}
}
func main() {
requestAndWriteFile()
}
func requestAndWriteFile() {
// 約有5千個元素
params := []string{
"001",
"002",
"003",
"004",
}
hostname := "https://test.com/api"
for _, val := range params {
url := hostname + "?=code" + val
byteData, err := Get(url)
if err != nil{
fmt.Println(err)
continue
}
fileFullPath := "/var/" + val + ".txt"
writeFile(fileFullPath, byteData)
}
}
func writeFile(fileFullPath string, byteData []byte) error {
path := filepath.Dir(fileFullPath)
_, err := os.Stat(path)
// 不存在則創建
if err != nil {
err = os.MkdirAll(path, 0755)
if err != nil {
fmt.Println("創建目錄錯誤")
return err
}
}
file, e := os.OpenFile(fileFullPath, os.O_RDWR|os.O_CREATE, 0766)
// 一定要close
defer file.Close()
if e != nil {
fmt.Println("打開目錄錯誤")
}
_, er := file.Write(byteData)
if er != nil {
fmt.Println("寫入文件錯誤")
return er
}
return nil
}
// 發送get請求
func Get(uri string) ([]byte, error) {
client := http.Client{
Transport: globalTransport,
}
res, err := client.Get(uri)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
return body, nil
}
如果確實有必要同時打開超過系統限制的多個文件,那么可以使用ulimit 命令修改。