Go36-44,45-File Operation (os.File)

Keywords: Go Unix Linux socket Windows

os package

Through the os package, you can have the ability to manipulate the computer operating system. This code package provides platform-independent API s. Whether it is Linux, macOS, Windows, FreeBSD, OpenBSD, Plan9, os packages can provide a unified use interface. In this way, different operating systems can be manipulated in the same way, and similar results can be obtained.
The API in the OS package mainly helps us to use the file system, permission system, environment variables, system processes and system signals in the operating system. Among them, the API of file system is the most abundant. It can be used not only to create and delete files and directories, but also to obtain various information, modify content and modify access rights. Wait. Here, the most commonly used data type is os.File.

Introduction to os.File Types

Literally, the os.File type represents the files in the operating system, but in fact, it represents much more than that. For example, for Unix-like operating systems, including Linux, macOS, FreeBSD and so on, everything in them can be regarded as files.
In addition to the common forms of text files, binary files, compressed files, directories, there are symbolic links, various physical devices (including built-in or external block-or character-oriented devices), named pipes, socket s, and so on. So there are many things that can be manipulated using the os.File type. Next, however, I will focus on the application of os.File types to regular files.

Implementation of io interface

The os.File type has pointer methods, whose pointers implement interfaces in many io packages.
For the three simple interfaces io.Reader, io.Writer and io.Closer, which are the core of the IO package, the * os.File type implements them. In addition, seven of the nine extended interfaces in the IO package are also implemented. Simple interfaces io.ByteReader and io.RuneReader are not implemented, so the two extended interfaces io. ByteScanner and io.RuneScanner are not implemented.
In a word, the values of os.File type and its pointer type can not only read and write the contents of a file in various ways, but also find and set the starting index position for the next reading or writing, and can also close the file at any time. However, it can not read the next byte or Unicode character in the file, nor can it do any read-back operation. However, the ability to read the next byte or character by reading alone can also be achieved in other ways. For example, pass in the appropriate parameters using the Read method.

Implementation of Reflection Check Interface
The following example enumerates all interfaces in the io package and checks whether * os.File implements the interface:

package main

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

// ioTypes represent the reflection types of all interfaces in the io code package.
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  // Store the interface information that is not implemented, and print it out uniformly.
    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())
}

Generally, we need to check whether the interface is implemented or not, and we don't need such advanced use of reflection.

Operation file

To manipulate a file, you first need to get a pointer value of os.File type, or File value for short. In the OS package, there are several functions as follows:

  • Create
  • NewFile
  • Open
  • OpenFile

os.Create function

Used to create a new file based on a given path. It returns a File value and an error value. On top of the File value returned by this function, the corresponding files can be read and written. Files created with this function are readable and writable for all users in the operating system.
Note that if a file already exists in a given path, the function empties the contents of the existing file before returning the file's File value. That is to overwrite the original file and create a new empty file. In addition, if there is an error, the error value is returned through the second parameter. For example, if the path does not exist, an error value of type * os.PathErro is returned.
The following example tries to create a file in the previous directory. It also truncates the last character of the name of the current directory, which should be a non-existent directory. It also tries to create a file and then returns an expected error:

package main

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

func main() {
    tempPath := os.TempDir()
    fmt.Println("Temporary folder of the system:", tempPath)

    fileName := "test.txt"
    var paths []string
    dir, _ := os.Getwd()
    dirPath := filepath.Join(dir, fileName)  // Create a file under the current folder
    paths = append(paths, dirPath)
    notExistsPath := filepath.Join(dir[:len(dir)-1], fileName)  // This folder path should not exist
    paths = append(paths, notExistsPath)

    for _, path := range paths {
        fmt.Println("create a file:", path)
        _, err := os.Create(path)  // The first parameter returned is * File, which is not required.
        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("Successful File Creation.")
    }
}

os.NewFile function

When called, the function accepts a value of uintptr type representing the file descriptor and a string representing the file name. If the given file descriptor is not valid, then nil is returned. Otherwise, the File value of the corresponding file is returned. Don't be misled here by the name of the function. Its function is not to create a new file, but to create a new File value that wraps the file based on the descriptor of an existing file. For example, you can get a File value that wraps standard error output like this:

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

Then, the File value is used to write something to the standard error output. The general effect is to print out the error message.

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

If it is an existing file, you can use the file descriptor of that file as the first parameter of the function to return the File value. There is another example in the content of the file descriptor below.

os.Open function

Open a file and return the File value that wrapped the file. This function can only open files in read-only mode. If any write method of File value is called, an error is returned:

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 {
        // Files may not exist. Create a file first.
        fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
        return
    }
    _, err = file.WriteString(" ")  // The file is read-only and attempting to write returns an error.
    var underlyingErr string
    if _, ok := err.(*os.PathError); ok {
        underlyingErr = "(path error)"
    }
    fmt.Fprintf(os.Stderr, "ERROR: %v %s\n", err, underlyingErr)
}

File descriptor

In fact, the read-only mode mentioned above applies to the file descriptor held by the File value.
File descriptors are represented by usually small non-negative integers. It is generally returned by I/O-related system calls and exists as a representation of a file.
From the operating system level, I/O operations for any file need to use this file descriptor. However, some data types in the Go language hide this descriptor for us. In fact, when the os.Create function, os.Open function and the os.OpenFile function mentioned later are called, the same system call is executed and such a file descriptor is obtained after success. This file descriptor will be stored in the returned File value. The os.File type has a Fd pointer method that returns a value of uintptr type. This value represents the file descriptor held by the current File value.
However, in the os package, only NewFile functions need to use it. So, if you only operate on regular files or directories, you don't need to pay special attention.
Examples related to file descriptors:

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 {
        // Files may not exist. Create a file first.
        fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
        return
    }
    file2, _ := os.Open(dirPath)  // Although the same file is opened, it is a different file descriptor
    file3 := os.NewFile(file1.Fd(), dirPath)  // File values can be obtained from file descriptors
    fmt.Println(file1.Fd(), file2.Fd(), file3.Fd())
}

The file descriptor obtained by the Fd method can return the File value through the os.NewFile function.

os.OpenFile function

This function is actually the underlying support of os.Create function and os.Open function, which is the most flexible. This function has three parameters:

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

The name parameter is the path to the file.
The flag parameter is a mode that needs to be applied to the file descriptor, called operation mode. The Open function is read-only because the parameter is specified when calling OpenFile in the Open function:

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

The perm parameter, which is also the mode, is called the permission mode. The type is os.FileMode, which is a redefined type based on uint32:

type FileMode uint32

Here are two models:

  • flag: Operational mode, which limits the way files are operated
  • perm: permission mode to control access to files

More details about the mode of operation and access rights will be continued later.
An example of how to open a file and write to it:

package main

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

func main() {
    fileName := "test.txt"
    dir, _ := os.Getwd()
    dirPath := filepath.Join(dir, fileName)
    // O_WRONLY: Write-only mode. O_CREATE: Create a file if it does not exist. O_TRUNC: Open and empty files
    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("Write operation")
    if err != nil {
        fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
    } else {
        fmt.Println("Number of bytes written( bytes):", n)
    }
}

Operation mode

The main modes of operation for File values are:

  • Read-only mode: os.O_RNONLY
  • Write-only mode: os.O_WRONLY
  • Read-write mode: os.O_RDWR

When creating a new file, one of the three modes must be set to the mode of operation of the file.
In addition, additional modes of operation can be set as follows:

  • os.O_APPEND: Appends to the back of existing content when writing
  • os.O_CREARE: When the file does not exist, create a new file
  • os.O_EXCL: Need to be used with os.O_CREATE to indicate that a given path cannot be an existing file. (that is, the specified file must not exist, and then create the file)
  • os.O_SYNC: Implement synchronous I/O over open files. It ensures that the read and write content always keeps in sync with the data on the hard disk.
  • os.O_TRUNC: If a file already exists and is a regular file, empty it first. (Create a file, create and overwrite it if it exists)

Use of Operational Mode

The os.Open function and the os.Create function are ready-made examples for the use of the above modes of operation:

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)
}

By the way, here's an example of access rights.
As can be seen here, multiple operators are combined by bitwise OR operator (|), and common combinations of write modes are as follows:

  • os.O_WRONLY|os.O_CREATE|os.O_EXCL: The file will be error-prone if it exists, and it will not be created until it does not exist.
  • os.O_WRONLY|os.O_CREATE|os.O_TRUNC: Whether a file exists or not, an empty file is obtained and can be written to new content.
  • os.O_WRONLY|os.O_APPEND: Adding content after file content

Access right

The third parameter of the os.OpenFile function, perm, represents the permission mode, of which the type is os. FileMode. In fact, the os. FIleMode type can represent not only the permission mode, but also the file mode, which can also be called the file type.
os.FileMode is a redefined type based on uint32, which contains 32 bits. Of these 32 bits, each bit has its specific meaning:

  • If the binary number on the highest bit is 1, then the file pattern of that value is equivalent to os.ModeDir, which represents a directory.
  • If the 26th bit is 1, then the file mode of the value is equivalent to os.ModeNamedPipe, which represents a named pipe.
  • The lowest nine bits are used to represent the permissions of the file. This permission refers to the ugo permissions of Linux.

All constants are specified in the source code:

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
)

It can be combined with bitwise OR operator (|) as in operation mode. Usually it's just 0666 or 0777.

Posted by Rabioza123 on Fri, 08 Feb 2019 05:51:16 -0800