本章我們將帶領大家深入了解一下 Go語言中的文件處理,重點在于文件而非目錄或者通用的文件系統,特別是如何讀寫標準格式(如 XML 和 JSON 格式)的文件以及自定義的純文本和二進制格式文件。 由于前面的內容已覆蓋 Go語言的所有特性,現在我們可以靈活地使用 Go語言提供的所有工具。我們會充分利用這種靈活性并利用閉包來避免重復性的代碼,同時在某些情況下充分利用 Go語言對面向對象的支持,特別是對為函數添加方法的支持。
本章內容:
11.1?Go語言自定義數據文件
對一個程序非常普遍的需求包括維護內部數據結構,為數據交換提供導入導出功能,也支持使用外部工具來處理數據。 由于我們這里的關注重點是文件處理,因此我們純粹只關心如何從程序內部數據結構中讀取數據并將其寫入標準和自定義格式的文件中,以及如何從標準和自定義格式文件中讀取數據并寫入程序的內部數據結構中。 本節中,我們會為所有的例子使用相同的數據,以便直接比較不同的文件格式。所有的代碼都來自 invoicedate 程序(在 invoicedata 目錄中的 invoicedata.go > gob.go、inv.go、jsn.go、txt.go 和 xml.go 等文件中)。大家可以從我的網盤(鏈接:?https://pan.baidu.com/s/1j22QfIScihrauVCVFV6MWw?提取碼: ajrk)下載相關的代碼。 該程序接受兩個文件名作為命令行參數,一個用于讀,另一個用于寫(它們必須是不同的文件)。程序從第一個文件中讀取數據(以其后綴所表示的任何格式),并將數據寫入第二個文件(也是以其后綴所表示的任何格式)。 由 invoicedata 程序創建的文件可跨平臺使用,也就是說,無論是什么格式,Windows 上創建的文件都可在 Mac OS X 以及 Linux 上讀取,反之亦然。Gzip 格式壓縮的文件(如 invoices.gob.gz)可以無縫讀寫。 這些數據由一個 []invoice 組成,也就是說,是一個保存了指向 Invoice 值的指針的切片。每一個發票數據都保存在一個 invoice 類型的值中,同時每一個發票數據都以 []*Item 的形式保存著 0 個或者多個項。
type Invoice struct { Id int Customerld int Raised time.Time Due time.Time Paid bool Note string Items []*Item } ? type Item struct { Id st ring Price float64 Quantity int Note string }
這兩個結構體用于保存數據。下表給出了一些非正式的對比,展示了每種格式下讀寫相同的 50000 份隨機發票數據所需的時間,以及以該格式所存儲文件的大小。 計時按秒計,并向上舍入到最近的十分之一秒。我們應該把計時結果認為是無絕對單位的,因為不同硬件以及不 同負載情況下該值都不盡相同。大小一欄以千字節(KB)算,該值在所有機器上應該都是相同的。 對于該數據集,雖然未壓縮文件的大小千差萬別,但壓縮文件的大小都驚人的相似。而代碼的 函數不包括所有格式通用的代碼(例如,那些用于壓縮和解壓縮以及定義結構體的代碼)。 ?
表:各種格式的速度以及大小對比
后綴讀取寫入大小(KiB)讀/寫LOC格式 .gob 0.3 0.2 7948 21 + 11 =32 Go二進制 .gob.gz 0.5 1.5 2589 jsn 4.5 2.2 16283 32+17 = 49 JSON .jsn.gz 4.5 3.4 2678 .xml 6.7 1.2 18917 45 + 30 = 75 XML .xml.gz 6.9 2.7 2730 ..txt 1.9 1.0 12375 86 + 53 = 139 純文本(UTF-8) .txt.gz 2.2 2.2 2514 .inv 1.7 3.5 7250 128 + 87 = 215 自定義二進制 .inv.gz 1.6 2.6 2400
這些讀寫時間和文件大小在我們的合理預期范圍內,除了純文本格式的讀寫異常快之外。這得益于 fmt 包優秀的打印和掃描函數,以及我們設計的易于解析的自定義文本格式。 對于 JSON 和 XML 格式,我們只簡單地存儲了日期部分而非存儲默認的 time.Time 值(一個 ISO-8601 日期/時間字符串),通過犧牲一些速度和增加一些額外代碼稍微減小了文件的大小。 例如,如果讓JSON代碼自己來處理time.Time值,它能夠運行得更快,并且其代碼行數與 Go語言二進制編碼差不多。 對于二進制數據,Go語言的二進制格式是最便于使用的。它非常快且極端緊湊,所需的代碼非常少,并且相對容易適應數據的變化。然而,如果我們使用的自定義類型不原生支持 gob 編碼,我們必須讓該類型滿足 gob.Encoder 和 gob. Decoder 接口,這樣會導致 gob 格式的 讀寫相當得慢,并且文件大小也會膨脹。 對于可讀的數據,XML 可能是最好使用的格式,特別是作為一種數據交換格式時非常有用。與處理 JSON 格式相比,處理 XML 格式需要更多行代碼。這是因為 Go [沒有一個 xml.Marshaler 接口,也因為我們這里使用了并行的數據類型 (XMLInvoice 和 XMLItem)來幫助映射 XML 數據和發票數據(invoice 和 Item)。 使用 XML 作為外部存儲格式的應用程序可能不需要并行的數據類型或者也不需要 invoicedata 程序這樣的 轉換,因此就有可能比 invoicedata 例子中所給出的更快,并且所需的代碼也更少。 除了讀寫速度和文件大小以及代碼行數之外,還有另一個問題值得考慮:格式的穩健性。例如,如果我們為 Invoice 結構體和 Item 結構體添加了一個字段,那么就必須再改變文件的格式。我們的代碼適應讀寫新格式并繼續支持讀舊格式的難易程度如何?如果我們為文件格式定義版本,這樣的變化就很容易被適應(會以本章一個練習的形式給岀),除了讓 JSON 格式同時適應讀寫新舊格式稍微復雜一點之外。 除了 Invoice 和 Item 結構體之外,所有文件格式都共享以下常量:
const ( fileType = "INVOICES" //用于純文本格式 magicNumber = 0xl25D // 用于二進制格式 fileVersion = 100 //用于所有的格式 dataFormat = "2006-01-02" //必須總是使用該日期 )
magicNumber 用于唯一標記發票文件。fileVersion 用于標記發票文件的版本,該標記便于之后修改程序來適應數據格式的改變。dataFormat 稍后介紹,它表 示我們希望數據如何按照可讀的格式進行格式化。 同時,我們也創建了一對接口。
type InvoiceMarshaler interface { Marshallnvoices(writer io.Writer, invoices []*Invoice) error } type InvoiceUnmarshaler interface { Unmarshallnvoices(reader io.Reader) ([]*Invoice, error) }
這樣做的目的是以統一的方式針對特定格式使用 reader 和 writer。例如,下列函數是 invoicedata 程序用來從一個打開的文件中讀取發票數據的。
func readinvoices(reader io.Reader, suffix string)([]*Invoice, error) { var unmarshaler InvoicesUnmarshaler switch suffix { case ".gobn: unmarshaler = GobMarshaler{} case H.inv": unmarshaler = InvMarshaler{} case ,f. jsn", H. jsonn: unmarshaler = JSONMarshaler{} case ".txt”: unmarshaler = TxtMarshaler{} case ".xml": unmarshaler = XMLMarshaler{} } if unmarshaler != nil { return unmarshaler.Unmarshallnvoices(reader) } return nil, fmt.Errorf("unrecognized input suffix: %s", suffix) }
其中,reader 是任何能夠滿足 io.Reader 接口的值,例如,一個打開的文件 ( 其類型為 *os . File)> 一個 gzip 解碼器 ( 其類型為 *gzip. Reader) 或者一個 string. Readero 字符串 suffix 是文件的后綴名 ( 從 .gz 文件中解壓之后)。 在接下來的小節中我們將會看到 GobMarshaler 和 InvMarshaler 等自定義的類型,它們提供了 MarshmlTnvoices() 和 Unmarshallnvoices() 方法 (因此滿足 InvoicesMarshaler 和 InvoicesUnmarshaler 接口)。
11.2?Go語言JSON文件的讀寫操作
JSON(JavaScript?Object Notation)是一種輕量級的數據交換格式,易于閱讀和編寫,同時也易于機器解析和生成。它基于?JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1999 的一個子集。 JSON 是一種使用 UTF-8 編碼的純文本格式,采用完全獨立于語言的文本格式,由于寫起來比 XML 格式方便,并且更為緊湊,同時所需的處理時間也更少,致使 JSON 格式越來越流行,特別是在通過網絡連接傳送數據方面。 開發人員可以使用 JSON 傳輸簡單的字符串、數字、布爾值,也可以傳輸一個數組或者一個更復雜的復合結構。在 Web 開發領域中,JSON 被廣泛應用于 Web 服務端程序和客戶端之間的數據通信。 Go語言內建對 JSON 的支持,使用內置的 encoding/json 標準庫,開發人員可以輕松使用Go程序生成和解析 JSON 格式的數據。 JSON 結構如下所示:
{"key1":"value1","key2":value2,"key3":["value3","value4","value5"]}
寫 JSON 文件
使用Go語言創建一個 json 文件非常方便,示例代碼如下:
package main ? import ( ??? "encoding/json" ??? "fmt" ??? "os" ) ? type Website struct { ??? Name?? string `xml:"name,attr"` ??? Url??? string ??? Course []string } ? func main() { ??? info := []Website{{"Golang", "http://c.biancheng.net/golang/", []string{"http://c.biancheng.net/cplus/", "http://c.biancheng.net/linux_tutorial/"}}, {"Java", "http://c.biancheng.net/java/", []string{"http://c.biancheng.net/socket/", "http://c.biancheng.net/python/"}}} ? ??? // 創建文件 ??? filePtr, err := os.Create("info.json") ??? if err != nil { ??????? fmt.Println("文件創建失敗", err.Error()) ??????? return ??? } ??? defer filePtr.Close() ? ??? // 創建Json編碼器 ??? encoder := json.NewEncoder(filePtr) ? ??? err = encoder.Encode(info) ??? if err != nil { ??????? fmt.Println("編碼錯誤", err.Error()) ? ??? } else { ??????? fmt.Println("編碼成功") ??? } }
運行上面的代碼會在當前目錄下生成一個 info.json 文件,文件內容如下:
[ ??? { ??????? "Name":"Golang", ??????? "Url":"http://c.biancheng.net/golang/", ??????? "Course":[ ??????????? "http://c.biancheng.net/golang/102/", ??????????? "http://c.biancheng.net/golang/concurrent/" ??????? ] ??? }, ??? { ??????? "Name":"Java", ??????? "Url":"http://c.biancheng.net/java/", ??????? "Course":[ ??????????? "http://c.biancheng.net/java/10/", ??????????? "http://c.biancheng.net/python/" ??????? ] ??? } ]
讀 JSON 文件
讀 JSON 數據與寫 JSON 數據一樣簡單,示例代碼如下:
package main ? import ( ??? "encoding/json" ??? "fmt" ??? "os" ) ? type Website struct { ??? Name?? string `xml:"name,attr"` ??? Url??? string ??? Course []string } ? func main() { ??? filePtr, err := os.Open("./info.json") ??? if err != nil { ??????? fmt.Println("文件打開失敗 [Err:%s]", err.Error()) ??????? return ??? } ??? defer filePtr.Close() ??? var info []Website ??? // 創建json解碼器 ??? decoder := json.NewDecoder(filePtr) ??? err = decoder.Decode(&info) ??? if err != nil { ??????? fmt.Println("解碼失敗", err.Error()) ??? } else { ??????? fmt.Println("解碼成功") ??????? fmt.Println(info) ??? } }
運行結果如下:
go run main.go 解碼成功 [{Golang http://c.biancheng.net/golang/ [http://c.biancheng.net/golang/102/ http://c.biancheng.net/golang/concurrent/]} {Java http://c.biancheng.net/java/ [http://c.biancheng.net/java/10/ http://c.biancheng.net/python/]}]
順便提一下,還有一種叫做 BSON (Binary JSON) 的格式與 JSON 非常類似,與 JSON 相比,BSON 著眼于提高存儲和掃描效率。BSON 文檔中的大型元素以長度字段為前綴以便于掃描。在某些情況下,由于長度前綴和顯式數組索引的存在,BSON 使用的空間會多于 JSON。
11.3?Go語言XML文件的讀寫操作
XML(extensible Markup Language)格式被廣泛用作一種數據交換格式,并且自成一種文件格式。與上一節介紹的?JSON?相比 XML 要復雜得多,而且手動寫起來相對乏味得多。 在 JSON 還未像現在這么廣泛使用時,XML 的使用相當廣泛。XML 作為一種數據交換和信息傳遞的格式,使用還是很廣泛的,現在很多開放平臺接口,基本都會支持 XML 格式。 Go語言內置的 encoding/xml 包可以用在結構體和 XML 格式之間進行編解碼,其方式跟 encoding/json 包類似。然而與 JSON 相比 XML 的編碼和解碼在功能上更苛刻得多,這是由于 encoding/xml 包要求結構體的字段包含格式合理的標簽,而 JSON 格式卻不需要。
寫 XML 文件
使用 encoidng/xml 包可以很方便的將 xml 數據存儲到文件中,示例代碼如下:
package main ? import ( ??? "encoding/xml" ??? "fmt" ??? "os" ) ? type Website struct { ??? Name?? string `xml:"name,attr"` ??? Url??? string ??? Course []string } ? func main() { ??? //實例化對象 ??? info := Website{"C語言中文網", "http://c.biancheng.net/golang/", []string{"Go語言入門教程", "Golang入門教程"}} ??? f, err := os.Create("./info.xml") ??? if err != nil { ??????? fmt.Println("文件創建失敗", err.Error()) ??????? return ??? } ??? defer f.Close() ??? //序列化到文件中 ??? encoder := xml.NewEncoder(f) ??? err = encoder.Encode(info) ??? if err != nil { ??????? fmt.Println("編碼錯誤:", err.Error()) ??????? return ??? } else { ??????? fmt.Println("編碼成功") ??? } }
運行上面的代碼會在當前目錄生成一個 info.xml 文件,文件的內容如下所示:
<Website name="C語言中文網"> ??? <Url>http://c.biancheng.net/golang/</Url> ??? <Course>Go語言入門教程</Course> ??? <Course>Golang入門教程</Course> </Website>
讀 XML 文件
讀 XML 文件比寫 XML 文件稍微復雜,特別是在必須處理一些我們自定義字段的時候(例如日期)。但是,如果我們使用合理的打上 XML 標簽的結構體,就不會復雜。示例代碼如下:
package main ? import ( ??? "encoding/xml" ??? "fmt" ??? "os" ) ? type Website struct { ??? Name?? string `xml:"name,attr"` ??? Url??? string ??? Course []string } ? func main() { ??? //打開xml文件 ??? file, err := os.Open("./info.xml") ??? if err != nil { ??????? fmt.Printf("文件打開失敗:%v", err) ??????? return ??? } ??? defer file.Close() ? ??? info := Website{} ??? //創建 xml 解碼器 ??? decoder := xml.NewDecoder(file) ??? err = decoder.Decode(&info) ??? if err != nil { ??????? fmt.Printf("解碼失敗:%v", err) ??????? return ??? } else { ??????? fmt.Println("解碼成功") ??????? fmt.Println(info) ??? } }
運行結果如下:
go run main.go 解碼成功 {C語言中文網 http://c.biancheng.net/golang/ [Go語言入門教程 Golang入門教程]}
正如寫 XML 時一樣,我們無需關心對所讀取的 XML 數據進行轉義,xml.NewDecoder.Decode() 函數會自動處理這些。 xml 包還支持更為復雜的標簽,包括嵌套。例如標簽名為 'xml:"Books>Author"' 產生的是 <Books><Author>content</Author></Books> 這樣的 XML 內容。同時除了 'xml:", attr"' 之外,該包還支持 'xml:",chardata"' 這樣的標簽表示將該字段當做字符數據來寫,支持 'xml:",innerxml"' 這樣的標簽表示按照字面量來寫該字段,以及 'xml:",comment"' 這樣的標簽表示將該字段當做 XML 注釋。因此,通過使用標簽化的結構體,我們可以充分利用好這些方便的編碼解碼函數,同時合理控制如何讀寫 XML 數據。
11.4?Go語言使用Gob傳輸數據
為了讓某個數據結構能夠在網絡上傳輸或能夠保存至文件,它必須被編碼然后再解碼。當然已經有許多可用的編碼方式了,比如?JSON、XML、Google 的 protocol buffers 等等。而現在又多了一種,由Go語言 encoding/gob 包提供的方式。 Gob 是Go語言自己以二進制形式序列化和反序列化程序數據的格式,可以在 encoding 包中找到。這種格式的數據簡稱為 Gob(即 Go binary 的縮寫)。類似于?Python?的“pickle”和?Java的“Serialization”。 Gob 和 JSON 的 pack 之類的方法一樣,由發送端使用 Encoder 對數據結構進行編碼。在接收端收到消息之后,接收端使用 Decoder 將序列化的數據變化成本地變量。 Go語言可以通過 JSON 或 Gob 來序列化 struct 對象,雖然 JSON 的序列化更為通用,但利用 Gob 編碼可以實現 JSON 所不能支持的 struct 的方法序列化,利用 Gob 包序列化 struct 保存到本地也十分簡單。 Gob 不是可外部定義、語言無關的編碼方式,它的首選的是二進制格式,而不是像 JSON 或 XML 那樣的文本格式。Gob 并不是一種不同于 Go 的語言,而是在編碼和解碼過程中用到了 Go 的反射。 Gob 通常用于遠程方法調用參數和結果的傳輸,以及應用程序和機器之間的數據傳輸。它和 JSON 或 XML 有什么不同呢?Gob 特定的用于純 Go 的環境中,例如兩個用Go語言寫的服務之間的通信。這樣的話服務可以被實現得更加高效和優化。 Gob 文件或流是完全自描述的,它里面包含的所有類型都有一個對應的描述,并且都是可以用Go語言解碼,而不需要了解文件的內容。 只有可導出的字段會被編碼,零值會被忽略。在解碼結構體的時候,只有同時匹配名稱和可兼容類型的字段才會被解碼。當源數據類型增加新字段后,Gob 解碼客戶端仍然可以以這種方式正常工作。解碼客戶端會繼續識別以前存在的字段,并且還提供了很大的靈活性,比如在發送者看來,整數被編碼成沒有固定長度的可變長度,而忽略具體的 Go 類型。 假如有下面這樣一個結構體 T:
type T struct { X, Y, Z int } var t = T{X: 7, Y: 0, Z: 8}
而在接收時可以用一個結構體 U 類型的變量 u 來接收這個值:
type U struct { X, Y *int8 } var u U
在接收時,X 的值是 7,Y 的值是 0(Y 的值并沒有從 t 中傳遞過來,因為它是零值)和 JSON 的使用方式一樣,Gob 使用通用的 io.Writer 接口,通過 NewEncoder() 函數創建 Encoder 對象并調用 Encode(),相反的過程使用通用的 io.Reader 接口,通過 NewDecoder() 函數創建 Decoder 對象并調用 Decode 。
創建 gob 文件
下面通過簡單的示例程序來演示Go語言是如何創建 gob 文件的,代碼如下所示:
package main ? import ( "encoding/gob" "fmt" "os" ) ? func main() { info := map[string]string{ "name": "C語言中文網", "website": "http://c.biancheng.net/golang/", } name := "demo.gob" File, _ := os.OpenFile(name, os.O_RDWR|os.O_CREATE, 0777) defer File.Close() enc := gob.NewEncoder(File) if err := enc.Encode(info); err != nil { fmt.Println(err) } }
運行上面的代碼會在當前目錄下生成 demo.gob 文件,文件的內容如下所示:
0eff 8104 0102 ff82 0001 0c01 0c00 0041 ff82 0002 046e 616d 6510 43e8 afad e8a8 80e4 b8ad e696 87e7 bd91 0777 6562 7369 7465 1e68 7474 703a 2f2f 632e 6269 616e ... ...
讀取 gob 文件
讀取 gob 文件與創建 gob 文件同樣簡單,示例代碼如下:
package main ? import ( "encoding/gob" "fmt" "os" ) ? func main() { var M map[string]string File, _ := os.Open("demo.gob") D := gob.NewDecoder(File) D.Decode(&M) fmt.Println(M) }
運行結果如下:
go run main.go map[name:C語言中文網 website:http://c.biancheng.net/golang/]
11.5?Go語言純文本文件的讀寫操作
Go語言提供了很多文件操作的支持,在不同場景下,有對應的處理方式,本節我們來介紹一下文本文件的讀寫操作。
寫純文本文件
由于Go語言的 fmt 包中打印函數強大而靈活,寫純文本數據非常簡單直接,示例代碼如下所示:
package main ? import ( "bufio" "fmt" "os" ) ? func main() { //創建一個新文件,寫入內容 filePath := "./output.txt" file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { fmt.Printf("打開文件錯誤= %v \n", err) return } //及時關閉 defer file.Close() //寫入內容 str := "http://c.biancheng.net/golang/\n" // \n\r表示換行 txt文件要看到換行效果要用 \r\n //寫入時,使用帶緩存的 *Writer writer := bufio.NewWriter(file) for i := 0; i < 3; i++ { writer.WriteString(str) } //因為 writer 是帶緩存的,因此在調用 WriterString 方法時,內容是先寫入緩存的 //所以要調用 flush方法,將緩存的數據真正寫入到文件中。 writer.Flush() }
運行上面代碼會在當前目錄下生成一個 output.txt 文件,文件內容如下:
http://c.biancheng.net/golang/ http://c.biancheng.net/golang/ http://c.biancheng.net/golang/
讀純文本文件
打開并讀取一個純文本格式的數據跟寫入純文本格式數據一樣簡單。要解析文本來重建原始數據可能稍微復雜,這需根據格式的復雜性而定。 示例代碼如下所示:
package main ? import ( "bufio" "fmt" "io" "os" ) ? func main() { //打開文件 file, err := os.Open("./output.txt") if err != nil { fmt.Println("文件打開失敗 = ", err) } //及時關閉 file 句柄,否則會有內存泄漏 defer file.Close() //創建一個 *Reader , 是帶緩沖的 reader := bufio.NewReader(file) for { str, err := reader.ReadString('\n') //讀到一個換行就結束 if err == io.EOF { //io.EOF 表示文件的末尾 break } fmt.Print(str) } fmt.Println("文件讀取結束...") }
運行結果如下:
go run main.go http://c.biancheng.net/golang/ http://c.biancheng.net/golang/ http://c.biancheng.net/golang/ 文件讀取結束...
11.6?Go語言二進制文件的讀寫操作
Go語言的二進制(gob)格式是一個自描述的二進制序列。從其內部表示來看,Go語言的二進制格式由一個 0 塊或者更多塊的序列組成,其中的每一塊都包含一個字節數,一個由 0 個或者多個 typeId-typeSpecification 對組成的序列,以及一個 typeId-value 對。 如果 typeId-value 對的 typeId 是預先定義好的(例如 bool、int 和 string 等),則這些 typeId-typeSpecification 對可以省略。否則就用類型對來描述一個自定義類型(如一個自定義的結構體)。類型對和值對之間的 typeId 沒有區別。 正如我們將看到的,我們無需了解其內部結構就可以使用 gob 格式, 因為 encoding/gob 包會在幕后為我們打理好一切底層細節。 Go語言中的 encoding/gob 包也提供了與 encoding/json 包一樣的編碼解碼功能,并且容易使用。通常而言如果對肉眼可讀性不做要求,gob 格式是Go語言上用于文件存儲和網絡傳輸最為方便的格式。
寫Go語言二進制文件
下面通過一個簡單的示例來演示一下Go語言是如何生成一個二進制文件的,代碼如下所示:
package main ? import ( "encoding/gob" "fmt" "os" ) ? func main() { info := "http://c.biancheng.net/golang/" file, err := os.Create("./output.gob") if err != nil { fmt.Println("文件創建失敗", err.Error()) return } defer file.Close() ? encoder := gob.NewEncoder(file) err = encoder.Encode(info) if err != nil { fmt.Println("編碼錯誤", err.Error()) return } else { fmt.Println("編碼成功") } }
運行上面的代碼會在當前目錄下生成一個 output.gob 文件,文件內容如下所示:
210c 001e 6874 7470 3a2f 2f63 2e62 6961 6e63 6865 6e67 2e6e 6574 2f67 6f6c 616e 672f?
讀Go語言二進制文件
讀 gob 數據和寫一樣簡單,示例代碼如下:
package main ? import ( "encoding/gob" "fmt" "os" ) ? func main() { file, err := os.Open("./output.gob") if err != nil { fmt.Println("文件打開失敗", err.Error()) return } defer file.Close() ? decoder := gob.NewDecoder(file) info := "" err = decoder.Decode(&info) if err != nil { fmt.Println("解碼失敗", err.Error()) } else { fmt.Println("解碼成功") fmt.Println(info) } }
運行結果如下:
go run main.go 解碼成功 http://c.biancheng.net/golang/
11.7?Go語言自定義二進制文件的讀寫操作
雖然Go語言的 encoding/gob 包非常易用,而且使用時所需代碼量也非常少,但是我們仍有可能需要創建自定義的二進制格式。自定義的二進制格式有可能做到最緊湊的數據表示,并且讀寫速度可以非常快。 不過,在實際使用中,我們發現以Go語言二進制格式的讀寫通常比自定義格式要快非常多,而且創建的文件也不會大很多。但如果我們必須通過滿足 gob.GobEncoder 和 gob.GobDecoder 接口來處理一些不可被 gob 編碼的數據,這些優勢就有可能會失去。 在有些情況下我們可能需要與一些使用自定義二進制格式的軟件交互,因此了解如何處理二進制文件就非常有用。
寫自定義二進制文件
Go語言的 encoding/binary 包中的 binary.Write() 函數使得以二進制格式寫數據非常簡單,函數原型如下:
func Write(w io.Writer, order ByteOrder, data interface{}) error
Write 函數可以將參數 data 的 binary 編碼格式寫入參數 w 中,參數 data 必須是定長值、定長值的切片、定長值的指針。參數 order 指定寫入數據的字節序,寫入結構體時,名字中有_的字段會置為 0。 下面通過一個簡單的示例程序來演示一下 Write 函數的使用,示例代碼如下:
package main ? import ( "bytes" "encoding/binary" "fmt" "os" ) ? type Website struct { Url int32 } ? func main() { file, err := os.Create("output.bin") for i := 1; i <= 10; i++ { info := Website{ int32(i), } if err != nil { fmt.Println("文件創建失敗 ", err.Error()) return } defer file.Close() ? var bin_buf bytes.Buffer binary.Write(&bin_buf, binary.LittleEndian, info) b := bin_buf.Bytes() _, err = file.Write(b) ? if err != nil { fmt.Println("編碼失敗", err.Error()) return } } fmt.Println("編碼成功") }
運行上面的程序會在當前目錄下生成 output.bin 文件,文件內容如下:
0100 0000 0200 0000 0300 0000 0400 0000 0500 0000 0600 0000 0700 0000 0800 0000 0900 0000 0a00 0000?
讀自定義二進制文件
讀取自定義的二進制數據與寫自定義二進制數據一樣簡單。我們無需解析這類數據,只需使用與寫數據時相同的字節順序將數據讀進相同類型的值中。 示例代碼如下:
package main ? import ( "bytes" "encoding/binary" "fmt" "os" ) ? type Website struct { Url int32 } ? func main() { file, err := os.Open("output.bin") defer file.Close() if err != nil { fmt.Println("文件打開失敗", err.Error()) return } m := Website{} for i := 1; i <= 10; i++ { data := readNextBytes(file, 4) buffer := bytes.NewBuffer(data) err = binary.Read(buffer, binary.LittleEndian, &m) if err != nil { fmt.Println("二進制文件讀取失敗", err) return } fmt.Println("第", i, "個值為:", m) } } ? func readNextBytes(file *os.File, number int) []byte { bytes := make([]byte, number) ? _, err := file.Read(bytes) if err != nil { fmt.Println("解碼失敗", err) } ? return bytes }
運行結果如下:
go run main.go 第 1 個值為: {1} 第 2 個值為: {2} 第 3 個值為: {3} 第 4 個值為: {4} 第 5 個值為: {5} 第 6 個值為: {6} 第 7 個值為: {7} 第 8 個值為: {8} 第 9 個值為: {9} 第 10 個值為: {10}
至此,我們完成了對自定義二進制數據的讀和寫操作。只要小心選擇表示長度的整數符號和大小,并將該長度值寫在變長值(如切片)的內容之前,那么使用二進制數據進行工作并不難。 Go語言對二進制文件的支持還包括隨機訪問。這種情況下,我們必須使用 os.OpenFile() 函數來打開文件(而非 os.Open()),并給它傳入合理的權限標志和模式(例如 os.O_RDWR 表示可讀寫)參數。 然后,就可以使用 os.File.Seek() 方法來在文件中定位并讀寫,或者使用 os.File.ReadAt() 和 os.File.WriteAt() 方法來從特定的字節偏移中讀取或者寫入數據。 Go語言還提供了其他常用的方法,包括 os.File.Stat() 方法,它返回的 os.FileInfo 包含了文件大小、權限以及日期時間等細節信息。
11.8?Go語言zip歸檔文件的讀寫操作
Go語言的標準庫提供了對幾種壓縮格式的支持,其中包括 gzip,因此 Go 程序可以無縫地讀寫 .gz 擴展名的 gzip 壓縮文件或非 .gz 擴展名的非壓縮文件。此外標準庫也提供了讀和寫 .zip 文件、tar 包文件(.tar 和 .tar.gz),以及讀 .bz2 文件(即 .tar .bz2 文件)的功能。 本節我們將主要介紹 zip 歸檔文件的讀寫操作。
創建 zip 歸檔文件
Go語言提供了?archive/zip 包來操作壓縮文件,下面通過一個簡單的的示例演示如何使用Go語言來創建一個 zip 文件,示例代碼如下:
package main ? import ( "archive/zip" "bytes" "fmt" "os" ) ? func main() { // 創建一個緩沖區用來保存壓縮文件內容 buf := new(bytes.Buffer) ? // 創建一個壓縮文檔 w := zip.NewWriter(buf) ? // 將文件加入壓縮文檔 var files = []struct { Name, Body string }{ {"Golang.txt", "http://c.biancheng.net/golang/"}, } for _, file := range files { f, err := w.Create(file.Name) if err != nil { fmt.Println(err) } _, err = f.Write([]byte(file.Body)) if err != nil { fmt.Println(err) } } ? // 關閉壓縮文檔 err := w.Close() if err != nil { fmt.Println(err) } ? // 將壓縮文檔內容寫入文件 f, err := os.OpenFile("file.zip", os.O_CREATE|os.O_WRONLY, 0666) if err != nil { fmt.Println(err) } buf.WriteTo(f) }
運行上面的文件會在當前目錄下生成 file.zip 文件,如下圖所示: ?
圖:生成的壓縮文件及文件的內容
讀取 zip 歸檔文件
讀取一個 .zip 歸檔文件與創建一個歸檔文件一樣簡單,只是如果歸檔文件中包含帶有路徑的文件名,就必須重建目錄結構。 示例代碼如下所示:
package main ? import ( "archive/zip" "fmt" "io" "os" ) ? func main() { // 打開一個zip格式文件 r, err := zip.OpenReader("file.zip") if err != nil { fmt.Printf(err.Error()) } defer r.Close() ? // 迭代壓縮文件中的文件,打印出文件中的內容 for _, f := range r.File { fmt.Printf("文件名: %s\n", f.Name) rc, err := f.Open() if err != nil { fmt.Printf(err.Error()) } _, err = io.CopyN(os.Stdout, rc, int64(f.UncompressedSize64)) if err != nil { fmt.Printf(err.Error()) } rc.Close() } }
運行結果如下:
go run main.go 文件名: Golang.txt http://c.biancheng.net/golang/
11.9?Go語言tar歸檔文件的讀寫操作
在上一節《創建 .zip 歸檔文件》中我們介紹了 zip 歸檔文件的創建和讀取,那么接下來介紹一下 tar 歸檔文件的創建及讀取。
創建 tar 歸檔文件
tar 是一種打包格式,但不對文件進行壓縮,所以打包后的文檔一般遠遠大于 zip 和 tar.gz,因為不需要壓縮的原因,所以打包的速度是非常快的,打包時 CPU 占用率也很低。 tar 的目的在于方便文件的管理,比如在我們的生活中,有很多小物品分散在房間的各個角落,為了方便整潔可以將這些零散的物品整理進一個箱子中,而 tar 的功能就類似這樣。 創建 tar 歸檔文件與創建 .zip 歸檔文件非常類似,主要不同點在于我們將所有數據都寫入相同的 writer 中,并且在寫入文件的數據之前必須寫入完整的頭部,而非僅僅是一個文件名。 tar 打包實現原理如下:
創建一個文件 x.tar,然后向 x.tar 寫入 tar 頭部信息; 打開要被 tar 的文件,向 x.tar 寫入頭部信息,然后向 x.tar 寫入文件信息; 當有多個文件需要被 tar 時,重復第二步直到所有文件都被寫入到 x.tar 中; 關閉 x.tar,完成打包。
下面通過示例程序簡單演示一下Go語言 tar 打包的實現:
package main ? import ( "archive/tar" "fmt" "io" "os" ) ? func main() { f, err := os.Create("./output.tar") //創建一個 tar 文件 if err != nil { fmt.Println(err) return } defer f.Close() ? tw := tar.NewWriter(f) defer tw.Close() ? fileinfo, err := os.Stat("./main.exe") //獲取文件相關信息 if err != nil { fmt.Println(err) } hdr, err := tar.FileInfoHeader(fileinfo, "") if err != nil { fmt.Println(err) } ? err = tw.WriteHeader(hdr) //寫入頭文件信息 if err != nil { fmt.Println(err) } ? f1, err := os.Open("./main.exe") if err != nil { fmt.Println(err) return } m, err := io.Copy(tw, f1) //將main.exe文件中的信息寫入壓縮包中 if err != nil { fmt.Println(err) } fmt.Println(m) }
運行上面的代碼會在當前目錄下生成一個 output.tar 文件,如下圖所示: ?
圖:生成的 output.tar 文件
解壓 tar 歸檔文件
解壓 tar 歸檔文件比創建 tar 歸檔文檔稍微簡單些。首先需要將其打開,然后從這個 tar 頭部中循環讀取存儲在這個歸檔文件內的文件頭信息,從這個文件頭里讀取文件名,以這個文件名創建文件,然后向這個文件里寫入數據即可。 示例代碼如下所示:
package main ? import ( "archive/tar" "fmt" "io" "os" ) ? func main() { f, err := os.Open("output.tar") if err != nil { fmt.Println("文件打開失敗", err) return } defer f.Close() r := tar.NewReader(f) for hdr, err := r.Next(); err != io.EOF; hdr, err = r.Next() { if err != nil { fmt.Println(err) return } fileinfo := hdr.FileInfo() fmt.Println(fileinfo.Name()) f, err := os.Create("123" + fileinfo.Name()) if err != nil { fmt.Println(err) } defer f.Close() _, err = io.Copy(f, r) if err != nil { fmt.Println(err) } } }
運行上面的程序會將 tar 包的文件解壓到當前目錄中,如下圖所示: ?
圖:解壓 tar 包
至此,我們完成了對壓縮和歸檔文件及常規文件處理的介紹。Go語言使用 io.Reader、io.ReadCloser、io.Writer 和 io.WriteCloser 等接口處理文件的方式讓開發者可以使用相同的編碼模式來讀寫文件或者其他流(如網絡流或者甚至是字符串),從而大大降低了難度。
11.10?Go語言使用buffer讀取文件
buffer 是緩沖器的意思,Go語言要實現緩沖讀取需要使用到 bufio 包。bufio 包本身包裝了 io.Reader 和 io.Writer 對象,同時創建了另外的 Reader 和 Writer 對象,因此對于文本 I/O 來說,bufio 包提供了一定的便利性。 buffer 緩沖器的實現原理就是,將文件讀取進緩沖(內存)之中,再次讀取的時候就可以避免文件系統的 I/O 從而提高速度。同理在進行寫操作時,先把文件寫入緩沖(內存),然后由緩沖寫入文件系統。
使用 bufio 包寫入文件
bufio 和 io 包中有很多操作都是相似的,唯一不同的地方是 bufio 提供了一些緩沖的操作,如果對文件 I/O 操作比較頻繁的,使用 bufio 包能夠提高一定的性能。 在 bufio 包中,有一個 Writer 結構體,而其相關的方法支持一些寫入操作,如下所示。
//Writer 是一個空的結構體,一般需要使用 NewWriter 或者 NewWriterSize 來初始化一個結構體對象 type Writer struct { // contains filtered or unexported fields } ? //NewWriterSize 和 NewWriter 函數 //返回默認緩沖大小的 Writer 對象(默認是4096) func NewWriter(w io.Writer) *Writer ? //指定緩沖大小創建一個 Writer 對象 func NewWriterSize(w io.Writer, size int) *Writer ? //Writer 對象相關的寫入數據的方法 ? //把 p 中的內容寫入 buffer,返回寫入的字節數和錯誤信息。如果 nn < len(p),返回錯誤信息中會包含為什么寫入的數據比較短 func (b *Writer) Write(p []byte) (nn int, err error) //將 buffer 中的數據寫入 io.Writer func (b *Writer) Flush() error ? //以下三個方法可以直接寫入到文件中 //寫入單個字節 func (b *Writer) WriteByte(c byte) error //寫入單個 Unicode 指針返回寫入字節數錯誤信息 func (b *Writer) WriteRune(r rune) (size int, err error) //寫入字符串并返回寫入字節數和錯誤信息 func (b *Writer) WriteString(s string) (int, error)
示例代碼如下所示:
package main ? import ( "bufio" "fmt" "os" ) ? func main() { name := "demo.txt" content := "http://c.biancheng.net/golang/" ? fileObj, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) if err != nil { fmt.Println("文件打開失敗", err) } ? defer fileObj.Close() writeObj := bufio.NewWriterSize(fileObj, 4096) ? //使用 Write 方法,需要使用 Writer 對象的 Flush 方法將 buffer 中的數據刷到磁盤 buf := []byte(content) if _, err := writeObj.Write(buf); err == nil { if err := writeObj.Flush(); err != nil { panic(err) } fmt.Println("數據寫入成功") } }
運行上面的代碼會在當前目錄之下生成 demo.txt 文件,并將“http://c.biancheng.net/golang/”寫入到該文件中。
使用 bufio 包讀取文件
使用 bufio 包讀取文件也非常方便,我們先來看下 bufio 包的相關的 Reader 函數方法:
//首先定義了一個用來緩沖 io.Reader 對象的結構體,同時該結構體擁有以下相關的方法 type Reader struct { } ? //NewReader 函數用來返回一個默認大小 buffer 的 Reader 對象(默認大小是 4096) 等同于 NewReaderSize(rd,4096) func NewReader(rd io.Reader) *Reader ? //該函數返回一個指定大小 buffer(size 最小為 16)的 Reader 對象,如果 io.Reader 參數已經是一個足夠大的 Reader,它將返回該 Reader func NewReaderSize(rd io.Reader, size int) *Reader ? //該方法返回從當前 buffer 中能被讀到的字節數 func (b *Reader) Buffered() int ? //Discard 方法跳過后續的 n 個字節的數據,返回跳過的字節數。如果 0 <= n <= b.Buffered(),該方法將不會從 io.Reader 中成功讀取數據 func (b *Reader) Discard(n int) (discarded int, err error) ? //Peekf 方法返回緩存的一個切片,該切片只包含緩存中的前 n 個字節的數據 func (b *Reader) Peek(n int) ([]byte, error) ? //把 Reader 緩存對象中的數據讀入到 []byte 類型的 p 中,并返回讀取的字節數。讀取成功,err 將返回空值 func (b *Reader) Read(p []byte) (n int, err error) ? //返回單個字節,如果沒有數據返回 err func (b *Reader) ReadByte() (byte, error) ? //該方法在 b 中讀取 delimz 之前的所有數據,返回的切片是已讀出的數據的引用,切片中的數據在下一次的讀取操作之前是有效的。如果未找到 delim,將返回查找結果并返回 nil 空值。因為緩存的數據可能被下一次的讀寫操作修改,因此一般使用 ReadBytes 或者 ReadString,他們返回的都是數據拷貝 func (b *Reader) ReadSlice(delim byte) (line []byte, err error) ? //功能同 ReadSlice,返回數據的拷貝 func (b *Reader) ReadBytes(delim byte) ([]byte, error) ? //功能同 ReadBytes,返回字符串 func (b *Reader) ReadString(delim byte) (string, error) ? //該方法是一個低水平的讀取方式,一般建議使用 ReadBytes('\n') 或 ReadString('\n'),或者使用一個 Scanner 來代替。ReadLine 通過調用 ReadSlice 方法實現,返回的也是緩存的切片,用于讀取一行數據,不包括行尾標記(\n 或 \r\n) func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error) ? //讀取單個 UTF-8 字符并返回一個 rune 和字節大小 func (b *Reader) ReadRune() (r rune, size int, err error)
示例代碼如下:
package main ? import ( "bufio" "fmt" "os" "strconv" ) ? func main() { fileObj, err := os.Open("demo.txt") if err != nil { fmt.Println("文件打開失敗:", err) return } defer fileObj.Close() //一個文件對象本身是實現了io.Reader的 使用bufio.NewReader去初始化一個Reader對象,存在buffer中的,讀取一次就會被清空 reader := bufio.NewReader(fileObj) buf := make([]byte, 1024) //讀取 Reader 對象中的內容到 []byte 類型的 buf 中 info, err := reader.Read(buf) if err != nil { fmt.Println(err) } fmt.Println("讀取的字節數:" + strconv.Itoa(info)) //這里的buf是一個[]byte,因此如果需要只輸出內容,仍然需要將文件內容的換行符替換掉 fmt.Println("讀取的文件內容:", string(buf)) }
運行結果如下:
go run main.go 讀取的字節數:30 讀取的文件內容: http://c.biancheng.net/golang/
11.11?Go語言并發目錄遍歷
11.12?Go語言從INI配置文件中讀取需要的值
11.13?Go語言文件的寫入、追加、讀取、復制操作
Go語言的 os 包下有一個 OpenFile 函數,其原型如下所示:
func OpenFile(name string, flag int, perm FileMode) (file *File, err error)
其中 name 是文件的文件名,如果不是在當前路徑下運行需要加上具體路徑;flag 是文件的處理參數,為 int 類型,根據系統的不同具體值可能有所不同,但是作用是相同的。 下面列舉了一些常用的 flag 文件處理參數:
O_RDONLY:只讀模式打開文件; O_WRONLY:只寫模式打開文件; O_RDWR:讀寫模式打開文件; O_APPEND:寫操作時將數據附加到文件尾部(追加); O_CREATE:如果不存在將創建一個新文件; O_EXCL:和 O_CREATE 配合使用,文件必須不存在,否則返回一個錯誤; O_SYNC:當進行一系列寫操作時,每次都要等待上次的 I/O 操作完成再進行; O_TRUNC:如果可能,在打開時清空文件。
【示例 1】:創建一個新文件 golang.txt,并在其中寫入 5 句“http://c.biancheng.net/golang/”。
package main ? import ( ??? "bufio" ??? "fmt" ??? "os" ) ? func main() { ??? //創建一個新文件,寫入內容 5 句 “http://c.biancheng.net/golang/” ??? filePath := "e:/code/golang.txt" ??? file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666) ??? if err != nil { ??????? fmt.Println("文件打開失敗", err) ??? } ??? //及時關閉file句柄 ??? defer file.Close() ? ??? //寫入文件時,使用帶緩存的 *Writer ??? write := bufio.NewWriter(file) ??? for i := 0; i < 5; i++ { ??????? write.WriteString("http://c.biancheng.net/golang/ \n") ??? } ??? //Flush將緩存的文件真正寫入到文件中 ??? write.Flush() }
執行成功之后會在指定目錄下生成一個 golang.txt 文件,打開該文件如下圖所示: ?
【示例 2】:打開一個存在的文件,在原來的內容追加內容“C語言中文網”
package main ? import ( ??? "bufio" ??? "fmt" ??? "os" ) ? func main() { ??? filePath := "e:/code/golang.txt" ??? file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_APPEND, 0666) ??? if err != nil { ??????? fmt.Println("文件打開失敗", err) ??? } ??? //及時關閉file句柄 ??? defer file.Close() ? ??? //寫入文件時,使用帶緩存的 *Writer ??? write := bufio.NewWriter(file) ??? for i := 0; i < 5; i++ { ??????? write.WriteString("C語言中文網 \r\n") ??? } ??? //Flush將緩存的文件真正寫入到文件中 ??? write.Flush() }
執行成功之后,打開 golang.txt 文件發現內容追加成功,如下圖所示: ?
【示例 3】:打開一個存在的文件,將原來的內容讀出來,顯示在終端,并且追加 5 句“Hello,C語言中文網”。
package main ? import ( ??? "bufio" ??? "fmt" ??? "io" ??? "os" ) ? func main() { ??? filePath := "e:/code/golang.txt" ??? file, err := os.OpenFile(filePath, os.O_RDWR|os.O_APPEND, 0666) ??? if err != nil { ??????? fmt.Println("文件打開失敗", err) ??? } ??? //及時關閉file句柄 ??? defer file.Close() ? ??? //讀原來文件的內容,并且顯示在終端 ??? reader := bufio.NewReader(file) ??? for { ??????? str, err := reader.ReadString('\n') ??????? if err == io.EOF { ??????????? break ??????? } ??????? fmt.Print(str) ??? } ? ??? //寫入文件時,使用帶緩存的 *Writer ??? write := bufio.NewWriter(file) ??? for i := 0; i < 5; i++ { ??????? write.WriteString("Hello,C語言中文網。 \r\n") ??? } ??? //Flush將緩存的文件真正寫入到文件中 ??? write.Flush() }
執行成功之后,會在控制臺打印出文件的內容,并在文件中追加指定的內容,如下圖所示: ?
【示例 4】:編寫一個程序,將一個文件的內容復制到另外一個文件(注:這兩個文件都已存在)
package main ? import ( ??? "fmt" ??? "io/ioutil" ) ? func main() { ??? file1Path := "e:/code/golang.txt" ??? file2Path := "e:/code/other.txt" ??? data, err := ioutil.ReadFile(file1Path) ??? if err != nil { ??????? fmt.Printf("文件打開失敗=%v\n", err) ??????? return ??? } ??? err = ioutil.WriteFile(file2Path, data, 0666) ??? if err != nil { ??????? fmt.Printf("文件打開失敗=%v\n", err) ??? } }
執行成功后,發現內容已經復制成功,如下圖所示: ?
11.14?Go語言文件鎖操作
我們使用Go語言開發一些程序的時候,往往出現多個進程同時操作同一份文件的情況,這很容易導致文件中的數據混亂。這時我們就需要采用一些手段來平衡這些沖突,文件鎖(flock)應運而生,下面我們就來介紹一下。 對于 flock,最常見的例子就是 Nginx,進程運行起來后就會把當前的 PID 寫入這個文件,當然如果這個文件已經存在了,也就是前一個進程還沒有退出,那么 Nginx 就不會重新啟動,所以 flock 還可以用來檢測進程是否存在。 flock 是對于整個文件的建議性鎖。也就是說,如果一個進程在一個文件(inode)上放了鎖,其它進程是可以知道的(建議性鎖不強求進程遵守)。最棒的一點是,它的第一個參數是文件描述符,在此文件描述符關閉時,鎖會自動釋放。而當進程終止時,所有的文件描述符均會被關閉。所以很多時候就不用考慮類似原子鎖解鎖的事情。 在具體介紹前,先上代碼
package main ? import ( "fmt" "os" "sync" "syscall" "time" ) ? //文件鎖 type FileLock struct { dir string f *os.File } ? func New(dir string) *FileLock { return &FileLock{ dir: dir, } } ? //加鎖 func (l *FileLock) Lock() error { f, err := os.Open(l.dir) if err != nil { return err } l.f = f err = syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) if err != nil { return fmt.Errorf("cannot flock directory %s - %s", l.dir, err) } return nil } ? //釋放鎖 func (l *FileLock) Unlock() error { defer l.f.Close() return syscall.Flock(int(l.f.Fd()), syscall.LOCK_UN) } ? func main() { test_file_path, _ := os.Getwd() locked_file := test_file_path ? wg := sync.WaitGroup{} ? for i := 0; i < 10; i++ { wg.Add(1) go func(num int) { flock := New(locked_file) err := flock.Lock() if err != nil { wg.Done() fmt.Println(err.Error()) return } fmt.Printf("output : %d\n", num) wg.Done() }(i) } wg.Wait() time.Sleep(2 * time.Second) ? }
在 Windows 系統下運行上面的代碼會出現下面的錯誤:
undefined: syscall.Flock undefined: syscall.LOCK_EX undefined: syscall.LOCK_NB undefined: syscall.Flock undefined: syscall.LOCK_UN
這是因為 Windows 系統不支持 pid 鎖,所以我們需要在 Linux 或 Mac 系統下才能正常運行上面的程序。 上面代碼中演示了同時啟動 10 個 goroutinue,但在程序運行過程中,只有一個 goroutine 能獲得文件鎖(flock)。其它的 goroutinue 在獲取不到 flock 后,會拋出異常的信息。這樣即可達到同一文件在指定的周期內只允許一個進程訪問的效果。 代碼中文件鎖的具體調用:
syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
我們采用了 syscall.LOCK_EX、syscall.LOCK_NB,這是什么意思呢? flock 屬于建議性鎖,不具備強制性。一個進程使用 flock 將文件鎖住,另一個進程可以直接操作正在被鎖的文件,修改文件中的數據,原因在于 flock 只是用于檢測文件是否被加鎖,針對文件已經被加鎖,另一個進程寫入數據的情況,內核不會阻止這個進程的寫入操作,也就是建議性鎖的內核處理策略。 flock 主要三種操作類型:
LOCK_SH:共享鎖,多個進程可以使用同一把鎖,常被用作讀共享鎖; LOCK_EX:排他鎖,同時只允許一個進程使用,常被用作寫鎖; LOCK_UN:釋放鎖。
進程使用 flock 嘗試鎖文件時,如果文件已經被其他進程鎖住,進程會被阻塞直到鎖被釋放掉,或者在調用 flock 的時候,采用 LOCK_NB 參數。在嘗試鎖住該文件的時候,發現已經被其他服務鎖住,會返回錯誤,錯誤碼為 EWOULDBLOCK。 flock 鎖的釋放非常具有特色,即可調用 LOCK_UN 參數來釋放文件鎖,也可以通過關閉 fd 的方式來釋放文件鎖(flock 的第一個參數是 fd),意味著 flock 會隨著進程的關閉而被自動釋放掉。
總結
以上是生活随笔 為你收集整理的第11章 Go语言文件处理 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。