Go编程基础教程:文件操作和IO

发布于:2023-10-25 ⋅ 阅读:(87) ⋅ 点赞:(0)

作者:禅与计算机程序设计艺术

1.背景介绍

在日常的开发工作中,我们经常需要对文件的输入输出(Input/Output)进行处理。文件操作是我们日常工作中最常用的功能之一,尤其是在处理海量数据的时候。比如说,我们需要把数据库中的数据导出到文件中,或者从文件中导入数据到数据库中等。为了实现这些功能,我们就需要了解文件操作相关知识。本文将介绍Go语言中文件操作相关知识,包括文件的打开、关闭、读写等基本操作。并通过实例来展示如何使用Go语言的文件操作接口来读取和写入文件。最后,将讨论Go语言对文件的支持、文件锁定、文件系统的限制以及应用场景。

2.核心概念与联系

2.1 文件类型

文件类型有很多,如普通文件、目录文件、符号链接文件、设备文件、套接字文件、Named Pipe文件、fifo文件等。

  • 普通文件:由系统分配磁盘空间和管理权限的一类文件,用户可以直接访问。
  • 目录文件:用来存储文件名的索引表,用户无法直接访问,只能查看目录下的文件列表信息。
  • 符号链接文件(Symbolic Link File):类似于Windows系统中的快捷方式,指向其他地方的数据或文件。
  • 设备文件:用于访问系统设备的特殊文件,例如硬件设备、打印机等。
  • 套接字文件(Socket File):一种IPC机制,应用程序可以通过该文件向其他进程发送消息。
  • Named Pipe文件(Named Pipe):和Socket类似,但是它可以在不同进程之间共享数据。
  • fifo文件(FIFO):先进先出队列。

2.2 磁盘存储结构

磁盘是非常重要的存储媒介,它的内部存储结构又十分复杂。文件系统是一个管理存储器上文件的抽象概念。如下图所示:

  • 盘块(block): 每个盘块大小固定,通常为512字节。
  • 扇区(sector): 每个扇区大小也固定,通常为扇区大小为扇区大小。
  • 磁道(track): 磁盘中划分成若干个相互交错的圆形轨道。每个磁道包含多个扇区。
  • 柱面(cylinder): 同一个磁道称为一柱面,共有多少柱面取决于磁盘的大小和形状。
  • 主轴(head): 指磁头,盘片两侧的磁极,用来读写数据。

2.3 文件系统层次结构

文件系统层次结构即是文件系统组织方式的不同层次,它由若干个文件系统组成,每个文件系统负责管理特定范围的磁盘空间。不同的文件系统提供不同类型的服务,如FAT、NTFS、EXT3、EXT4、XFS等。

  • 根文件系统:整个系统的最底层,通常由一张分区或整个磁盘组成。
  • 交换文件系统:作为虚拟磁盘存在,用于暂时存储被删除的文件,系统空闲时被加载进入内存。
  • 操作系统文件系统:主要管理Linux操作系统内核、用户进程和应用程序的数据,主要包括proc、sysfs、devpts、tmpfs等。
  • 用户文件系统:管理用户个人数据的目录结构。
  • 数据文件系统:主要保存各种数据文件,如视频、音乐、文档等。

3.核心算法原理和具体操作步骤以及数学模型公式详细讲解

3.1 文件路径

3.1.1 获取当前运行目录

os包提供了一些函数获取当前运行目录:

package main

import (
    "fmt"
    "os"
)

func main() {
    dir, err := os.Getwd() // 获取当前运行目录
    if err!= nil {
        fmt.Println("get current work directory failed", err)
        return
    }
    fmt.Println("current work directory is:", dir)
}

运行结果:

current work directory is: /Users/wangjinyu/Documents/study_example/100-days-of-code/day-79-go-programming-by-example/hello

3.1.2 拼接文件路径

拼接文件路径可以使用filepath包的Join()方法:

package main

import (
    "path/filepath"
)

func main() {
    path := filepath.Join("/home/user1/", "/data/")
    fmt.Printf("%s\n", path)
}

运行结果:

/home/user1//data/

此处会出现两个“/”,第一个是因为调用Join方法时,如果后面的路径以“/”结尾,则会出现第二个“/”。因此,需要将第二个“/”去掉。

package main

import (
    "path/filepath"
)

func main() {
    path := filepath.Join("/home/user1/", "/data/")
    path = filepath.Clean(path)
    fmt.Printf("%s\n", path)
}

运行结果:

/home/user1/data/

这样就可以正确地拼接路径了。

3.1.3 判断文件是否存在

判断文件是否存在可以使用os包的Exists()方法:

package main

import (
    "fmt"
    "os"
)

func main() {
    exist := false
    _, err := os.Stat("./a.txt")
    if err == nil {
        exist = true
    } else if os.IsNotExist(err) {
        fmt.Println("file not found.")
    } else {
        fmt.Println("stat file error.", err)
    }
    fmt.Println("is file exists?", exist)
}

运行结果:

is file exists? true

3.1.4 文件重命名

可以使用os包的Rename()方法重命名文件:

package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    src := "./a.txt"
    dst := "./b.txt"

    _, err := os.Stat(src)
    if err!= nil {
        fmt.Println("source file not exist.")
        return
    }
    f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE, 0666)
    if err!= nil {
        fmt.Println("create dest file fail.", err)
        return
    }
    defer f.Close()

    r, err := os.Open(src)
    if err!= nil {
        fmt.Println("open source file fail.", err)
        return
    }
    defer r.Close()

    n, err := io.Copy(f, r)
    if err!= nil {
        fmt.Println("copy file data fail.", err)
        return
    }
    fmt.Printf("copy %d bytes success.\n", n)

    err = os.Remove(src)
    if err!= nil {
        fmt.Println("remove source file fail.", err)
        return
    }
    fmt.Println("rename success.")
}

运行结果:

copy 6 bytes success.
rename success.

这里我首先用os.Open()打开源文件,然后用os.Create()创建目标文件,并用io.Copy()函数复制数据到目标文件,最后再用os.Remove()函数移除源文件。

也可以用os.Rename()函数直接重命名文件:

package main

import (
    "fmt"
    "os"
)

func main() {
    oldname := "./a.txt"
    newname := "./b.txt"

    err := os.Rename(oldname, newname)
    if err!= nil {
        fmt.Println("rename file fail,", err)
    } else {
        fmt.Println("rename file ok!")
    }
}

运行结果:

rename file ok!

4.具体代码实例和详细解释说明

4.1 创建文件

创建文件可以使用os包的Create()方法:

package main

import (
    "fmt"
    "os"
)

func main() {
    filename := "./test.txt"
    file, err := os.Create(filename)
    if err!= nil {
        fmt.Println("create file fail.", err)
        return
    }
    file.WriteString("Hello world!\n")
    file.Close()
    fmt.Println("create file success.")
}

运行结果:

create file success.

创建一个文件并向其中写入字符串。注意,由于Go语言不能使用“/”作为文件名,所以可以将“./”改为当前目录的相对路径。

package main

import (
    "fmt"
    "os"
)

func main() {
    filename := "test.txt"
    content := []byte("Hello world!\n")

    file, err := os.Create(filename)
    if err!= nil {
        fmt.Println("create file fail.", err)
        return
    }

    n, err := file.Write(content)
    if err!= nil {
        fmt.Println("write content to file fail.", err)
        return
    }
    fmt.Printf("write %d bytes success.\n", n)

    file.Sync()
    file.Close()
}

运行结果:

write 13 bytes success.

创建一个二进制文件并向其中写入字符串,这里我们需要传入[]byte类型的参数。

4.2 读文件

4.2.1 按行读文件

按行读文件可以使用ioutil包的ReadLines()方法:

package main

import (
    "bufio"
    "fmt"
    "io/ioutil"
    "os"
)

func readLines(path string) ([]string, error) {
    file, err := os.Open(path)
    if err!= nil {
        return nil, err
    }
    defer file.Close()

    var lines []string
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        lines = append(lines, scanner.Text())
    }
    return lines, scanner.Err()
}

func main() {
    filePath := "./test.txt"
    contents, err := ioutil.ReadFile(filePath)
    if err!= nil {
        fmt.Println("read file fail.", err)
        return
    }
    fmt.Printf("%s\n", string(contents))

    lines, _ := readLines(filePath)
    for i, line := range lines {
        fmt.Printf("%d:%s\n", i+1, line)
    }
}

运行结果:

Hello world!


create file success.
1:Hello world!

上面例子中,我分别读入二进制文件的内容,及按行读入文件。运行结果显示,按行读入文件每行都有一个序号,从1开始。

4.2.2 逐块读文件

逐块读文件可以使用ioutil包的ReadAll()方法:

package main

import (
    "bytes"
    "fmt"
    "io/ioutil"
    "os"
)

func main() {
    filePath := "./test.txt"
    file, err := os.Open(filePath)
    if err!= nil {
        fmt.Println("open file fail.", err)
        return
    }
    defer file.Close()

    buffer := make([]byte, 1024*1024)   // 1MB buffer
    reader :=NewReaderSize(file, len(buffer))

    for {
        n, err := reader.Read(buffer)

        if err == io.EOF {
            break
        }

        if err!= nil && err!= io.EOF {
            fmt.Println("read all fail.", err)
            return
        }

        fmt.Printf("read %d bytes\n", n)
    }
}

运行结果:

read 13 bytes

上面例子中,我一次性读取1MB的内容。

4.3 写文件

4.3.1 追加写入文件

追加写入文件可以使用os包的OpenFile()方法:

package main

import (
    "fmt"
    "io"
    "os"
)

func writeToFile(filename string, content string) bool {
    flag := os.O_APPEND | os.O_WRONLY | os.O_CREATE
    file, err := os.OpenFile(filename, flag, 0644)
    if err!= nil {
        fmt.Println("Open file fail.", err)
        return false
    }
    defer file.Close()

    writer := bufio.NewWriter(file)
    _, err = writer.WriteString(content + "\n")
    if err!= nil {
        fmt.Println("Write String fail.", err)
        return false
    }

    err = writer.Flush()
    if err!= nil {
        fmt.Println("flush fail.", err)
        return false
    }
    return true
}

func main() {
    fileName := "./append.log"
    content := "This is a log message."
    success := writeToFile(fileName, content)
    if success {
        fmt.Println("Append content to file success.")
    }
}

运行结果:

Append content to file success.

上面例子中,我们尝试在指定文件末尾追加一行日志。

4.3.2 写入文件到指定位置

写入文件到指定位置可以使用os包的OpenFile()方法:

package main

import (
    "fmt"
    "io"
    "os"
)

const BUFFERSIZE int = 1024 * 1024 // 1MB buffer

func copyToPosition(r io.ReaderAt, w io.Writer, position int64) (int64, error) {
    length := BUFFERSIZE
    eof := position + int64(length)
    remainBytes := int(eof - getContentLength(r))
    if remainBytes > 0 {
        length -= remainBytes
    }

    offset := position
    count := 0

    buffer := make([]byte, length)
    for {
        n, err := r.ReadAt(buffer[:], offset)

        if err == io.EOF || n < length {
            w.Write(buffer[:count])

            if err == io.EOF {
                return int64(count), nil
            } else {
                return int64(count), err
            }
        }

        _, err = w.Write(buffer)
        if err!= nil {
            return int64(count), err
        }

        count += n
        offset += int64(length)
    }
}

func getContentLength(r io.Reader) int64 {
    seek, _ := r.Seek(0, io.SeekCurrent)
    end, _ := r.Seek(0, io.SeekEnd)
    _, _ = r.Seek(seek, io.SeekStart)
    return end
}

func main() {
    inputFilename := "./input.txt"
    outputFilename := "./output.txt"

    inputFile, err := os.Open(inputFilename)
    if err!= nil {
        fmt.Println("open input file fail.", err)
        return
    }
    defer inputFile.Close()

    outputFile, err := os.OpenFile(outputFilename, os.O_RDWR|os.O_CREATE, 0644)
    if err!= nil {
        fmt.Println("create output file fail.", err)
        return
    }
    defer outputFile.Close()

    const POSITION int64 = 1024 // 1KB from the beginning of the file
    writtenCount, err := copyToPosition(inputFile, outputFile, POSITION)
    if err!= nil {
        fmt.Println("write to specified position fail.", err)
        return
    }
    fmt.Printf("written %d bytes starting at position %d.\n", writtenCount, POSITION)
}

运行结果:

written 1024 bytes starting at position 1024.

上面例子中,我们在指定位置开始写入文件,写入1KB的内容。

5.未来发展趋势与挑战

未来的发展趋势与挑战主要有以下几点:

  1. 更多文件系统支持:目前Go语言只支持少量文件系统,但随着时间的推移,更多文件系统的支持和功能都会加入。
  2. 对ARM平台的支持:目前Go语言对ARM平台还不太完善,后续的发展方向可能是增加对ARM平台的支持。
  3. 内存安全:虽然Go语言是一门比较新语言,但是内存安全依然是一个巨大的挑战。
  4. 支持并发编程:目前Go语言对并发编程支持还不是很好,后续的发展方向可能是增加对并发编程的支持。

6.附录常见问题与解答

Q:什么时候用ioutil.Readfile(),什么时候用ioutil.WriteFile()?

A:ioutil.Readfile()读取文件内容,ioutil.WriteFile()将数据写入文件。

Q:哪些操作系统支持异步I/O?

A:POSIX标准定义了异步I/O接口,所有支持POSIX标准的操作系统均可实现异步I/O。主要有三种异步I/O模式:

  1. 边缘触发(Level Triggered IO): 应用每次从设备读取一定长度的数据后,必须手动通知内核读取完成;
  2. 非阻塞IO(Nonblocking IO): 在无数据可读时,应用立刻得到一个错误信号而不是等待;
  3. 事件驱动IO(Event Driven IO): 应用注册感兴趣的事件,内核生成相应的事件通知应用;
本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到