1. 程式人生 > >Go36-44,45-文件操作(os.File)

Go36-44,45-文件操作(os.File)

dea can 壓縮文件 iat 整數 反射 指針方法 director pipe

os包

通過os包,可以擁有操控計算機操作系統的能力。這個代碼包提供的都是平臺不相關的API。無論是Linux、macOS、Windows、FreeBSD、OpenBSD、Plan9,os包都可以提供統一的使用接口。這樣就可以用同樣的方法來操縱不同的操作系統,並得到相似的結果。
os包中的API主要可以幫助我們使用操作系統中的文件系統、權限系統、環境變量、系統進程以及系統信號。其中,文件系統的API最豐富。不但可以用來創建和刪除文件以及目錄,還可以獲取到各種信息、修改內容、修改訪問權限。等等。這裏,最常用的數據類型就是:os.File。

os.File類型介紹

從字面上看,os.File類型代表了操作系統中的文件,但是實際上,它代表的遠不止於此。比如對於類Unix的操作系統,包括Linux、macOS、FreeBSD等,其中的一切都可以被看作是文件。

除了文本文件、二進制文件、壓縮文件、目錄這些常見的形式之外,還有符號鏈接、各種物理設備(包括內置或外接的面向塊或者字符的設備)、命名管道,以及套接字(也就是socket),等等。所以能夠利用os.File類型操縱的東西有很多。不過接下來主要介紹os.File類型應用於常規文件。

實現的io接口

os.File類型擁有的都是指針方法,它的指針實現了很多io包中的接口。
對於io包中最核心的3個簡單接口io.Reader、io.Writer和io.Closer,*os.File類型都實現了它們。另外還順便實現了io包中的9個擴展接口中的7個。沒有實現簡單接口io.ByteReader和io.RuneReader,所以也沒有實現上面這兩個的擴展接口io.ByteScannser和io.RuneScanner。

總之,os.File類型及其指針類型的值,不但可以通過各種方式讀取和寫入某個文件中的內容,還可以尋找並設定下一次讀取或寫入時的起始索引位置,另外還可以隨時對文件進行關閉。但是,它並不能專門的讀取文件中的下一個字節或Unicode字符,也不能進行任何的讀回退操作。不過,單讀讀取下一個字節或字符的功能也可以通過其他方式來實現。比如用Read方法傳入適當的參數。

使用反射檢查接口的實現
下面的示例枚舉了io包中的所有接口,檢查*os.File是否實現了該接口:

package main

import (
    "bytes"
    "fmt"
    "io"
    "os"
    "reflect"
)

// ioTypes 代表了io代碼包中的所有接口的反射類型。
var ioTypes = []reflect.Type{
    reflect.TypeOf((*io.Reader)(nil)).Elem(),
    reflect.TypeOf((*io.Writer)(nil)).Elem(),
    reflect.TypeOf((*io.Closer)(nil)).Elem(),

    reflect.TypeOf((*io.ByteReader)(nil)).Elem(),
    reflect.TypeOf((*io.RuneReader)(nil)).Elem(),
    reflect.TypeOf((*io.ReaderAt)(nil)).Elem(),
    reflect.TypeOf((*io.Seeker)(nil)).Elem(),
    reflect.TypeOf((*io.WriterTo)(nil)).Elem(),
    reflect.TypeOf((*io.ByteWriter)(nil)).Elem(),
    reflect.TypeOf((*io.WriterAt)(nil)).Elem(),
    reflect.TypeOf((*io.ReaderFrom)(nil)).Elem(),

    reflect.TypeOf((*io.ByteScanner)(nil)).Elem(),
    reflect.TypeOf((*io.RuneScanner)(nil)).Elem(),
    reflect.TypeOf((*io.ReadSeeker)(nil)).Elem(),
    reflect.TypeOf((*io.ReadCloser)(nil)).Elem(),
    reflect.TypeOf((*io.WriteCloser)(nil)).Elem(),
    reflect.TypeOf((*io.WriteSeeker)(nil)).Elem(),
    reflect.TypeOf((*io.ReadWriter)(nil)).Elem(),
    reflect.TypeOf((*io.ReadWriteSeeker)(nil)).Elem(),
    reflect.TypeOf((*io.ReadWriteCloser)(nil)).Elem(),
}

func main() {
    var file os.File
    fileType := reflect.TypeOf(&file)
    var buf bytes.Buffer  // 存放沒有實現的那些接口信息,最後統一打印出來
    fmt.Fprintf(&buf, "Type %T not implements:\n", &file)
    fmt.Printf("Type %T implements:\n", &file)
    for _, t := range ioTypes {
        if fileType.Implements(t) {
            fmt.Println(t.String())
        } else {
            fmt.Fprintln(&buf, t.String())
        }
    }
    fmt.Println()
    fmt.Println(buf.String())
}

一般要檢查接口是否實現了,不需要用到反射這麽高級的用法。

操作文件

要操作文件,首先要獲取一個os.File類型的指針值,簡稱File值。在os包中,有如下幾個函數:

  • Create
  • NewFile
  • Open
  • OpenFile

os.Create函數

用於根據給定的路徑創建一個新的文件。它會返回一個File值和一個錯誤值。可以在該函數返回的File值之上,對相應的文件進行讀操作和寫操作。使用這個函數創建的文件,對操作系統中的所有用戶來說都是可以讀和寫的。
註意,如果給定的路徑已經存在一個文件了,那麽該函數會先清空現有文件中的內容,然後再把該文件的File值返回。就是覆蓋原有文件創建一個新的空文件。另外,如果有錯誤,會通過第二個參數返回錯誤值。比如,如果路徑不存在,那麽會返回一個*os.PathErro類型的錯誤值。
下面的示例,嘗試當在前目錄下創建一個文件。還會把當前目錄的名稱截掉最後一個字符,這應該會是一個不存在的目錄,同樣嘗試創建一個文件,然後會返回一個預期的錯誤:

package main

import (
    "fmt"
    "os"
    "path/filepath"
)

func main() {
    tempPath := os.TempDir()
    fmt.Println("系統的臨時文件夾:", tempPath)

    fileName := "test.txt"
    var paths []string
    dir, _ := os.Getwd()
    dirPath := filepath.Join(dir, fileName)  // 在當前文件夾下創建一個文件
    paths = append(paths, dirPath)
    notExistsPath := filepath.Join(dir[:len(dir)-1], fileName)  // 這個文件夾路徑應該不存在
    paths = append(paths, notExistsPath)

    for _, path := range paths {
        fmt.Println("創建文件:", path)
        _, err := os.Create(path)  // 返回的第一個參數是*File,就不要的
        if err != nil {
            var underlyingErr string
            if _, ok := err.(*os.PathError); ok {
                underlyingErr = "(path error)"
            }
            fmt.Fprintf(os.Stderr, "ERROR: %v %s\n", err, underlyingErr)
            continue
        }
        fmt.Println("創建文件成功.")
    }
}

os.NewFile函數

該函數在被調用的時候需要接受一個代表文件描述符的uintptr類型的值,以及一個用於表示文件名的字符串。如果給定的不是有效的文件描述符,那麽會返回nil。否則,返回相應文件的File值。這裏不要被函數名稱誤導,它的功能不是創建一個新的文件,而是依據一個已經存在的文件的描述符,來新建一個包裝了該文件的File值。比如,可以像這樣拿到一個包裝了標準錯誤輸出的File值:

file := os.NewFile(uintptr(syscall.Stderr), "/dev/stderr")

然後,通過這個File值向標準錯誤輸出寫入一些內容,一般的效果就是打印出錯誤信息:

if file != nil {
    file.WriteString("Test Stderr.\n")
}

如果是一個已存在的文件,可以使用該文件的文件描述符作為函數的第一個參數返回File值。下面文件描述符的內容裏還有一個示例。

os.Open函數

打開一個文件並返回包裝了該文件的File值。該函數只能以只讀模式打開文件。如果調用了File值的任何寫入方法,都會返回錯誤:

package main

import (
    "fmt"
    "os"
    "path/filepath"
)

func main() {
    fileName := "test.txt"
    dir, _ := os.Getwd()
    dirPath := filepath.Join(dir, fileName)
    file, err := os.Open(dirPath)
    if err != nil {
        // 文件可能不存在,先創建一個文件
        fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
        return
    }
    _, err = file.WriteString(" ")  // 文件是只讀的,嘗試寫入會返回錯誤
    var underlyingErr string
    if _, ok := err.(*os.PathError); ok {
        underlyingErr = "(path error)"
    }
    fmt.Fprintf(os.Stderr, "ERROR: %v %s\n", err, underlyingErr)
}

文件描述符

實際上,上面說的只讀模式,正是應用在File值所持有的文件描述符之上的。
文件描述符,是由通常很小的非負整數代表的。它一般會由I/O相關的系統調用返回,並作為某個文件的一個表示存在。
從操作系統的層面看,針對任何文件的I/O操作都需要用到這個文件描述符。只不過,Go語言中的一些數據類型,為我們隱匿掉了這個描述符。實際上,在調用os.Create函數、os.Open函數以及之後會提到的os.OpenFile函數時,都會執行同一個系統調用,並且在成功之後會得到這樣一個文件描述符。這個文件描述符將會被存儲在返回的File值中。os.File類型有一個Fd的指針方法,返回一個uintptr類型的值。這個值就代表了當前的File值所持有的那個文件描述符。
不過在os包中,只有NewFile函數需要用到它。所以,如果操作的只是常規的文件或目錄,也無需特別在意。
文件描述符相關的示例:

package main

import (
    "fmt"
    "os"
    "path/filepath"
)

func main() {
    fileName := "test.txt"
    dir, _ := os.Getwd()
    dirPath := filepath.Join(dir, fileName)
    file1, err := os.Open(dirPath)
    if err != nil {
        // 文件可能不存在,先創建一個文件
        fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
        return
    }
    file2, _ := os.Open(dirPath)  // 雖然打開的是同一個文件,但卻是不同的文件描述符
    file3 := os.NewFile(file1.Fd(), dirPath)  // 可以通過文件描述符,獲取File值
    fmt.Println(file1.Fd(), file2.Fd(), file3.Fd())
}

通過Fd方法獲取到的文件描述符可以通過os.NewFile函數返回File值。

os.OpenFile函數

這個函數其實是os.Create函數和os.Open函數的底層支持,它最靈活。這個函數有3個參數:

func OpenFile(name string, flag int, perm FileMode) (*File, error) {
    testlog.Open(name)
    return openFileNolog(name, flag, perm)
}

name參數,是文件的路徑。
flag參數,是需要施加在文件描述符上的模式,叫操作模式,Open函數是只讀的就是因為在Open函數裏調用OpenFile的時候指定了該參數:

func Open(name string) (*File, error) {
    return OpenFile(name, O_RDONLY, 0)
}

perm參數,也是模式,叫權限模式。類型是os.FileMode,此類型是一個基於uint32類型的再定義類型:

type FileMode uint32

這裏的兩個模式:

  • flag : 操作模式,限定了操作文件方式
  • perm : 權限模式,控制文件的訪問權限

關於操作模式和訪問權限的更多細節,在後面繼續講。
打開文件並寫入內容的操作示例:

package main

import (
    "fmt"
    "os"
    "path/filepath"
)

func main() {
    fileName := "test.txt"
    dir, _ := os.Getwd()
    dirPath := filepath.Join(dir, fileName)
    // O_WRONLY:只寫模式。O_CREATE:文件不存在就創建。O_TRUNC:打開並清空文件
    file, err := os.OpenFile(dirPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
    if err != nil {
        fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
        return
    }
    n, err := file.WriteString("寫入操作")
    if err != nil {
        fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
    } else {
        fmt.Println("寫入字節數(bytes):", n)
    }
}

操作模式

針對File值的操作模式主要有:

  • 只讀模式:os.O_RNONLY
  • 只寫模式:os.O_WRONLY
  • 讀寫模式:os.O_RDWR

在新建一個文件的時候,必須把這三個模式中的一個設定為此文件的操作模式。
另外,還可以再設置額外的操作模式,選項如下:

  • os.O_APPEND : 寫入時追加到現有內容的後邊
  • os.O_CREARE : 當文件不存在是,創建一個新文件
  • os.O_EXCL : 需要與os.O_CREATE一同使用,表示給定的路徑不能是一個已存在的文件。(就是指定的文件必須不存在,然後創建文件)
  • os.O_SYNC : 在打開的文件之上實施同步I/O。它會保證讀寫的內容總會與硬盤上的數據保持同步
  • os.O_TRUNC : 如果文件已存在,並且是常規文件,就先清空其中的內容。(就是創建文件,如果文件存在則新建並覆蓋)

操作模式的使用

對於以上操作模式的使用,os.Open函數和os.Create函數都是現成的例子:

func Open(name string) (*File, error) {
    return OpenFile(name, O_RDONLY, 0)
}

func Create(name string) (*File, error) {
    return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

這裏順便也是下面訪問權限的例子了。
這裏可以看到,多個操作符是通過按位或操作符(|)組合起來的,常用的寫模式的組合還有:

  • os.O_WRONLY|os.O_CREATE|os.O_EXCL : 文件存在會報錯,不存在才新建
  • os.O_WRONLY|os.O_CREATE|os.O_TRUNC : 無論是否有文件存在,都會得到一個空文件,然後可以寫入新內容
  • os.O_WRONLY|os.O_APPEND : 在文件內容後面追加內容

訪問權限

os.OpenFile函數的第三個參數perm代表的是權限模式,類型是os.FileMode。實際上,os.FIleMode類型能夠代表的,不只權限模式,還可以代表文件模式,也可以稱之為文件種類。
os.FileMode是基於uint32類型的再定義類型,它包含了32個比特位。在這32個比特位中,每個比特位都有其特定的意義:

  • 如果最高位上的二進制數是1,那麽該值的文件模式等同於os.ModeDir,這代表是一個目錄。
  • 如果第26個比特位是1,那麽該值的文件模式等同於os.ModeNamedPipe,這代表是一個命名管道。
  • 最低的9個比特位,這幾位才用於表示文件的權限。這個權限參考Linux的ugo權限。

所有的常量都在源碼裏有說明:

const (
    // The single letters are the abbreviations
    // used by the String method‘s formatting.
    ModeDir        FileMode = 1 << (32 - 1 - iota) // d: is a directory
    ModeAppend                                     // a: append-only
    ModeExclusive                                  // l: exclusive use
    ModeTemporary                                  // T: temporary file; Plan 9 only
    ModeSymlink                                    // L: symbolic link
    ModeDevice                                     // D: device file
    ModeNamedPipe                                  // p: named pipe (FIFO)
    ModeSocket                                     // S: Unix domain socket
    ModeSetuid                                     // u: setuid
    ModeSetgid                                     // g: setgid
    ModeCharDevice                                 // c: Unix character device, when ModeDevice is set
    ModeSticky                                     // t: sticky
    ModeIrregular                                  // ?: non-regular file; nothing else is known about this file

    // Mask for the type bits. For regular files, none will be set.
    ModeType = ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice | ModeIrregular

    ModePerm FileMode = 0777 // Unix permission bits
)

可以像操作模式那樣用按位或操作符(|)組合起來。一般也就直接0666或0777就好了。

Go36-44,45-文件操作(os.File)