golang文件IO操作总结
Go语言中的文件与IO操作也是很丰富的,但是相关操作分散在多个包中,这里就简单总结一下
文件
这一部分主要在os包内
创建文件
func main(){
f,err:= os.Create("t.txt")
if err != nil {
log.Fatal(err)
}
defer f.Close()
}
主要使用os.Create方法,创建一个空文件,如果文件已存在,会覆盖,最后还是会得到一个空文件
修改文件大小
os.Truncate("t.txt",10)
单位是字节,如果文件不存在会创建文件。查看源码发现内部调用的实际是file的Truncate方法。
文件信息
func main(){
fileInfo, err := os.Stat("t.txt")
if err != nil {
log.Fatal(err)
}
}
主要利用stat方法,返回一个FileInfo对象,表示文件信息,个FileInfo是一个接口提供以下信息
type FileInfo interface {
Name() string // 文件名
Size() int64 // 文件大小
Mode() FileMode // 文件读写模式
ModTime() time.Time // 最近修改时间
IsDir() bool // 是否是目录
Sys() interface{} // 基础数据
}
由于stat在文件不存在时会返回错误,所以一般可以用来判断文件是否存在
移动
os.Rename("t.txt","s.txt")
既可以重命名还可以移动文件。注意如果新路径已经存在会被覆盖。当然如果旧文件不存在会报错
删除
os.Remove("s.txt")
还有一个RemoveAll方法用于删除一个路径下所有文件
打开和关闭文件
func main(){
file,err:=os.Open("t.txt")
if err != nil {
log.Fatalln(err)
}
defer file.Close()
}
默认以只读的方式打开文件,文件不存在时会报错,利用defer机制关闭文件
func main(){
file,err:=os.OpenFile("s.txt",os.O_APPEND|os.O_CREATE,0666)
if err != nil {
log.Fatalln(err)
}
defer file.Close()
}
这是一种比较开放式的方法,可以指定打开的方法以及权限。第二个参数常用的参数如下
const (
O_RDONLY int = syscall.O_RDONLY // 只读
O_WRONLY int = syscall.O_WRONLY // 只写
O_RDWR int = syscall.O_RDWR // 可读写
O_APPEND int = syscall.O_APPEND // 以追加形式写
O_CREATE int = syscall.O_CREAT // 文件不存在时创建
O_EXCL int = syscall.O_EXCL // 和O_CREATE一起使用,文件不能存在
O_SYNC int = syscall.O_SYNC // 以同步IO模式打开
O_TRUNC int = syscall.O_TRUNC // 打开时裁剪文件
)
第三个参数在只读时可以直接传0,写的时候要指定文件权限,如0666。
权限
err := os.Chmod("test.txt", 0777)
就是linux的chmod命令。使用时可以利用打开文件的错误来检查读写权限。
func main(){
os.Chmod("s.txt",0400)
file,err:=os.OpenFile("s.txt",os.O_WRONLY,0666)
if err != nil {
if os.IsPermission(err) {
fmt.Println("没有写权限")
}
}
defer file.Close()
}
改变文件属性
os.Chown("s.txt", os.Getuid(), os.Getgid()) //改变所有者
//修改最后访问和修改时间
os.Chtimes("test.txt", time.Now().Add(2*time.Minute), time.Now().Add(2*time.Minute))
读写
涉及io、os包中的许多方法
复制文件
func main(){
old,err:=os.Open("s.txt")
if err != nil {
log.Fatal(err)
}
defer old.Close()
new,err:=os.Create("s_copy.txt")
if err != nil {
log.Fatal(err)
}
defer new.Close()
len,err:=io.Copy(new,old)
if err != nil {
log.Fatal(err)
}
fmt.Printf("copied %d bytes",len)
}
Copy实际上是操作两个分别实现了读写接口的对象。
跳转到文件指定位置
func main(){
file,err:=os.Open("s.txt")
if err!=nil {
log.Fatalln(err)
}
defer file.Close()
pos,err:=file.Seek(2,0)
if err!=nil {
log.Fatalln(err)
}
fmt.Println(pos)
}
seek接受两个参数,第一个是offset,表示偏移量。第二个是whence,表示从哪里开始:0:表示文件开始位置;1:表示当前位置;2:表示文件结尾
- file.Seek(0, 1) 表示文件当前位置
- file.Seek(0, 0) 表示文件开始位置
写文件
func main(){
file,err:=os.OpenFile("s.txt",os.O_RDWR,0666)
if err != nil {
log.Fatal(err)
}
defer file.Close()
_,err=file.Write([]byte("hello world"))
if err != nil {
log.Fatal(err)
}
}
另外在ioutil包中还有一个更方便的方法
ioutil.WriteFile("s.txt",[]byte("hello"),0666)
带缓存的写
一般情况下我们可以自己定义一个buffer用来缓存要写入的数据,等数据够多时一次写入,这样可以提高IO性能,不过标准包里已经给我们封装好了
func main(){
file,err:=os.OpenFile("t.txt",os.O_CREATE|os.O_RDWR,0666)
if err!=nil {
log.Fatalln(err)
}
defer file.Close()
buffer := bufio.NewWriter(file)
len,err:=buffer.Write([]byte("hello"))
if err!=nil {
log.Fatalln(err)
}
fmt.Printf("写入 %d 字节\n", len)
len,err=buffer.WriteString("world")
if err!=nil {
log.Fatalln(err)
}
fmt.Printf("写入 %d 字节\n", len)
fmt.Printf("已缓存 %d 字节\n",buffer.Buffered())
fmt.Printf("还有 %d 字节可用\n",buffer.Available())
err=buffer.Flush()
if err!=nil {
log.Fatalln(err)
}
}
此外bufio的Writer还有一个reset方法,他接受一个实现Writer接口的对象,然后将buffer中的内容清空,后续的写入都将写到传入的对象中。
bufio的默认缓存长度为4096,可以使用bufio.NewWriterSize去指定。更多详细内容见后文源码分析
读文件
func main(){
file,err:=os.Open("t.txt")
if err != nil {
log.Fatalln(err)
}
defer file.Close()
b:=make([]byte,5)
n,err:=file.Read(b)
if err != nil {
log.Fatalln(err)
}
fmt.Printf("读了%d个字节",n)
fmt.Println(string(b))
}
read方法需要传入一个切片,由于切片长度限制所以最多可以读若干的字节
func main(){
file,err:=os.Open("t.txt")
if err != nil {
log.Fatalln(err)
}
defer file.Close()
b:=make([]byte,12)
n,err:=io.ReadFull(file,b)
if err != nil {
}
fmt.Printf("读了%d个字节",n)
fmt.Println(string(b))
}
ReadFull则是要求将切片填满,即最少读多少字节,若不够则会返回错误,但还是会读取到内容。类似的还有func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error)
,也是指定最少要读的字节数,否则会报错,但是已读内容会保留。
func main(){
file,err:=os.Open("t.txt")
if err != nil {
log.Fatalln(err)
}
defer file.Close()
data,err:=ioutil.ReadAll(file)
if err != nil {
log.Fatalln(err)
}
fmt.Println(string(data))
}
ReadAll是一次读取所有内容
func main(){
data,err:=ioutil.ReadFile("t.txt")
if err != nil {
log.Fatalln(err)
}
fmt.Println(string(data))
}
这是专门针对文件设计的一次读取全部内容
带缓存读
func main(){
file, err := os.Open("t.txt")
if err != nil {
log.Fatal(err)
}
buffer := bufio.NewReader(file)
bytes, _ := buffer.Peek(5)
fmt.Println(string(bytes))
b,_:=buffer.ReadByte()
fmt.Println(string(b))
buf := make([]byte,5)
buffer.Read(buf)
fmt.Println(string(buf))
fmt.Println(buffer.ReadString(byte('r')))
}
注意和几个方法的区别,Peek是读取指定数量的内容,但是读完指针不会变。剩余几个Read方法指针会随着读取后移,ReadString以及未提到的ReadBytes都是从当前指针位置开始读到指定位置为止。
scanner
这是bufio包中的类,它读取时比较特使是按照给定的分隔符读取的,类似于split的功能
func main(){
file, err := os.Open("t.txt")
if err != nil {
log.Fatal(err)
}
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
err = scanner.Err()
if err == nil {
fmt.Println("读取完成")
} else {
log.Fatalln(err)
}
}
上面我们按照空格分隔,循环读取,直到完成。用法的关键是Split方法的参数,他需要一个SplitFunc类型,用来指定划分方法
type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)
例子中我们用了包中定义好的方法,以空格分隔
func ScanWords(data []byte, atEOF bool) (advance int, token []byte, err error) {
// Skip leading spaces.
start := 0
for width := 0; start < len(data); start += width {
var r rune
r, width = utf8.DecodeRune(data[start:])
if !isSpace(r) {
break
}
}
// Scan until space, marking end of word.
for width, i := 0, start; i < len(data); i += width {
var r rune
r, width = utf8.DecodeRune(data[i:])
if isSpace(r) {
return i + width, data[start:i], nil
}
}
// If we're at EOF, we have a final, non-empty, non-terminated word. Return it.
if atEOF && len(data) > start {
return len(data), data[start:], nil
}
// Request more data.
return start, nil, nil
}
这个逻辑也很简单,首先跳过开头的空格,然后从第一个不是空格位置开始扫描,直到遇到下一个空格为止,返回下一次扫描时起始位置、本次扫描的有效数据。根据这个例子我们可以很简单的修改为其他分隔符功能
其他操作
打包与解包
func main(){
out,err:=os.Create("test.zip")
if err!=nil{
log.Fatalln(err)
}
defer out.Close()
zipWriter := zip.NewWriter(out)
defer zipWriter.Close()
m:=make(map[string]string)
m["t.txt"] = "aaa"
m["tt.txt"] = "bbb"
for name,body:=range m{
file,err:=zipWriter.Create(name)
if err!=nil{
log.Fatalln(err)
}
_,err=file.Write([]byte(body))
if err!=nil{
log.Fatalln(err)
}
}
}
流程很简单,就是创建一个文件,然后打开一个与该文件相关的写入对象,然后向里面写入文件,每次写入文件都要手动创建并写入内容。解包如下
func main(){
zipreader,err:=zip.OpenReader("test.zip")
if err != nil {
log.Fatal(err)
}
defer zipreader.Close()
for _,file:=range zipreader.Reader.File{
zipfile,err:=file.Open()
if err != nil {
log.Fatal(err)
}
defer zipfile.Close()
if file.FileInfo().IsDir() {
err=os.MkdirAll(file.Name, file.Mode())
if err != nil {
log.Fatal(err)
}
}else{
out,err:=os.OpenFile(file.Name,os.O_CREATE|os.O_RDWR|os.O_TRUNC,0666)
if err != nil {
log.Fatal(err)
}
defer out.Close()
_,err=io.Copy(out,zipfile)
if err != nil {
log.Fatal(err)
}
}
}
}
解包会稍微麻烦一点,先用zip包打开要解包的文件,获得一个reader对象,然后他的Reader.File字段保存着所有文件,需要先用open方法打开,然后在判断是否是文件夹,在确定正确路径,真正的解包是将遍历到的文件使用copy方法复制出来即可。
其他解压缩操作
除了提供zip类型文件操作,标准包还提供了tar,gzip,zlib, bz2, flate, lzw支持,而且都有例子,这里就不一一举例了。
临时目录及文件
ioutil还另外提供了两个与临时目录及临时文件有个的方法:TempDir()
、TempFile()
func main(){
tempDir,err:=ioutil.TempDir("","tempdir")
if err != nil {
log.Fatalln(err)
}
fmt.Println("创建临时文件夹:",tempDir)
tempfile,err:=ioutil.TempFile(tempDir,"t.txt")
if err != nil {
log.Fatalln(err)
}
fmt.Println("创建临时文件:",tempfile.Name())
defer func() {
tempfile.Close()
os.Remove(tempfile.Name())
os.Remove(tempDir)
}()
n,err:=tempfile.WriteString("hello world")
if err != nil {
log.Fatalln(err)
}
fmt.Println("写入字节数:",n)
b,err:=ioutil.ReadFile(tempfile.Name())
fmt.Println("读取字节数:",n)
fmt.Println(string(b))
}
不同的操作系统由不同的临时目录,可以通过os.TempDir()
查看
下载文件
func main(){
file,err:=os.Create("pic.jpg")
if err!=nil{
log.Fatalln(err)
}
defer file.Close()
resp,err:=http.Get("http://b.hiphotos.baidu.com/image/pic/item/908fa0ec08fa513db777cf78376d55fbb3fbd9b3.jpg")
defer resp.Body.Close()
n,err:=io.Copy(file,resp.Body)
if err!=nil{
log.Fatalln(err)
}
fmt.Println("下载字节数:",n)
}
前面说过,copy操作的是两个实现读写接口的对象,所以也可以用来下载文件。
bufio.writer源码
这个实现比较简单,我们简要看一下源码实现
构造
defaultBufSize = 4096
func NewWriter(w io.Writer) *Writer {
return NewWriterSize(w, defaultBufSize)
}
func NewWriterSize(w io.Writer, size int) *Writer {
b, ok := w.(*Writer)
if ok && len(b.buf) >= size {
return b
}
if size <= 0 {
size = defaultBufSize
}
return &Writer{
buf: make([]byte, size),
wr: w,
}
}
type Writer struct {
err error
buf []byte
n int
wr io.Writer
}
构造比较简单,最终是创建一个Writer对象,Writer中封装了一个字节切片用来缓存,还封装了我们传入的writer对象,用来最后实际写。切片的默认长度是4096。另外还有一个整数n用来记录已缓存量。
Available & Buffered
func (b *Writer) Available() int { return len(b.buf) - b.n }
func (b *Writer) Buffered() int { return b.n }
Available就是总长度减去已写入长度,Buffered就是返回已写入长度
reset
func (b *Writer) Reset(w io.Writer) {
b.err = nil
b.n = 0
b.wr = w
}
重置Writer,就是将Writer的几个成员根据情况初始化
write
func (b *Writer) Write(p []byte) (nn int, err error) {
for len(p) > b.Available() && b.err == nil {
var n int
if b.Buffered() == 0 {
n, b.err = b.wr.Write(p)
} else {
n = copy(b.buf[b.n:], p)
b.n += n
b.Flush()
}
nn += n
p = p[n:]
}
if b.err != nil {
return nn, b.err
}
n := copy(b.buf[b.n:], p)
b.n += n
nn += n
return nn, nil
}
首先考虑要写入内容大于可用空间的情况。此时,如果已缓存长度为0,表示这个缓存总长度就是0,所以所有内容直接写入指定的writer。否则,先将缓存填满,并将缓存中的内容写入writer。之后判断剩余的内容是否超过可用空间,是的话循环前面操作。如果可以写入,则直接写入,同时已写入长度标记n对应增长。
func (b *Writer) WriteByte(c byte) error {
if b.err != nil {
return b.err
}
if b.Available() <= 0 && b.Flush() != nil {
return b.err
}
b.buf[b.n] = c
b.n++
return nil
}
写入单个字符就比较简单了,只要可用空间不为0就可以写入,否则就先flush在写入
func (b *Writer) WriteString(s string) (int, error) {
nn := 0
for len(s) > b.Available() && b.err == nil {
n := copy(b.buf[b.n:], s)
b.n += n
nn += n
s = s[n:]
b.Flush()
}
if b.err != nil {
return nn, b.err
}
n := copy(b.buf[b.n:], s)
b.n += n
nn += n
return nn, nil
}
字符串本质上也是一个字节数组,和写入一个字节数组的方法类型
func (b *Writer) WriteRune(r rune) (size int, err error) {
if r < utf8.RuneSelf {
err = b.WriteByte(byte(r))
if err != nil {
return 0, err
}
return 1, nil
}
if b.err != nil {
return 0, b.err
}
n := b.Available()
if n < utf8.UTFMax {
if b.Flush(); b.err != nil {
return 0, b.err
}
n = b.Available()
if n < utf8.UTFMax {
return b.WriteString(string(r))
}
}
size = utf8.EncodeRune(b.buf[b.n:], r)
b.n += size
return size, nil
}
写入一个rune类型数据有些不同。首先判断传入的值是否仅仅是一个字节(r < utf8.RuneSelf),是的话调用WriteByte。否则,先判断可用空间是否小于UTFMax(4个字节),是的话要先将缓存内容flush,然后在判断,若还不够则转为string类型写入。若可以写入则调用EncodeRune将rune类型写入字节数组。
flush
func (b *Writer) Flush() error {
if b.err != nil {
return b.err
}
if b.n == 0 {
return nil
}
n, err := b.wr.Write(b.buf[0:b.n])
if n < b.n && err == nil {
err = io.ErrShortWrite
}
if err != nil {
if n > 0 && n < b.n {
copy(b.buf[0:b.n-n], b.buf[n:b.n])
}
b.n -= n
b.err = err
return err
}
b.n = 0
return nil
}
这就是实际写入writer的方法,首先检查写入缓存过程中是否有错,有错的话直接返回错误。之后看是否有数据要写入,有的话调用writer的write的方法一次写入一个数组,接着对具体错误做出处理即可。这里额外考虑了写入的长度不是我们给的长度的情况。另外对于有错的话,对于那些未写入的内容还要予以保留。
题图来自unsplash:https://unsplash.com/photos/salaAJW7Xdg