Go官方庫的文件操作分散在多個包中,比如os
、ioutil
包,我本來想寫一篇總結性的Go文件操作的文章,卻發現已經有人2015年已經寫了一篇這樣的文章,寫的非常好,所以我翻譯成了中文,強烈推薦你閱讀一下。
原文: Working
with Files in Go, 作者: NanoDano
介紹
萬物皆文件
UNIX 的一個基礎設計就是"萬物皆文件"(everything is a file)。我們不必知道一個文件到底映射成什麼,操作系統的設備驅動抽象成文件。操作系統爲設備提供了文件格式的接口。
Go語言中的reader和writer接口也類似。我們只需簡單的讀寫字節,不必知道reader的數據來自哪裏,也不必知道writer將數據發送到哪裏。
你可以在/dev
下查看可用的設備,有些可能需要較高的權限才能訪問。
基本操作
創建空文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
package main import ( "log" "os" ) var ( newFile *os.File err error ) func main() { newFile, err = os.Create("test.txt") if err != nil { log.Fatal(err) } log.Println(newFile) newFile.Close() }
|
Truncate文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
package main import ( "log" "os" ) func main() { err := os.Truncate("test.txt", 100) if err != nil { log.Fatal(err) } }
|
得到文件信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
package main import ( "fmt" "log" "os" ) var ( fileInfo os.FileInfo err error ) func main() { fileInfo, err = os.Stat("test.txt") if err != nil { log.Fatal(err) } fmt.Println("File name:", fileInfo.Name()) fmt.Println("Size in bytes:", fileInfo.Size()) fmt.Println("Permissions:", fileInfo.Mode()) fmt.Println("Last modified:", fileInfo.ModTime()) fmt.Println("Is Directory: ", fileInfo.IsDir()) fmt.Printf("System interface type: %T\n", fileInfo.Sys()) fmt.Printf("System info: %+v\n\n", fileInfo.Sys()) }
|
重命名和移動
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
package main import ( "log" "os" ) func main() { originalPath := "test.txt" newPath := "test2.txt" err := os.Rename(originalPath, newPath) if err != nil { log.Fatal(err) } }
|
譯者按: rename 和 move 原理一樣
刪除文件
1 2 3 4 5 6 7 8 9 10 11 12 13
|
package main import ( "log" "os" ) func main() { err := os.Remove("test.txt") if err != nil { log.Fatal(err) } }
|
打開和關閉文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
package main import ( "log" "os" ) func main() { file, err := os.Open("test.txt") if err != nil { log.Fatal(err) } file.Close() file, err = os.OpenFile("test.txt", os.O_APPEND, 0666) if err != nil { log.Fatal(err) } file.Close() }
|
譯者按:熟悉Linux的讀者應該很熟悉權限模式,通過Linux命令chmod
可以更改文件的權限
https://www.linux.com/learn/understanding-linux-file-permissions
補充了原文未介紹的flag
檢查文件是否存在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
package main import ( "log" "os" ) var ( fileInfo *os.FileInfo err error ) func main() { fileInfo, err := os.Stat("test.txt") if err != nil { if os.IsNotExist(err) { log.Fatal("File does not exist.") } } log.Println("File does exist. File information:") log.Println(fileInfo) }
|
檢查讀寫權限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
package main import ( "log" "os" ) func main() { file, err := os.OpenFile("test.txt", os.O_WRONLY, 0666) if err != nil { if os.IsPermission(err) { log.Println("Error: Write permission denied.") } } file.Close() file, err = os.OpenFile("test.txt", os.O_RDONLY, 0666) if err != nil { if os.IsPermission(err) { log.Println("Error: Read permission denied.") } } file.Close() }
|
改變權限、擁有者、時間戳
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
package main import ( "log" "os" "time" ) func main() { err := os.Chmod("test.txt", 0777) if err != nil { log.Println(err) } err = os.Chown("test.txt", os.Getuid(), os.Getgid()) if err != nil { log.Println(err) } twoDaysFromNow := time.Now().Add(48 * time.Hour) lastAccessTime := twoDaysFromNow lastModifyTime := twoDaysFromNow err = os.Chtimes("test.txt", lastAccessTime, lastModifyTime) if err != nil { log.Println(err) } }
|
硬鏈接和軟鏈接
一個普通的文件是一個指向硬盤的inode的地方。
硬鏈接創建一個新的指針指向同一個地方。只有所有的鏈接被刪除後文件纔會被刪除。硬鏈接只在相同的文件系統中才工作。你可以認爲一個硬鏈接是一個正常的鏈接。
symbolic link,又叫軟連接,和硬鏈接有點不一樣,它不直接指向硬盤中的相同的地方,而是通過名字引用其它文件。他們可以指向不同的文件系統中的不同文件。並不是所有的操作系統都支持軟鏈接。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
package main import ( "os" "log" "fmt" ) func main() { err := os.Link("original.txt", "original_also.txt") if err != nil { log.Fatal(err) } fmt.Println("creating sym") err = os.Symlink("original.txt", "original_sym.txt") if err != nil { log.Fatal(err) } fileInfo, err := os.Lstat("original_sym.txt") if err != nil { log.Fatal(err) } fmt.Printf("Link info: %+v", fileInfo) err = os.Lchown("original_sym.txt", os.Getuid(), os.Getgid()) if err != nil { log.Fatal(err) } }
|
讀寫
複製文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
package main import ( "os" "log" "io" ) func main() { originalFile, err := os.Open("test.txt") if err != nil { log.Fatal(err) } defer originalFile.Close() newFile, err := os.Create("test_copy.txt") if err != nil { log.Fatal(err) } defer newFile.Close() bytesWritten, err := io.Copy(newFile, originalFile) if err != nil { log.Fatal(err) } log.Printf("Copied %d bytes.", bytesWritten) err = newFile.Sync() if err != nil { log.Fatal(err) } }
|
跳轉到文件指定位置(Seek)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
|
package main import ( "os" "fmt" "log" ) func main() { file, _ := os.Open("test.txt") defer file.Close() var offset int64 = 5 var whence int = 0 newPosition, err := file.Seek(offset, whence) if err != nil { log.Fatal(err) } fmt.Println("Just moved to 5:", newPosition) newPosition, err = file.Seek(-2, 1) if err != nil { log.Fatal(err) } fmt.Println("Just moved back two:", newPosition) currentPosition, err := file.Seek(0, 1) fmt.Println("Current position:", currentPosition) newPosition, err = file.Seek(0, 0) if err != nil { log.Fatal(err) } fmt.Println("Position after seeking 0,0:", newPosition) }
|
寫文件
可以使用os
包寫入一個打開的文件。
因爲Go可執行包是靜態鏈接的可執行文件,你import的每一個包都會增加你的可執行文件的大小。其它的包如io
、`ioutil`、`bufio`提供了一些方法,但是它們不是必須的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
package main import ( "os" "log" ) func main() { file, err := os.OpenFile( "test.txt", os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666, ) if err != nil { log.Fatal(err) } defer file.Close() byteSlice := []byte("Bytes!\n") bytesWritten, err := file.Write(byteSlice) if err != nil { log.Fatal(err) } log.Printf("Wrote %d bytes.\n", bytesWritten) }
|
快寫文件
ioutil
包有一個非常有用的方法WriteFile()
可以處理創建/打開文件、寫字節slice和關閉文件一系列的操作。如果你需要簡潔快速地寫字節slice到文件中,你可以使用它。
1 2 3 4 5 6 7 8 9 10 11 12 13
|
package main import ( "io/ioutil" "log" ) func main() { err := ioutil.WriteFile("test.txt", []byte("Hi\n"), 0666) if err != nil { log.Fatal(err) } }
|
使用緩存寫
bufio
包提供了帶緩存功能的writer,所以你可以在寫字節到硬盤前使用內存緩存。當你處理很多的數據很有用,因爲它可以節省操作硬盤I/O的時間。在其它一些情況下它也很有用,比如你每次寫一個字節,把它們攢在內存緩存中,然後一次寫入到硬盤中,減少硬盤的磨損以及提升性能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
|
package main import ( "log" "os" "bufio" ) func main() { file, err := os.OpenFile("test.txt", os.O_WRONLY, 0666) if err != nil { log.Fatal(err) } defer file.Close() bufferedWriter := bufio.NewWriter(file) bytesWritten, err := bufferedWriter.Write( []byte{65, 66, 67}, ) if err != nil { log.Fatal(err) } log.Printf("Bytes written: %d\n", bytesWritten) bytesWritten, err = bufferedWriter.WriteString( "Buffered string\n", ) if err != nil { log.Fatal(err) } log.Printf("Bytes written: %d\n", bytesWritten) unflushedBufferSize := bufferedWriter.Buffered() log.Printf("Bytes buffered: %d\n", unflushedBufferSize) bytesAvailable := bufferedWriter.Available() if err != nil { log.Fatal(err) } log.Printf("Available buffer: %d\n", bytesAvailable) bufferedWriter.Flush() bufferedWriter.Reset(bufferedWriter) bytesAvailable = bufferedWriter.Available() if err != nil { log.Fatal(err) } log.Printf("Available buffer: %d\n", bytesAvailable) bufferedWriter = bufio.NewWriterSize( bufferedWriter, 8000, ) bytesAvailable = bufferedWriter.Available() if err != nil { log.Fatal(err) } log.Printf("Available buffer: %d\n", bytesAvailable) }
|
讀取最多N個字節
os.File
提供了文件操作的基本功能,
而io
、ioutil
、bufio
提供了額外的輔助函數。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
package main import ( "os" "log" ) func main() { file, err := os.Open("test.txt") if err != nil { log.Fatal(err) } defer file.Close() byteSlice := make([]byte, 16) bytesRead, err := file.Read(byteSlice) if err != nil { log.Fatal(err) } log.Printf("Number of bytes read: %d\n", bytesRead) log.Printf("Data read: %s\n", byteSlice) }
|
讀取正好N個字節
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
package main import ( "os" "log" "io" ) func main() { file, err := os.Open("test.txt") if err != nil { log.Fatal(err) } byteSlice := make([]byte, 2) numBytesRead, err := io.ReadFull(file, byteSlice) if err != nil { log.Fatal(err) } log.Printf("Number of bytes read: %d\n", numBytesRead) log.Printf("Data read: %s\n", byteSlice) }
|
讀取至少N個字節
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
package main import ( "os" "log" "io" ) func main() { file, err := os.Open("test.txt") if err != nil { log.Fatal(err) } byteSlice := make([]byte, 512) minBytes := 8 numBytesRead, err := io.ReadAtLeast(file, byteSlice, minBytes) if err != nil { log.Fatal(err) } log.Printf("Number of bytes read: %d\n", numBytesRead) log.Printf("Data read: %s\n", byteSlice) }
|
讀取全部字節
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
package main import ( "os" "log" "fmt" "io/ioutil" ) func main() { file, err := os.Open("test.txt") if err != nil { log.Fatal(err) } data, err := ioutil.ReadAll(file) if err != nil { log.Fatal(err) } fmt.Printf("Data as hex: %x\n", data) fmt.Printf("Data as string: %s\n", data) fmt.Println("Number of bytes read:", len(data)) }
|
快讀到內存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
package main import ( "log" "io/ioutil" ) func main() { data, err := ioutil.ReadFile("test.txt") if err != nil { log.Fatal(err) } log.Printf("Data read: %s\n", data) }
|
使用緩存讀
有緩存寫也有緩存讀。
緩存reader會把一些內容緩存在內存中。它會提供比os.File
和io.Reader
更多的函數,缺省的緩存大小是4096,最小緩存是16。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
|
package main import ( "os" "log" "bufio" "fmt" ) func main() { file, err := os.Open("test.txt") if err != nil { log.Fatal(err) } bufferedReader := bufio.NewReader(file) byteSlice := make([]byte, 5) byteSlice, err = bufferedReader.Peek(5) if err != nil { log.Fatal(err) } fmt.Printf("Peeked at 5 bytes: %s\n", byteSlice) numBytesRead, err := bufferedReader.Read(byteSlice) if err != nil { log.Fatal(err) } fmt.Printf("Read %d bytes: %s\n", numBytesRead, byteSlice) myByte, err := bufferedReader.ReadByte() if err != nil { log.Fatal(err) } fmt.Printf("Read 1 byte: %c\n", myByte) dataBytes, err := bufferedReader.ReadBytes('\n') if err != nil { log.Fatal(err) } fmt.Printf("Read bytes: %s\n", dataBytes) dataString, err := bufferedReader.ReadString('\n') if err != nil { log.Fatal(err) } fmt.Printf("Read string: %s\n", dataString) }
|
使用 scanner
Scanner
是bufio
包下的類型,在處理文件中以分隔符分隔的文本時很有用。
通常我們使用換行符作爲分隔符將文件內容分成多行。在CSV文件中,逗號一般作爲分隔符。
os.File
文件可以被包裝成bufio.Scanner
,它就像一個緩存reader。
我們會調用Scan()
方法去讀取下一個分隔符,使用Text()
或者Bytes()
獲取讀取的數據。
分隔符可以不是一個簡單的字節或者字符,有一個特殊的方法可以實現分隔符的功能,以及將指針移動多少,返回什麼數據。
如果沒有定製的SplitFunc
提供,缺省的ScanLines
會使用newline
字符作爲分隔符,其它的分隔函數還包括ScanRunes
和ScanWords
,皆在bufio
包中。
1 2 3 4 5 6
|
type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)
|
下面的例子中,爲一個文件創建了bufio.Scanner
,並按照單詞逐個讀取:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
package main import ( "os" "log" "fmt" "bufio" ) func main() { file, err := os.Open("test.txt") if err != nil { log.Fatal(err) } scanner := bufio.NewScanner(file) scanner.Split(bufio.ScanWords) success := scanner.Scan() if success == false { err = scanner.Err() if err == nil { log.Println("Scan completed and reached EOF") } else { log.Fatal(err) } } fmt.Println("First word found:", scanner.Text()) }
|
壓縮
打包(zip) 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
|
package main import ( "archive/zip" "log" "os" ) func main() { outFile, err := os.Create("test.zip") if err != nil { log.Fatal(err) } defer outFile.Close() zipWriter := zip.NewWriter(outFile) var filesToArchive = []struct { Name, Body string } { {"test.txt", "String contents of file"}, {"test2.txt", "\x61\x62\x63\n"}, } for _, file := range filesToArchive { fileWriter, err := zipWriter.Create(file.Name) if err != nil { log.Fatal(err) } _, err = fileWriter.Write([]byte(file.Body)) if err != nil { log.Fatal(err) } } err = zipWriter.Close() if err != nil { log.Fatal(err) } }
|
抽取(unzip) 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
|
package main import ( "archive/zip" "log" "io" "os" "path/filepath" ) func main() { zipReader, err := zip.OpenReader("test.zip") if err != nil { log.Fatal(err) } defer zipReader.Close() for _, file := range zipReader.Reader.File { zippedFile, err := file.Open() if err != nil { log.Fatal(err) } defer zippedFile.Close() targetDir := "./" extractedFilePath := filepath.Join( targetDir, file.Name, ) if file.FileInfo().IsDir() { log.Println("Creating directory:", extractedFilePath) os.MkdirAll(extractedFilePath, file.Mode()) } else { log.Println("Extracting file:", file.Name) outputFile, err := os.OpenFile( extractedFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode(), ) if err != nil { log.Fatal(err) } defer outputFile.Close() _, err = io.Copy(outputFile, zippedFile) if err != nil { log.Fatal(err) } } } }
|
壓縮文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
package main import ( "os" "compress/gzip" "log" ) func main() { outputFile, err := os.Create("test.txt.gz") if err != nil { log.Fatal(err) } gzipWriter := gzip.NewWriter(outputFile) defer gzipWriter.Close() _, err = gzipWriter.Write([]byte("Gophers rule!\n")) if err != nil { log.Fatal(err) } log.Println("Compressed data written to file.") }
|
解壓縮文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
package main import ( "compress/gzip" "log" "io" "os" ) func main() { gzipFile, err := os.Open("test.txt.gz") if err != nil { log.Fatal(err) } gzipReader, err := gzip.NewReader(gzipFile) if err != nil { log.Fatal(err) } defer gzipReader.Close() outfileWriter, err := os.Create("unzipped.txt") if err != nil { log.Fatal(err) } defer outfileWriter.Close() _, err = io.Copy(outfileWriter, gzipReader) if err != nil { log.Fatal(err) } }
|
其它
臨時文件和目錄
ioutil
提供了兩個函數: TempDir()
和 TempFile()
。
使用完畢後,調用者負責刪除這些臨時文件和文件夾。
有一點好處就是當你傳遞一個空字符串作爲文件夾名的時候,它會在操作系統的臨時文件夾中創建這些項目(/tmp on Linux)。
os.TempDir()
返回當前操作系統的臨時文件夾。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
package main import ( "os" "io/ioutil" "log" "fmt" ) func main() { tempDirPath, err := ioutil.TempDir("", "myTempDir") if err != nil { log.Fatal(err) } fmt.Println("Temp dir created:", tempDirPath) tempFile, err := ioutil.TempFile(tempDirPath, "myTempFile.txt") if err != nil { log.Fatal(err) } fmt.Println("Temp file created:", tempFile.Name()) err = tempFile.Close() if err != nil { log.Fatal(err) } err = os.Remove(tempFile.Name()) if err != nil { log.Fatal(err) } err = os.Remove(tempDirPath) if err != nil { log.Fatal(err) } }
|
通過HTTP下載文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
package main import ( "os" "io" "log" "net/http" ) func main() { newFile, err := os.Create("devdungeon.html") if err != nil { log.Fatal(err) } defer newFile.Close() url := "http://www.devdungeon.com/archive" response, err := http.Get(url) defer response.Body.Close() numBytesWritten, err := io.Copy(newFile, response.Body) if err != nil { log.Fatal(err) } log.Printf("Downloaded %d byte file.\n", numBytesWritten) }
|
哈希和摘要
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
package main import ( "crypto/md5" "crypto/sha1" "crypto/sha256" "crypto/sha512" "log" "fmt" "io/ioutil" ) func main() { data, err := ioutil.ReadFile("test.txt") if err != nil { log.Fatal(err) } fmt.Printf("Md5: %x\n\n", md5.Sum(data)) fmt.Printf("Sha1: %x\n\n", sha1.Sum(data)) fmt.Printf("Sha256: %x\n\n", sha256.Sum256(data)) fmt.Printf("Sha512: %x\n\n", sha512.Sum512(data)) }
|
上面的例子複製整個文件內容到內存中,傳遞給hash函數。
另一個方式是創建一個hash writer, 使用Write
、WriteString
、Copy
將數據傳給它。
下面的例子使用 md5 hash,但你可以使用其它的Writer。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
package main import ( "crypto/md5" "log" "fmt" "io" "os" ) func main() { file, err := os.Open("test.txt") if err != nil { log.Fatal(err) } defer file.Close() hasher := md5.New() _, err = io.Copy(hasher, file) if err != nil { log.Fatal(err) } sum := hasher.Sum(nil) fmt.Printf("Md5 checksum: %x\n", sum) }
|
文章來源 http://colobu.com/2016/10/12/go-file-operations/ 一個非常好的個人技術blog