作者:禅与计算机程序设计艺术
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.未来发展趋势与挑战
未来的发展趋势与挑战主要有以下几点:
- 更多文件系统支持:目前Go语言只支持少量文件系统,但随着时间的推移,更多文件系统的支持和功能都会加入。
- 对ARM平台的支持:目前Go语言对ARM平台还不太完善,后续的发展方向可能是增加对ARM平台的支持。
- 内存安全:虽然Go语言是一门比较新语言,但是内存安全依然是一个巨大的挑战。
- 支持并发编程:目前Go语言对并发编程支持还不是很好,后续的发展方向可能是增加对并发编程的支持。
6.附录常见问题与解答
Q:什么时候用ioutil.Readfile(),什么时候用ioutil.WriteFile()?
A:ioutil.Readfile()读取文件内容,ioutil.WriteFile()将数据写入文件。
Q:哪些操作系统支持异步I/O?
A:POSIX标准定义了异步I/O接口,所有支持POSIX标准的操作系统均可实现异步I/O。主要有三种异步I/O模式:
- 边缘触发(Level Triggered IO): 应用每次从设备读取一定长度的数据后,必须手动通知内核读取完成;
- 非阻塞IO(Nonblocking IO): 在无数据可读时,应用立刻得到一个错误信号而不是等待;
- 事件驱动IO(Event Driven IO): 应用注册感兴趣的事件,内核生成相应的事件通知应用;