Jochen的goland小抄-进阶篇-File

目录

小抄系列进阶篇涉及的概念较多,知识点重要,故每块知识点独立成篇,方便日后笔记的查询

文件操作是必须要点满的技能,太重要了,开发必定会用到,学就完事

本篇的主题是:文件操作

文件

go语言的文件类(结构体+方法)定义在os包中,在其中封装了底层的文件描述符、文件相关信息和读写文件的方法

如下工程目录下除了示例代码文件外,还有用于测试文件操作的test.txt文件

.
├── file1.go
└── test.txt

#text.txt内容:
goland是世界上最好的语言!

获取文件信息

go语言定义了一个名为FileInfo接口,对外提供了获取文件相关信息的方法

// A FileInfo describes a file and is returned by Stat and Lstat.
type FileInfo interface {
   Name() string       // 文件名.拓展名
   Size() int64        // 文件大小
   Mode() FileMode     // 文件权限 rwxrwxrwx  r->可读 w->可写 x->可执行 从左至右三个一组,分别为文件拥有者 组 其他用户
   ModTime() time.Time // 文件最后一次修改时间
   IsDir() bool        // 是否为文件夹
   Sys() interface{}   // 基础数据源接口 (can return nil)
}

ps:文件权限可以用八进制表示:

r -> 004
w -> 002
x -> 001
- -> 000

go语言给权限设定了很多常量,但是习惯上可以使用二进制数值表示文件的权限如0777


有了接口,那么如何获取实现接口的文件对象呢?

go语言对外暴露的两个获取FileInfo类型对象的方法

  • func Stat

    func Stat(name string) (fi FileInfo, err error)
    

    Stat返回一个描述name指定的文件对象的FileInfo。如果指定的文件对象是一个符号链接,返回的FileInfo描述该符号链接指向的文件的信息,本函数会尝试跳转该链接。如果出错,返回的错误值为*PathError类型

  • func Lstat

    func Lstat(name string) (fi FileInfo, err error)
    

    Lstat返回一个描述name指定的文件对象的FileInfo。如果指定的文件对象是一个符号链接,返回的FileInfo描述该符号链接的信息,本函数不会试图跳转该链接。如果出错,返回的错误值为*PathError类型

代码实操:

package main

import (
	"fmt"
	"os"
)

func main() {
	/*
		通过 FileInfo接口 获取文件信息
	*/
	//1.获取文件对象
	fileInfo, err := os.Stat("/home/GoWorkSpace/src/fileTest/test.txt")
	//fileInfo,err := os.Stat("/home/GoWorkSpace/src/fileTest/test1.txt") //不存在的文件
	if err != nil {
		fmt.Println(err) //stat /home/GoWorkSpace/src/fileTest/test1.txt: no such file or directory
		return
	}
	fmt.Printf("%T\n", fileInfo) //*os.fileStat

	//2.获取文件名
	fileName := fileInfo.Name()
	fmt.Printf("%T, %v\n", fileName, fileName) //string, test.txt

	//3.获取文件大小
	fmt.Printf("%T, %v\n", fileInfo.Size(), fileInfo.Size()) //int64, 36

	//4.判断是否为文件夹
	fmt.Printf("%T, %v\n", fileInfo.IsDir(), fileInfo.IsDir()) //bool, false

	//5.获取最后修改时间
	fmt.Printf("%T, %v\n", fileInfo.ModTime(), fileInfo.ModTime()) //bool, false

	//6.获取文件读写权限
	fmt.Printf("%T, %v\n", fileInfo.Mode(), fileInfo.Mode()) //os.FileMode, -rw-r--r--

}

文件路径

获取文件路径的信息和操作定义在path和path/filepath包中

package main

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

func main() {
	/*
		路径:
			相对路径:relative
				text.txt  相对于当前工程
			绝对路径:absolute
				/home/GoWorkSpace/src/fileTest/test1.txt
			. 当前路径
			.. 上一层
	*/
	//1.判断是为绝对路径
	relative := "text.txt"                                 //相对路径
	absolute := "/home/GoWorkSpace/src/fileTest/test1.txt" //绝对路径
	fmt.Println(filepath.IsAbs(relative))                  //false
	fmt.Println(filepath.IsAbs(absolute))                  //true

	//2.获取文件绝对路径(主要是提供相对路径获取绝对路径,绝对路径获取绝对路径意义不大)
	fmt.Println(filepath.Abs(relative)) // /home/GoWorkSpace/src/fileTest/text.txt <nil>

	//3.获取文件的父目录 下面两种方式都可以
	var parentDir = path.Join(absolute, "..")
	fmt.Println(parentDir)                    ///home/GoWorkSpace/src/fileTest
	fmt.Println(path.Dir(absolute))           // /home/GoWorkSpace/src/fileTest
	fmt.Println(path.Dir(path.Dir(absolute))) // /home/GoWorkSpace/src

	//4.获取文件名
	fmt.Println(path.Base(absolute)) //test1.txt

}

File操作

操作文件或目录的函数也定义在os包下

创建文件/文件夹

package main

import (
	"fmt"
	"os"
)

func main() {
	/*
			1.创建文件夹:
				如果文件存在则创建失败
				os.Mkdir()    创建一层文件
				os.MkdirAll() 创建多层文件夹

			2.创建文件
				Create采用666模式(任何人可读写但不可执行),创建一个指定名称的文件,如果文件一存在会覆盖它(为空文件)
				os.Create() 创建文件,可用相对路径也可以用绝对路径

		ps:注意:Create创建的文件返回的文件对象对应的文件描述符为O_RDONLY模式,即只能进行读操作
	*/

	//1.创建目录
	//1.1、os.Mkdir() 需要传入两个参数:1.创建的文件夹完全名 2.文件夹权限
	err1 := os.Mkdir("/home/GoWorkSpace/src/fileTest/dir", os.ModePerm) //os.ModePerm常量为777
	if err1 != nil {
		fmt.Println(err1) //重复执行报错:mkdir /home/GoWorkSpace/src/fileTest/dir: file exists
	} else {
		fmt.Println("创建成功")
	} //ps:Mkdir只能创建一层文件夹
	//1.2、当想连续创建多层子目录的时候,则需要使用os.MkdirAll()方法,传入参数和上面一致
	err2 := os.MkdirAll("/home/GoWorkSpace/src/fileTest/dir1/dir2/dir", os.ModePerm)
	if err2 != nil {
		fmt.Println(err2) //重复执行报错:mkdir /home/GoWorkSpace/src/fileTest/dir: file exists
	} else {
		fmt.Println("递归创建我目录成功")
	}

	//2.创建文件
	//Create采用666模式(任何人可读写但不可执行),创建一个指定名称的文件,如果文件一存在会覆盖它(为空文件)
	file1, err3 := os.Create("/home/GoWorkSpace/src/fileTest/test2.txt")
	if err3 != nil {
		fmt.Println(err3) //如果创建的文件路径不存在则报错
		//创建已经存在的同名文件,会覆盖掉旧的文件
	} else {
		fmt.Printf("%T\n", file1) //*os.File 返回的是文件对象指针
		fmt.Println(file1)        //&{0xc000050180}
	}
	//也可以用相对路径创建文件(相对与当前工程)
	file2, err4 := os.Create("test3.txt")
	if err4 != nil {
		fmt.Println(err4)
	} else {
		fmt.Printf("%T\n", file2) //*os.File 返回的是文件对象指针
		fmt.Println(file2)        //&{0xc0000501e0}
	}
	//ps:注意:Create创建的文件返回的文件对象对应的文件描述符为O_RDONLY模式,即只能进行读操作
}

删除文件/文件夹

示例代码延续上面创建的文件和目录

package main

import (
	"fmt"
	"os"
)

func main() {
	/*
		删除文件或文件夹
			os.Remove() 删除空目录或文件
			os.RemoveAll()	删除所有
	*/
	//os.Remove既可以删除文件,也可以删除目录
	//删除文件
	err1 := os.Remove("/home/GoWorkSpace/src/fileTest/test2.txt") //删除一个文件
	if err1 != nil {
		//重复删除报错
		fmt.Println(err1) //remove /home/GoWorkSpace/src/fileTest/test2.txt: no such file or directory
	} else {
		fmt.Println("文件删除成功")
	}

	//删除空目录
	err2 := os.Remove("/home/GoWorkSpace/src/fileTest/dir")
	//err2 := os.Remove("/home/GoWorkSpace/src/fileTest/dir1")
	if err2 != nil {
		//如果 1.目录里面有文件或文件夹  2.目录不存在 则报错
		fmt.Println(err2) //remove /home/GoWorkSpace/src/fileTest/dir1: directory not empty
	} else {
		fmt.Println("目录删除成功")
	}
	//递归删除目录和目录下所有东西
	err3 := os.RemoveAll("/home/GoWorkSpace/src/fileTest/dir1")
	if err3 != nil {
		fmt.Println(err3)

	} else {
		fmt.Println("递归删除目录成功")
	}
}

打开文件

打开文件实际就是上当前程序和指定的文件之间建立一个连接,使得程序可以去愉快的"玩弄"这个文件,例如读写

因为是一个连接,所以要记住,骚操作完后要去关闭连接,节省资源,避免内存泄漏

学习过别的语言的文件操作应该很熟悉,打开一个文件是要提供对应的打开模式的(是只写、只读、可读可写还是追加等等)

对于打开文件的模式,go语言提供了一组常量供我们使用(源码注解还是挺见名知意的,就不全部翻译了):

const (
   // Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified.
   O_RDONLY int = syscall.O_RDONLY // open the file read-only.
   O_WRONLY int = syscall.O_WRONLY // open the file write-only.
   O_RDWR   int = syscall.O_RDWR   // open the file read-write.
   // The remaining values may be or'ed in to control behavior.
   O_APPEND int = syscall.O_APPEND // append data to the file when writing.
   O_CREATE int = syscall.O_CREAT  // create a new file if none exists.
   O_EXCL   int = syscall.O_EXCL   // 和O_CREATE搭配使用,文件必须不存在
   O_SYNC   int = syscall.O_SYNC   // 打开文件用于同步I/O.
   O_TRUNC  int = syscall.O_TRUNC  // 如果可能,打开时清空文件.
)

还是通过代码示例,看下如果打开文件

package main

import (
	"fmt"
	"os"
)

func main() {
	/*
		1.打开文件:程序和指定文件建立连接
			os.Open():打开文件,只有读的权限(文件描述符为有O_RDONLY模式)
			os.OpenFile():指定模式打开文件,需要传入打开文件的模式,可随即应对所有情况
				第一个参数:文件名
				第二个参数:文件的打开方式
				第三个参数:文件的权限,文件不存在则创建文件,该文件需要赋予权限

		2.关闭文件:程序和文件之间断开连接
			file.Close()
	*/

	//1.打开文件
	file1, err1 := os.Open("test.txt") //可以写相对路径也可以使用绝对路径
	if err1 != nil {
		fmt.Println(err1)
	} else {
		fmt.Printf("%T\n", file1) //*os.File 返回的文件对象指针,只有写权限
		fmt.Println(file1)        //&{0xc000104120}
	}
	//指定模式打开文件
	file2, err2 := os.OpenFile("test.txt", os.O_RDONLY|os.O_WRONLY, os.ModePerm) //读写模式打开文件,后面的权限参数主要用于文件不存在时,创建文件后赋予的权限使用
	if err2 != nil {
		fmt.Println(err2)
	} else {
		fmt.Printf("%T\n", file2) //*os.File 返回的文件对象指针,只有写权限
		fmt.Println(file2)        //&{0xc000104180}

	}

	//2.关闭文件:打开文件操作完后记得要去关闭文件,可以通过defer函数统一去关闭所有的文件对象,避免内存泄漏
	defer file1.Close()
	defer file2.Close()

}

文件读写

读写文件即磁盘I/O操作,首先需要认识下什么是I/O

I/O操作

I/O操作即输入输出操作。用于读写数据

输入和输出是相对来说的,如果以程序读写文件或远程网络来说(主体是程序),读取文件或从远程网络获取数据,就是数据输入的过程;如果数据在程序产生要用于持久化到文件或者上传到网络上,就是数据输出的过程

在某些语言中,I/O操作也叫做流操作,指的是数据通信的通道,可以把流理解成是对程序外界交换数据的一种抽象

go语言的io操作的相关api定义在io包中

io包中只是定义了一系列I/O操作的接口和封装了一些底层实现,具体设备的io操作实现分别定义与各自的包中,如文件的I/O操作(磁盘I/O)即文件的读写操作具体实现就定义在os包下

在io包中,其中有两个最为重要的接口:Reader和Writer接口

Reader接口

Reader接口定义如下:

type Reader interface {
   Read(p []byte) (n int, err error)
}

接口中只定义了Read()方法,其用于读取数据

  • 参数p是byte类型的切片,Read方法读取到的数据就存储到p当中

  • 返回值n表示读取数据的字节数(0<=n<=len(p))

  • 当Read遇到错误时或 EOF(到达末尾),返回的err!=niln=0

  • os包下的File结构就提供了I/O操作,其实现了Reader方法,用于读取文件

Writer接口

Writer接口定义如下:

type Writer interface {
   Write(p []byte) (n int, err error)
}

官方文档中关于该接口方法的说明:

Write 将 len(p) 个字节从 p 中写入到基本数据流中。它返回从 p 中被写入的字节数 n(0 <= n <= len(p))以及任何遇到的引起写入提前停止的错误。若 Write 返回的 n < len(p),它就必须返回一个 非nil 的错误。

实现了Write方法的类型都实现了 io.Writer 接口

Seeker接口

接口定义如下:

type Seeker interface {
    Seek(offset int64, whence int) (ret int64, err error)
}

官方文档中关于该接口方法的说明:

Seek 设置下一次 Read 或 Write 的偏移量为 offset,它的解释取决于 whence: 0 表示相对于文件的起始处,1 表示相对于当前的偏移,而 2 表示相对于其结尾处。 Seek 返回新的偏移量和一个错误,如果有的话。

whence 的值,在 io 包中定义了相应的常量,应该使用这些常量

const (
  SeekStart   = 0 // seek relative to the origin of the file
  SeekCurrent = 1 // seek relative to the current offset
  SeekEnd     = 2 // seek relative to the end
)

读取文件

读取文件是通常通过File类实现的Reader接口中的Read方法而实现,其定义如下:

func (f *File) Read(b []byte) (n int, err error) {
   if err := f.checkValid("read"); err != nil {
      return 0, err
   }
   n, e := f.read(b)
   return n, f.wrapErr("read", e)

下面通过示例演示文件的读操作:
有工程目录下有test.txt文件,在其中写着goland是世界上最好的语言!

package main

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

func main() {
	/*
	读取文件:
		Read接口:
			Read(p []byte) (n int, err error)
	*/
	//读取本地test.txt的数据
	//step1:打开文件(获取File对象)
	fileName := "/home/GoWorkSpace/src/fileTest/test.txt"
	file, err := os.Open(fileName) //和本地文件建立连接,可以读写数据了
	if err != nil{
		fmt.Println(err)
		return
	}

	//step3:关闭文件(这第三步在go语言中一般提前使用defer去执行)
	defer file.Close() //断开连接

	//step2:读取数据
	bs := make([]byte,24,24) //Read方法读取到的数据存储在bs字节切片中
	//第一次读取
	n, err2 := file.Read(bs) //n为读取到的字符数,err2为返回的错误
	fmt.Printf("读取的字符数:%d\n",n)
	fmt.Println("错误信息为",err2)
	fmt.Println("读取到的数据为:",bs)
	fmt.Printf("读取到的数据为:%s\n",bs) //以utf8格式现实字符 也可以使用string(bs)进行强转
	/*
	输出:
		读取的字符数:24
		错误信息为 <nil>
		读取到的数据为: [103 111 108 97 110 100 230 152 175 228 184 150 231 149 140 228 184 138 230 156 128 229 165 189]
		读取到的数据为:goland是世界上最好

	*/
	//第二次读取
	n, err3 := file.Read(bs)
	fmt.Printf("读取的字符数:%d\n",n)
	fmt.Println("错误信息为",err3)
	fmt.Println("读取到的数据为:",bs)
	fmt.Printf("读取到的数据为:%s\n",bs) //以utf8格式现实字符 也可以使用string(bs)进行强转
	/*
	输出:
		读取的字符数:12
		错误信息为 <nil>
		读取到的数据为: [231 154 132 232 175 173 232 168 128 239 188 129 231 149 140 228 184 138 230 156 128 229 165 189]
		读取到的数据为:的语言!界上最好

	光标继上一次读取到位置继续往下读取内容
	取实际上读取到 "的语言!" 后,文件已经读取完毕了,后面的字符是之前上一次读取留下的
	即再次读取,如果还有数据未读完,会重新从字节切片的开头填充内容

	*/
	//第三此读取
	n, err4 := file.Read(bs)
	fmt.Printf("读取的字符数:%d\n",n)
	fmt.Println("错误信息为",err4)
	fmt.Println("读取到的数据为:",bs)
	fmt.Println("读取到的数据为:",string(bs))
	/*
	输出:
		读取的字符数:0
		错误信息为 EOF
		读取到的数据为: [231 154 132 232 175 173 232 168 128 239 188 129 231 149 140 228 184 138 230 156 128 229 165 189]
		读取到的数据为:的语言!界上最好
	上一次已经读取完所以内容,再次调用读取方法,err返回EOF,读取的字符数为0

	*/

	//平时写也不会那么呆瓜一次一次的自己去读取,而是通过循环,判断err是否到达末尾作为终止条件
	//下面是一般的标准写法
	println("--------------------------------")
	file2, err := os.Open(fileName)
	defer file2.Close()
	bs2 := make([]byte,1024,1024) //字节数组作为读取缓冲区,一般设置为1024
	n1 := -1 //返回读取的数量,输出化为-1
	for {
		n1,err = file2.Read(bs2)
		if n1 == 0 || err == io.EOF {
			fmt.Println("读取到了文件的末尾,结束读取操作")
			break
		}
		fmt.Println(string(bs2[:n1]))  //打印的数据应该是读取了多少打印了多少
	}
	/*
	输出:
		goland是世界上最好的语言!
		读取到了文件的末尾,结束读取操作
	*/

	//除了Read方法读取,还有ReaderAt和ReaderFrom
	//ReaderAt方法用于指定位置去读取(Reader是从头开始读)
	fmt.Println(file.ReadAt(bs,2)) //从第二个字节(不是字符)位置开始读
	fmt.Println(string(bs)) //land是世界上最好�

	//ReaderFrom() 从io对象读取数据
	//下面的例子简单的实现将文件中的数据全部读取(显示在标准输出):
	writer := bufio.NewWriter(os.Stdout)
	writer.ReadFrom(file)
	writer.Flush()
	/*
		ReadFrom 从 r 中读取数据,直到 EOF 或发生错误。
		其返回值 n 为读取的字节数。除 io.EOF 之外,在读取过程中遇到的任何错误也将被返回。

		我们可以通过 ioutil 包的 ReadFile 函数获取文件全部内容。
		其实,跟踪一下 ioutil.ReadFile 的源码,会发现其实也是通过 ReadFrom 方法实现(用的是 bytes.Buffer,它实现了 ReaderFrom 接口)
	
		如果不通过 ReadFrom 接口来做这件事,而是使用 io.Reader 接口,我们有两种思路:
			1.先获取文件的大小(File 的 Stat 方法),之后定义一个该大小的 []byte,通过 Read 一次性读取
			2.定义一个小的 []byte,不断的调用 Read 方法直到遇到 EOF,将所有读取到的 []byte 连接到一起


	*/
}

写文件

读取文件通常是通过File类实现的Writer接口中的Write方法而实现,其定义如下:

// Write writes len(b) bytes to the File.
// It returns the number of bytes written and an error, if any.
// Write returns a non-nil error when n != len(b).
func (f *File) Write(b []byte) (n int, err error) {
   if err := f.checkValid("write"); err != nil {
      return 0, err
   }
   n, e := f.write(b)
   if n < 0 {
      n = 0
   }
   if n != len(b) {
      err = io.ErrShortWrite
   }

   epipecheck(f, e)

   if e != nil {
      err = f.wrapErr("write", e)
   }

   return n, err
}

还是通过代码示例直观演示:
在工程目录下创建writeFile.txt文件

package main

import (
	"fmt"
	"os"
)

func main() {
	/*
		写文件
	*/

	fileName := "/home/GoWorkSpace/src/fileTest/writeFile.txt" 	//所要写文件的fullName

	//step1:打开文件
	//file, err := os.Open(fileName) //Open只能通过只读的方式打开文件,不能实现文件不存在时创建文件
	file, err := os.OpenFile(fileName,os.O_RDWR|os.O_CREATE,os.ModePerm)
	if err != nil{
		fmt.Println(err)
		return
	}

	//step3:关闭文件
	defer file.Close()

	//step2:写数据
	//写数据,会覆盖原有的数据,因为我们是通过O_RDWR模式打开的文件,如果想追加末尾写数据需要使用O_APPEND模式打开
	bs := []byte{'J','o','c','h','e','n','\n'} //byte<->uint8
	n,err := file.Write(bs)
	fmt.Printf("写进文件%d个字节\n",n)
	fmt.Println(err)

	//获取文件的字节数量,创建相应大小的缓冲区,一次性读取
	info,_ := file.Stat()
	rbs := make([]byte,info.Size(),info.Size())
	rn, rerr := file.ReadAt(rbs,0) //写入数据光标会停留在文件末尾,所以使用ReadAt方法指定光标位置读取
	fmt.Println("读取的字节数为:",rn)
	fmt.Println(rerr)
	fmt.Println("读取的数据为:",string(rbs))

	//写文件除了Write方法
	//1.还可以使用StringWriter接口定义的WriteString方法该方法可以直接写入字符串,不用搞上面的字节切片那么麻烦
	n2, err2 := file.WriteString("写的好爽啊哈哈哈\n")
	fmt.Println(n2)
	fmt.Println(err2)
	//用write写直接写字符串也可以,因为字符串是可以转成字节切片的
	n3, err3 := file.Write([]byte("写的也还算舒服\n"))
	fmt.Println(n3)
	fmt.Println(err3)

	//2.还可以使用WriteAt方法,作用和ReadAt一毛一样,就是指定光标位置去写
	n4, err4 := file.WriteAt([]byte("指定位置写\n"),0) //重头开始写
	fmt.Println(n4)
	fmt.Println(err4)

}

本系列学习资料参考:
https://www.bilibili.com/video/BV1jJ411c7s3?p=15
https://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter01/01.1.html

上一篇:Jochen的goland小抄-进阶篇-time包


下一篇:最新破解JetBrains全家桶:Pycharm,Idea,Goland,WebStorm,PHP