在 Go 語言中,文件使用指向 os.File
類型的指針來表示的,也叫做文件句柄。我們在前面章節(jié)使用到過標準輸入 os.Stdin
和標準輸出 os.Stdout
,他們的類型都是 *os.File
。讓我們來看看下面這個程序:
示例 12.4 fileinput.go:
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
inputFile, inputError := os.Open("input.dat")
if inputError != nil {
fmt.Printf("An error occurred on opening the inputfile\n" +
"Does the file exist?\n" +
"Have you got acces to it?\n")
return // exit the function on error
}
defer inputFile.Close()
inputReader := bufio.NewReader(inputFile)
for {
inputString, readerError := inputReader.ReadString('\n')
fmt.Printf("The input was: %s", inputString)
if readerError == io.EOF {
return
}
}
}
變量 inputFile
是 *os.File
類型的。該類型是一個結(jié)構(gòu),表示一個打開文件的描述符(文件句柄)。然后,使用 os
包里的 Open
函數(shù)來打開一個文件。該函數(shù)的參數(shù)是文件名,類型為 string
。在上面的程序中,我們以只讀模式打開 input.dat
文件。
如果文件不存在或者程序沒有足夠的權(quán)限打開這個文件,Open函數(shù)會返回一個錯誤:inputFile, inputError = os.Open("input.dat")
。如果文件打開正常,我們就使用 defer inputFile.Close()
語句確保在程序退出前關(guān)閉該文件。然后,我們使用 bufio.NewReader
來獲得一個讀取器變量。
通過使用 bufio
包提供的讀取器(寫入器也類似),如上面程序所示,我們可以很方便的操作相對高層的 string 對象,而避免了去操作比較底層的字節(jié)。
接著,我們在一個無限循環(huán)中使用 ReadString('\n')
或 ReadBytes('\n')
將文件的內(nèi)容逐行(行結(jié)束符 '\n')讀取出來。
注意: 在之前的例子中,我們看到,Unix和Linux的行結(jié)束符是 \n,而Windows的行結(jié)束符是 \r\n。在使用 ReadString
和 ReadBytes
方法的時候,我們不需要關(guān)心操作系統(tǒng)的類型,直接使用 \n 就可以了。另外,我們也可以使用 ReadLine()
方法來實現(xiàn)相同的功能。
一旦讀取到文件末尾,變量 readerError
的值將變成非空(事實上,常量 io.EOF
的值是 true),我們就會執(zhí)行 return
語句從而退出循環(huán)。
其他類似函數(shù):
1) 將整個文件的內(nèi)容讀到一個字符串里:
如果您想這么做,可以使用 io/ioutil
包里的 ioutil.ReadFile()
方法,該方法第一個返回值的類型是 []byte
,里面存放讀取到的內(nèi)容,第二個返回值是錯誤,如果沒有錯誤發(fā)生,第二個返回值為 nil。請看示例 12.5。類似的,函數(shù) WriteFile()
可以將 []byte
的值寫入文件。
示例 12.5 read_write_file1.go:
package main
import (
"fmt"
"io/ioutil"
"os"
)
func main() {
inputFile := "products.txt"
outputFile := "products_copy.txt"
buf, err := ioutil.ReadFile(inputFile)
if err != nil {
fmt.Fprintf(os.Stderr, "File Error: %s\n", err)
// panic(err.Error())
}
fmt.Printf("%s\n", string(buf))
err = ioutil.WriteFile(outputFile, buf, 0644) // oct, not hex
if err != nil {
panic(err.Error())
}
}
2) 帶緩沖的讀取
在很多情況下,文件的內(nèi)容是不按行劃分的,或者干脆就是一個二進制文件。在這種情況下,ReadString()
就無法使用了,我們可以使用 bufio.Reader
的 Read()
,它只接收一個參數(shù):
buf := make([]byte, 1024)
...
n, err := inputReader.Read(buf)
if (n == 0) { break}
變量 n 的值表示讀取到的字節(jié)數(shù).
3) 按列讀取文件中的數(shù)據(jù)
如果數(shù)據(jù)是按列排列并用空格分隔的,你可以使用 fmt
包提供的以 FScan 開頭的一系列函數(shù)來讀取他們。請看以下程序,我們將 3 列的數(shù)據(jù)分別讀入變量 v1、v2 和 v3 內(nèi),然后分別把他們添加到切片的尾部。
示例 12.6 read_file2.go:
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.Open("products2.txt")
if err != nil {
panic(err)
}
defer file.Close()
var col1, col2, col3 []string
for {
var v1, v2, v3 string
_, err := fmt.Fscanln(file, &v1, &v2, &v3)
// scans until newline
if err != nil {
break
}
col1 = append(col1, v1)
col2 = append(col2, v2)
col3 = append(col3, v3)
}
fmt.Println(col1)
fmt.Println(col2)
fmt.Println(col3)
}
輸出結(jié)果:
[ABC FUNC GO]
[40 56 45]
[150 280 356]
注意: path
包里包含一個子包叫 filepath
,這個子包提供了跨平臺的函數(shù),用于處理文件名和路徑。例如 Base() 函數(shù)用于獲得路徑中的最后一個元素(不包含后面的分隔符):
import "path/filepath"
filename := filepath.Base(path)
練習(xí) 12.3:read_csv.go
文件 products.txt 的內(nèi)容如下:
"The ABC of Go";25.5;1500
"Functional Programming with Go";56;280
"Go for It";45.9;356
"The Go Way";55;500
每行的第一個字段為 title,第二個字段為 price,第三個字段為 quantity。內(nèi)容的格式基本與 示例 12.3c 的相同,除了分隔符改成了分號。請讀取出文件的內(nèi)容,創(chuàng)建一個結(jié)構(gòu)用于存取一行的數(shù)據(jù),然后使用結(jié)構(gòu)的切片,并把數(shù)據(jù)打印出來。
關(guān)于解析 CSV 文件,encoding/csv
包提供了相應(yīng)的功能。具體請參考 http://golang.org/pkg/encoding/csv/
compress
包:讀取壓縮文件compress
包提供了讀取壓縮文件的功能,支持的壓縮文件格式為:bzip2、flate、gzip、lzw 和 zlib。
下面的程序展示了如何讀取一個 gzip 文件。
示例 12.7 gzipped.go:
package main
import (
"fmt"
"bufio"
"os"
"compress/gzip"
)
func main() {
fName := "MyFile.gz"
var r *bufio.Reader
fi, err := os.Open(fName)
if err != nil {
fmt.Fprintf(os.Stderr, "%v, Can't open %s: error: %s\n", os.Args[0], fName,
err)
os.Exit(1)
}
fz, err := gzip.NewReader(fi)
if err != nil {
r = bufio.NewReader(fi)
} else {
r = bufio.NewReader(fz)
}
for {
line, err := r.ReadString('\n')
if err != nil {
fmt.Println("Done reading file")
os.Exit(0)
}
fmt.Println(line)
}
}
請看以下程序:
示例 12.8 fileoutput.go:
package main
import (
"os"
"bufio"
"fmt"
)
func main () {
// var outputWriter *bufio.Writer
// var outputFile *os.File
// var outputError os.Error
// var outputString string
outputFile, outputError := os.OpenFile("output.dat", os.O_WRONLY|os.O_CREATE, 0666)
if outputError != nil {
fmt.Printf("An error occurred with file opening or creation\n")
return
}
defer outputFile.Close()
outputWriter := bufio.NewWriter(outputFile)
outputString := "hello world!\n"
for i:=0; i<10; i++ {
outputWriter.WriteString(outputString)
}
outputWriter.Flush()
}
除了文件句柄,我們還需要 bufio
的 Writer
。我們以只寫模式打開文件 output.dat
,如果文件不存在則自動創(chuàng)建:
outputFile, outputError := os.OpenFile("output.dat", os.O_WRONLY|os.O_CREATE, 0666)
可以看到,OpenFile
函數(shù)有三個參數(shù):文件名、一個或多個標志(使用邏輯運算符“|”連接),使用的文件權(quán)限。
我們通常會用到以下標志:
os.O_RDONLY
:只讀 os.O_WRONLY
:只寫 os.O_CREATE
:創(chuàng)建:如果指定文件不存在,就創(chuàng)建該文件。 os.O_TRUNC
:截斷:如果指定文件已存在,就將該文件的長度截為0。在讀文件的時候,文件的權(quán)限是被忽略的,所以在使用 OpenFile
時傳入的第三個參數(shù)可以用0。而在寫文件時,不管是 Unix 還是 Windows,都需要使用 0666。
然后,我們創(chuàng)建一個寫入器(緩沖區(qū))對象:
outputWriter := bufio.NewWriter(outputFile)
接著,使用一個 for 循環(huán),將字符串寫入緩沖區(qū),寫 10 次:outputWriter.WriteString(outputString)
緩沖區(qū)的內(nèi)容緊接著被完全寫入文件:outputWriter.Flush()
如果寫入的東西很簡單,我們可以使用 fmt.Fprintf(outputFile, "Some test data.\n")
直接將內(nèi)容寫入文件。fmt
包里的 F 開頭的 Print 函數(shù)可以直接寫入任何 io.Writer
,包括文件(請參考章節(jié)12.8)。
程序 filewrite.go
展示了不使用 fmt.FPrintf
函數(shù),使用其他函數(shù)如何寫文件:
示例 12.8 filewrite.go:
package main
import "os"
func main() {
os.Stdout.WriteString("hello, world\n")
f, _ := os.OpenFile("test", os.O_CREATE|os.O_WRONLY, 0666)
defer f.Close()
f.WriteString("hello, world in a file\n")
}
使用 os.Stdout.WriteString("hello, world\n")
,我們可以輸出到屏幕。
我們以只寫模式創(chuàng)建或打開文件"test",并且忽略了可能發(fā)生的錯誤:f, _ := os.OpenFile("test", os.O_CREATE|os.O_WRONLY, 0666)
我們不使用緩沖區(qū),直接將內(nèi)容寫入文件:f.WriteString( )
練習(xí) 12.4:wiki_part1.go
(這是一個獨立的練習(xí),但是同時也是為章節(jié)15.4做準備)
程序中的數(shù)據(jù)結(jié)構(gòu)如下,是一個包含以下字段的結(jié)構(gòu):
type Page struct {
Title string
Body []byte
}
請給這個結(jié)構(gòu)編寫一個 save
方法,將 Title 作為文件名、Body作為文件內(nèi)容,寫入到文本文件中。
再編寫一個 load
函數(shù),接收的參數(shù)是字符串 title,該函數(shù)讀取出與 title 對應(yīng)的文本文件。請使用 *Page
做為參數(shù),因為這個結(jié)構(gòu)可能相當巨大,我們不想在內(nèi)存中拷貝它。請使用 ioutil
包里的函數(shù)(參考章節(jié)12.2.1)。