首页 > 技术文章 > Jochen的golang小抄-进阶篇-File

deehuang 2021-02-22 01:46 原文

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

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

本篇的主题是:文件操作

文件

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

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

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

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

获取文件信息

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文件,在其中写着golang是世界上最好的语言!

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]
		读取到的数据为:golang是世界上最好

	*/
	//第二次读取
	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]))  //打印的数据应该是读取了多少打印了多少
	}
	/*
	输出:
		golang是世界上最好的语言!
		读取到了文件的末尾,结束读取操作
	*/

	//除了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)

}

复制文件

复制文件是指

  • 将A文件的内容复制到B文件(A称为源文件,B称为目标文件)
  • 原理就是通过程序从文件A读数据,然后写入B文件中
  • 如果数据量小可以一次读取一次写入(上面的例子有用到过),数量大(如图片,电影等大文件)则采用边读边写的方式去做

实现示例:

  • 方式一:通过Write和Reader,按块去复制文件

    package main
    
    import (
       "fmt"
       "io"
       "os"
    )
    
    func main() {
       /*
          拷贝文件
       */
       srcFile := "/home/GoWorkSpace/src/fileTest/test.txt"      //源文件
       destFile := "/home/GoWorkSpace/src/fileTest/destFile.txt" //目标文件
    
       src, err1 := os.Open(srcFile)
       if err1 != nil {
          fmt.Println(err1)
          return
       }
    
       dest, err2 := os.OpenFile(destFile, os.O_RDWR|os.O_CREATE, os.ModePerm)
       if err2 != nil {
          fmt.Println(err2)
          return
       }
       fmt.Println()
    
       defer src.Close()
       defer dest.Close()
    
       //读写
       bs := make([]byte, 1024, 1024)
       total := 0 //总数据量
       //循环把实现边读边写
       for {
          n, err := src.Read(bs)
          if n == 0 || err == io.EOF{
             fmt.Println("读完")
             break
          }else if err != nil{
             fmt.Println("读取的时候发生错误:",err)
          }
          //将读到的数据写如dest中
          n, err = dest.Write(bs[:n]) //不是写入bs的全部数据,而是本次读到多少数据
          fmt.Printf("写了 %d 条数据\n", n)
          fmt.Println(err)
          total += n
       }
    }
    
  • 方式二(推荐,性能更好):通过io包下的Copy方法实现
    方法定义如下:

    func Copy(dst Writer, src Reader) (written int64, err error)
    

    官方注解:

    将src的数据拷贝到dst,直到在src上到达EOF或发生错误。返回拷贝的字节数和遇到的第一个错误。

    对成功的调用,返回值err为nil而非EOF,因为Copy定义为从src读取直到EOF,它不会将读取到EOF视为应报告的错误。如果src实现了WriterTo接口,本函数会调用src.WriteTo(dst)进行拷贝;否则如果dst实现了ReaderFrom接口,本函数会调用dst.ReadFrom(src)进行拷贝。

    package main
    
    import (
       "fmt"
       "io"
       "os"
    )
    
    func main() {
       /*
          拷贝文件
       */
       srcFile := "/home/GoWorkSpace/src/fileTest/test.txt"      //源文件
       destFile := "/home/GoWorkSpace/src/fileTest/destFile.txt" //目标文件
    
       src, err1 := os.Open(srcFile)
       if err1 != nil {
          fmt.Println(err1)
          return
       }
    
       dest, err2 := os.OpenFile(destFile, os.O_RDWR|os.O_CREATE, os.ModePerm)
       if err2 != nil {
          fmt.Println(err2)
          return
       }
       fmt.Println()
    
       defer src.Close()
       defer dest.Close()
    
       //Copy 参数1为目标对象, 参数2为源对象
       n ,err := io.Copy(dest,src)
       if err != nil{
          fmt.Println(err)
          return
       }
       fmt.Println("写入的数据量:",n)
       
    
    }
    

    拓展:io包下的拷贝方法除了Copy()外还有CopyN()以及CopyBuffer()方法

    CopyNCopy的区别在于,接收多一个参数n,表示拷贝n个字节到目标文件,定义如下:

    CopyBufferCopy功能一样,只不过可以自己通过一个提供的缓冲区(Copy内部是分配的临时的),Copy的内部也是通过CopyBuffer实现的

    func CopyN(dst Writer, src Reader, n int64) (written int64, err error)
    func CopyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error)
    
  • 方式三:通过ioutil包下的WriteFile()ReadFile(),这两个方法用的是文件一次性读取再一次性写入的方式,不太适合大文件复制,可能会造成内存溢出
    使用比较简单,不多介绍了,需要注意的是``ReadFile()读取完毕后返回的err==nil而非io.EOF`

    package main
    
    import (
    	"fmt"
    	"io/ioutil"
    	"os"
    )
    
    func main() {
    	/*
    		拷贝文件
    	*/
    	srcFile := "/home/GoWorkSpace/src/fileTest/test.txt"      //源文件
    	destFile := "/home/GoWorkSpace/src/fileTest/destFile.txt" //目标文件
    
    	//ioutil.ReadFile ioutil.WriteFile一次性读取 一次性写入
    	bs, err := ioutil.ReadFile(srcFile) //返回读取到的字节切片
    	if err != nil {
    		fmt.Println(err)
    		return
    	}
    	ioutil.WriteFile(destFile, bs, os.ModePerm) //当文件不存在时候,会自动创建文件
    }
    
    

断点续传

我们知道Seek方法可以设置读取的偏移量,下面代码示例可以深入认识其使用

package main

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

func main() {
   /*
      设置指针光标位置:
      Seek(offset int64, whence int) (ret int64, err error)
         第一个参数:编译量
         第二个参数:如何设置
            0 -> 文件开头
            1 -> 当前偏移量
            2 -> 文件末尾
      第二个参数,go的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
         )
   */
   //示例text.txt中内容:golang is best language!

   fileName := "/home/GoWorkSpace/src/fileTest/text.txt"
   file, err := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE, os.ModePerm)
   if err != nil{
      log.Fatal(err)
   }
   defer file.Close()

   //读写
   bs := make([]byte,1,1)
   file.Read(bs) //读文件的第一个数据
   fmt.Println(string(bs)) //g
   //上述操作后,光标已经移动到了第一个字符后面的位置
   //我们可以通过seek设置光标的位置
   file.Seek(4,io.SeekStart) //光标移动到文件的开头偏移四个字节的位置
   file.Read(bs)
   fmt.Println(string(bs)) //n

   file.Seek(0,io.SeekEnd) //光标移动文件末尾
   file.WriteString("hahaha")
   file.Seek(0,io.SeekStart) //偏移回开头,不然读取就为空了
   bs2, _:= ioutil.ReadAll(file)
   fmt.Println(string(bs2))

   file.Seek(-2,io.SeekCurrent) //光标移动到当前位置的前两个字节的位置
   file.Read(bs)
   fmt.Println(string(bs)) //h

}

思考:

  • 是否有方法缩短传输大文件的耗时?
  • 文件传递过程中意外中断,下次重启时是否又要重头传?
  • 传递文件过程能否支持暂停和恢复

上面的思考的问题我们作为网络冲浪用户应该已经司空见惯,我们在百度下载文件可不就是满足上面的思考条件的吗。一般我们把上述的文件传递方式我们称为断点续传

go语言通过Seek()就可以实现断点续传

实现思路:实现断点续传的道理很简单,只需要记住上一次“已经”读取了多少数据,当再次执行程序的时候,从上一次所记住的那个“位置”再开始去写入就可以了

示例不引入数据库的使用。下面我们通过创建一个临时文件temp.txt在文件中记录已经传了多少数据量,这意味着在传递过程中,需要不停的修改文件中已经传递的数据量

package main

import (
   "fmt"
   "io"
   "log"
   "os"
   "path/filepath"
   "strconv"
   "strings"
)

func main() {
   /*
      断点续传:
         文件的传递:本质上就是文件的复制,即就是一个读取写入的过程

      示例:
         有一图片:
         /home/jochen/下载/img.png
         复制到当前工程下

      思路:边复制,边记录复制的总量
   */
   srcFileName := "/home/jochen/下载/img.png"
   destFileName := srcFileName[strings.LastIndex(srcFileName,"/")+1:] //巧妙获取文件名
   fmt.Println(filepath.Base("/home/jochen/下载/img.png"))              //img.png
   fmt.Println(destFileName)                                          //img.png

   tempFileName := destFileName + "tempFile.txt" //临时文件的名称
   fmt.Println(tempFileName)                 //img.pngtemp.txt

   srcFile, err1 := os.Open(srcFileName) //源文件只读就可以了
   if err1 != nil {
      log.Fatal(err1)
   }
   destFile, err2 := os.OpenFile(destFileName,os.O_CREATE|os.O_RDWR,os.ModePerm)
   if err1 != nil {
      log.Fatal(err2)
   }
   tempFile, err3 := os.OpenFile(tempFileName,os.O_CREATE|os.O_RDWR,os.ModePerm)
   if err1 != nil {
      log.Fatal(err3)
   }
   defer srcFile.Close()
   defer destFile.Close()
   defer tempFile.Close()

   //step1:先读取临时文件中的数据,再seek
   tempFile.Seek(0,io.SeekStart)
   bs := make([]byte,100,100)
   n1, err := tempFile.Read(bs)

   countStr := string(bs[:n1]) //读取到的临时文件所记录的已传输数据量
   count, _ := strconv.ParseInt(countStr,10, 64) //一开始没有数据,""转型失败默认值就是0
   fmt.Println("已经传输大小:",count)


   //step2:根据count值设置偏移量,每次从偏移量往下去复制(写入)
   srcFile.Seek(count,io.SeekStart)
   destFile.Seek(count,io.SeekStart)
   data := make([]byte, 1024, 1024) //用以复制文件的缓冲区
   n2 := -1 //读取的数据量
   n3 := -1 //写出的数据量
   var total  = count //读取的总量

   //step3:复制文件
   for {
      //读多少写多少
      n2 ,err = srcFile.Read(data)
      if n2 == 0 || err == io.EOF {
         fmt.Print("文件复制完毕,总大小为:",total)
         os.Remove(tempFileName) //删除临时文件
         break
      }
      n3, err = destFile.Write(data[:n2])
      total += int64(n3)

      //将复制的总量 存储到临时文件中
      tempFile.Seek(0,io.SeekStart) //从头开始写 将原有的数据擦除掉(数据肯定是越来越大的,所以肯定会覆盖掉原有的)
      tempFile.WriteString(strconv.FormatInt(total,10))

      fmt.Println("已复制:",total)
      //模拟中断
      if total > 6666 {
         panic("断啦")

      }
   }


}

bufio包

bugio包是go语言提供缓存io操作的包,使用这个包可以大幅提高文件的读写效率

原理

bufio实现了带缓存的io操作,也就是说bufio是通过缓存来提升效率的

io操作效率低主要体现在频繁的访问本地磁盘文件时,为此bufio提供了缓存区(分配一块内存),读写都先在缓冲区做好,最后再一并进行读写到文件。这样就达到了降低访问本地磁盘的次数的效果,从而提供性能

例如:

  • 读操作时,把文件先全部读取进缓存(内存),后续再进行文件读取的时候可以避免文件系统的io
  • 写操作时,把文件写入缓存(内存),写完后一并写入文件系统

Reader和Writer类型

bufioio.Readerio.Writer接口进行了封装,将这两个对象分别放在bufio包中定义的同名结构体成员字段中

ps:封装的结构名与io包中定义的这两个接口名相同

Reader类型

bufio.Reader 结构封装了原有的 io.Reader 对象作为成员字段,同时自己本身实现了 io.Reader 接口
在实现的Read方法和更多对外暴露的读方法如ReadLine()中使用封装io.Reader字段其他字段配合提供缓存的功能和便捷读取的功能(如读取一行数据)

结构定义如下:

    type Reader struct {
        buf          []byte        // 缓存
        rd           io.Reader    // 底层的io.Reader
        //  r:从buf中读走的字节(偏移);w:buf中填充内容的偏移;
        // w - r 是buf中可被读的长度(缓存数据的大小),也是Buffered()方法的返回值
        r, w         int
        err          error        // 读过程中遇到的错误
        lastByte     int        // 最后一次读到的字节(ReadByte/UnreadByte)
        lastRuneSize int        // 最后一次读到的Rune的大小 (ReadRune/UnreadRune)
    }

下面是bufio中的Reader对象,对io.Reader接口的实现

// Read reads data into p.
// It returns the number of bytes read into p.
// The bytes are taken from at most one Read on the underlying Reader,
// hence n may be less than len(p).
// To read exactly len(p) bytes, use io.ReadFull(b, p).
// At EOF, the count will be zero and err will be io.EOF.
func (b *Reader) Read(p []byte) (n int, err error) { //每次调用读取len(p)大小的内容
	n = len(p)
	if n == 0 {
		if b.Buffered() > 0 {
			return 0, nil
		}
		return 0, b.readErr()
	}
	if b.r == b.w {
		if b.err != nil {
			return 0, b.readErr()
		}
		if len(p) >= len(b.buf) {
			// Large read, empty buffer.
			// Read directly into p to avoid copy.
			n, b.err = b.rd.Read(p)
			if n < 0 {
				panic(errNegativeRead)
			}
			if n > 0 {
				b.lastByte = int(p[n-1])
				b.lastRuneSize = -1
			}
			return n, b.readErr()
		}
		// One read.
		// Do not use b.fill, which will loop.
		b.r = 0
		b.w = 0
		n, b.err = b.rd.Read(b.buf)
		if n < 0 {
			panic(errNegativeRead)
		}
		if n == 0 {
			return 0, b.readErr()
		}
		b.w += n
	}

	// copy as much as we can
	n = copy(p, b.buf[b.r:b.w])
	b.r += n
	b.lastByte = int(b.buf[b.r-1])
	b.lastRuneSize = -1
	return n, nil
} 

其实现过程主要思路如下:

有一本地文件A,现需把A的数据读到程序当中

一般情况下文件读取逻辑:程序当中创建一个切片p,读取数据的过程就是把本地文件A当中的数据读取切片p,然后对切片p操作就是在操作A的数据了

bufio进行文件读取时,首先会创建一个缓冲区(本质就是内存),然后对针对不同的情况执行下面的操作

  1. 当切片p的大小比缓冲区中的大,此时缓冲区的存在是没有意义的,故直接从A一次读取到p即可
  2. 当切片p的大小比缓冲区中的小A的数据先读到缓冲区中,然后程序再从缓冲区中一点一点的将数据读取到切片p中,读取完毕后清空缓冲区。再次将文件数据读入缓冲区,重复上述操作,直到A读取完毕
    例如A中有2000条数据,缓冲区可以存888条,而切片p只能存100条。此时使用切片p直接读取A的数据就会需要读很多次即频繁的进行io操作。可以先将888条数据读取到缓冲区,然后p再冲缓冲区一点一点的读取,这样就减少了大量的io操作并且还能大幅度提高读取的效率

创建Reader对象

bufio 包提供了两个实例化 bufio.Reader 对象的函数:NewReaderNewReaderSize
其二者的主要区别是NewReaderSize可以指定缓冲区的大小。
其中,NewReader 函数是调用 NewReaderSize 函数实现的,只不过前者的缓冲区大小是一个固定的常量:

    func NewReader(rd io.Reader) *Reader {
        // 默认缓存大小:defaultBufSize=4096
        return NewReaderSize(rd, defaultBufSize)
    }

还是从示例代码中学习:

package main

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

func main() {
	/*
		缓存io:bufio
			作用:高效读写
			原理:将io包下的Reader和Writer对象进行包装(加入缓存),提高读写效率
	*/

	//创建o.Reader对象 如文件
	fileName := "/home/GoWorkSpace/src/fileTest/test.txt"
	file ,err := os.Open(fileName)
	if err != nil{
		fmt.Println(err)
		return
	}
	defer file.Close()

	//创建bufio.Reader对象
	b1 := bufio.NewReader(file) //参数io.Reader对象 里面有固定大小的缓冲区 defaultBufSize = 4096 这个值在go1.16好像变大了巨多
	//p := make([]byte, 5000) //当我们定义的字节切片大小超过了缓冲区,缓冲区是没有意义的
	p := make([]byte, 1024) //当我们定义的字节切片大小比缓冲区小,读取数据会先从文件读到缓冲区,然后p在一点点从缓冲区读
	n1,err := b1.Read(p)
	fmt.Println(n1)
	fmt.Println(string(p[:n1]))
	/*
		输出:
			1. golang是世界上最好的语言!
			2. golang是世界上最好的语言!
			3. golang是世界上最好的语言!
			5. golang是世界上最好的语言!
			6. golang是世界上最好的语言!
			7. golang是世界上最好的语言!
			8. golang是世界上最好的语言!

	*/

	//bufio.Reader对象对外暴露的其他读取方法

	//1.ReadLine()读取一行数据
	//返回 数据字节切片 line,读取数据是否超过缓冲的标识符 isPrefix, 错误 err
	//ReadLine尝试返回一行数据,不包括行尾标志的字节。如果行太长超过了缓冲,返回值isPrefix会被设为true,并返回行的前面一部分。该行剩下的部分将在之后的调用中返回
	file.Seek(0,io.SeekStart) //将光标重置会开头
	data, flag, err := b1.ReadLine()
	fmt.Println(flag)
	fmt.Println(string(data)) //1. golang是世界上最好的语言!
	//ReadLine是一个低水平的行数据读取原语。大多数调用者应使用ReadBytes('\n')或ReadString('\n')代替,或者使用Scanner。

	//2.ReaderString(delim byte) delim参数表示分隔符,界定符的意思
	//返回值直接是一个字符串数据和错误
	//读取的数据从光标开始以传入的分隔符结束
	//如果在读取到分隔符之前遇到错误,会返回这之前读到的数据和错误信息,通常是EOF,读到末尾都没有找到标识符的情况
	res,err := b1.ReadString('\n') //读取行一般用这种方式
	fmt.Println(res) //2. golang是世界上最好的语言!
	fmt.Println(err) //nil

	//3.ReaderByte() 读取一个字节
	res2, err := b1.ReadByte()
	fmt.Println(res2) //51
	fmt.Println(string(res2)) //3

	//4.ReaderBytes(delim byte) 和ReaderString作用一样,只不过返回的是字节切片
	res3, err := b1.ReadBytes('\n')
	fmt.Println(res3)
	//[46 32 103 111 108 97 110 100 230 152 175 228 184 150 231 149 140 228 184 138 230 156 128 229 165 189 231 154 132 232 175 173 232 168 128 33 10]
	fmt.Println(string(res3)) //. golang是世界上最好的语言!

	//5.在本地电脑中 数据除了一般从文件读取,bufio还提供了从标准输入读取数据的方法
	//Scanner 是bufio下提供从标准输入中读取数据的类
	s1 := ""
	fmt.Scanln(&s1) //读取键盘输入
	fmt.Println(s1) //fmt包下的Scanln方法用于从标准输入中读取数据,有缺点:遇到空格就停止读取

	b2 := bufio.NewReader(os.Stdin) //获取标准输入Reader对象
	s2, err := b2.ReadString('\n') //读取输入的所有内容直接输入回车结束
	fmt.Println(s2)
}


Writer类型

bufio.Writerbufio包下对io.Writer的封装,为的是提供缓冲写入的功能,其定义如下:

// Writer implements buffering for an io.Writer object.
// If an error occurs writing to a Writer, no more data will be
// accepted and all subsequent writes, and Flush, will return the error.
// After all data has been written, the client should call the
// Flush method to guarantee all data has been forwarded to
// the underlying io.Writer.
type Writer struct {
   err error
   buf []byte
   n   int
   wr  io.Writer
}

封装的Writer对象同样也实现了io.Writer接口(实现了Write(p []byte)方法):

// Write writes the contents of p into the buffer.
// It returns the number of bytes written.
// If nn < len(p), it also returns an error explaining
// why the write is short.
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 {
         // Large write, empty buffer.
         // Write directly from p to avoid copy.
         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
}

其实现Write(p []byte)的思路如下:

  1. 判断缓冲区buf中可用容量是否可以放下p
  2. 当缓冲区的容量可以存储p中的内容,则将p的内容放到缓冲区
  3. 缓冲区的容量无法放下p中的内容,并且此时缓冲区中没有内容,此时内容不会写入缓冲区,而是直接写入文件中
  4. 缓冲区的容量无法放下p中的内容,但是此刻缓冲区中有一部分内容,此时会先把p中的部分内容拿去填满缓冲区。
    然后把缓冲区中的所有内容写到文件,并清空缓冲区。
    然后再判断p剩下的内容大小能否放入缓冲区中,如果放的下就放到缓冲区,放不下直接写入文件

创建Writer对象

与Reader对象一样,bufio 包提供了两个实例化 bufio.Writer对象的函数:NewWriterNewWriterSize
其二者的主要区别是NewWriterSize可以指定缓冲区的大小。
其中,NewWriter 函数是调用 NewWriterSize 函数实现的,只不过前者的缓冲区大小是一个固定的常量:

func NewWriter(w io.Writer) *Writer 
func NewWriterSize(w io.Writer, size int) *Writer

还是从示例代码中学习:

package main

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

func main() {
	/*
		bufio.Writer:高效带缓存的输出对象
	*/
	fileName := "/home/GoWorkSpace/src/fileTest//Writer.txt"
	file, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR, os.ModePerm)
	if err != nil{
		fmt.Println(err)
		return
	}
	defer file.Close()

	w1 := bufio.NewWriter(file) //获取writer对象 默认buf=> defaultBufSize = 4096
	//n,err := w1.WriteString("hello golang")
	//fmt.Println("写入的数据量为:",n) //写入的数据量为: 12
	//fmt.Println(err) //<nil>
	//打开本地文件发现数据并未写入,这是因为数据只是写到了缓冲区,并未持久化
	//需要手动的调用Flush方法,去刷新缓冲区
	w1.Flush() //刷新缓冲区-》将缓冲区的数据写到目标文件中


	//当写入的数据比缓冲区要大的多时
	for i := 1; i <= 1000; i++ {
		w1.WriteString(fmt.Sprintf("%d. golang is best language!\n",i))
	}//缓冲区被填满后会自动的把数据保存到文件中并清空缓冲区继续接收未写完的数据
	w1.Flush() //会把缓冲区残留的数据保存到文件

	//bufio的Writer对象和Reader,对外暴露了其他的写入方法,比较见名知意就不多介绍了,感兴趣找标准库看两眼吧
	//w1.Write()
	//w1.WriteByte()
	//w1.WriteRune()
}

ioutil包

ioutil包是go提供的辅助读写数据的工具包

ioutil包在go1.6版本已经移除

简单通过代码介绍其使用吧:

package main

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

func main() {

   fileName := "/home/GoWorkSpace/src/fileTest/text.txt"
   //1.ioutil.ReadFile() 读取指定文件的所有内容
   data, err := ioutil.ReadFile(fileName) //参数:文件名   返回:字节切片和错误
   fmt.Println(data)                      //[103 111 108 97 110 100 32 105 115 32 98 101 115 116 32 108 97 110 103 117 97 103 101 33]
   fmt.Println(string(data))              //golang is best language!
   fmt.Println(err)                       //<nil>

   //2.ioutil.ReadAll() 读取io.reader对象中的所有数据  ReadFile()的底层也是调用该方法
   content := "瞎写了一段话用来读取"
   strReader := strings.NewReader(content) //得到的也是io.Reader对象,其是string的封装
   data, err = ioutil.ReadAll(strReader)
   fmt.Println(data)         //[231 158 142 229 134 153 228 186 134 228 184 128 230 174 181 232 175 157 231 148 168 230 157 165 232 175 187 229 143 150]
   fmt.Println(string(data)) //瞎写了一段话用来读取
   fmt.Println(err)          //<nil>

   //3.ioutil.WriteFile()向指定文件写数据,若文件不存在会按指定权限创建(该方法在写文件前会清空文件的内容)
   fileWriteName := "/home/GoWorkSpace/src/fileTest/WriterTest.txt"
   s := "我要把这段话写到WriterTest.txt文件中!"
   err = ioutil.WriteFile(fileWriteName, []byte(s), os.ModePerm) //参数:文件名 写入的内容字节数组 创建文件的权限(文件不存在时)
   fmt.Println(err)                                              //<nil>

   //4.ioutil.ReadDir()读取某个目录中的所有文件列表 返回的是os.FileInfo文件信息对象列表(只能读取一层)
   fileInfos, err := ioutil.ReadDir("./")
   fmt.Printf("%T\n", fileInfos) //[]os.FileInfo
   for i, fileIno := range fileInfos {
      fmt.Printf("第%d个文件,名称:%s, 是否是目录:%t\n", i, fileIno.Name(), fileIno.IsDir())
   }

   //5.临时目录和临时文件
   //创建临时目录 参数:1.所要创建的目录的目录 2.目录名前缀
   dir, err := ioutil.TempDir("/home/GoWorkSpace/src/fileTest/","TestDir")
   if err != nil {
      fmt.Println(err)
      return
   }
   //返回的是目录名,其是我们指定的参数2+随即字符串组成
   fmt.Println(dir) ///home/GoWorkSpace/src/fileTest/TestDir190096320
   //临时目录最终要记得手动删除
   defer os.RemoveAll(dir)

   //创建临时目录 参数:1.所要创建的文件的目录 2.文件名前缀
   fileTemp, err := ioutil.TempFile(dir,"TestFile")
   if err != nil {
      fmt.Println(err)
      return
   }
   //返回的是对象
   fmt.Printf("%T\n",fileTemp) //*os.File
   fmt.Println(fileTemp.Name()) //home/GoWorkSpace/src/fileTest/TestDir603047065
   //临时文件最终要记得手动删除
   defer os.Remove(fileTemp.Name())
}

拓展:遍历文件夹

我们经常会有需求需要获取某个路径下所有的文件内容(包括所有子目录中的内容)
前面学习了ioutil.readDir()方法可以获取到某个目录中的文件信息,但其只能读取一层目录中的信息
go并没有提供给我们递归获取某个目录下所有的文件包括子目录内的文件的方法,需要我们手撸一个:

package main

import (
	"fmt"
	"io/ioutil"
)

func main() {
	/*
		遍历文件夹
	*/
	dirName := "//home/GoWorkSpace/src/fileTest/"
	res, err := getFileListFromDir(dirName, 0)
	fmt.Println(err)
	fmt.Println(res)
}

//获取某目录下所有的文件信息(递归获取包括所有子目录下的文件信息)
func getFileListFromDir(dir string, level int) (res []string, err error) {
	//level用来记录当前递归层次,生成带有层次感的空格
	s := "|——"
	for i := 0; i < level; i++ {
		s = "|  " + s
	}

	fileinfos, err := ioutil.ReadDir(dir)
	if err != nil {
		return nil, err
	}
	for _, fileinfo := range fileinfos {
		filename := dir + "/" + fileinfo.Name() //获取的文件名非完全名,需要拼接路径
		fmt.Printf("%s%s\n", s, filename)

		if fileinfo.IsDir() { //当文件类型是目录时
			//递归获取其下所有文件
			fileNameList, err := getFileListFromDir(filename, level+1)
			if err != nil {
				return nil, err
			}
			//将返回的文件名列表添加到返回结果切片中
			res = append(res, fileNameList...)
		}

		res = append(res, fileinfo.Name())
	}

	return res, err
}

笔者输出的内容:

|——//home/GoWorkSpace/src/fileTest//.idea
  |——//home/GoWorkSpace/src/fileTest//.idea/.gitignore
  |——//home/GoWorkSpace/src/fileTest//.idea/fileTest.iml
  |——//home/GoWorkSpace/src/fileTest//.idea/inspectionProfiles
    |——//home/GoWorkSpace/src/fileTest//.idea/inspectionProfiles/Project_Default.xml
  |——//home/GoWorkSpace/src/fileTest//.idea/modules.xml
  |——//home/GoWorkSpace/src/fileTest//.idea/workspace.xml
|——//home/GoWorkSpace/src/fileTest//TestDir190096320
|——//home/GoWorkSpace/src/fileTest//TestDir340603911
|——//home/GoWorkSpace/src/fileTest//TestDir603047065
|——//home/GoWorkSpace/src/fileTest//TestDir655589838
  |——//home/GoWorkSpace/src/fileTest//TestDir655589838/TestFile412803285
  |——//home/GoWorkSpace/src/fileTest//TestDir655589838/TestFile465854906
  |——//home/GoWorkSpace/src/fileTest//TestDir655589838/TestFile533090596
|——//home/GoWorkSpace/src/fileTest//Writer.txt
|——//home/GoWorkSpace/src/fileTest//WriterTest.txt
|——//home/GoWorkSpace/src/fileTest//destFile.txt
|——//home/GoWorkSpace/src/fileTest//file1.go
|——//home/GoWorkSpace/src/fileTest//file10.go
|——//home/GoWorkSpace/src/fileTest//file11.go
|——//home/GoWorkSpace/src/fileTest//file12.go
|——//home/GoWorkSpace/src/fileTest//file13.go
|——//home/GoWorkSpace/src/fileTest//file14.go
|——//home/GoWorkSpace/src/fileTest//file15.go
|——//home/GoWorkSpace/src/fileTest//file16.go
|——//home/GoWorkSpace/src/fileTest//file2.go
|——//home/GoWorkSpace/src/fileTest//file3.go
|——//home/GoWorkSpace/src/fileTest//file4.go
|——//home/GoWorkSpace/src/fileTest//file5.go
|——//home/GoWorkSpace/src/fileTest//file6.go
|——//home/GoWorkSpace/src/fileTest//file7.go
|——//home/GoWorkSpace/src/fileTest//file8.go
|——//home/GoWorkSpace/src/fileTest//file9.go
|——//home/GoWorkSpace/src/fileTest//img.png
|——//home/GoWorkSpace/src/fileTest//test.txt
|——//home/GoWorkSpace/src/fileTest//test3.txt
|——//home/GoWorkSpace/src/fileTest//text.txt
|——//home/GoWorkSpace/src/fileTest//writeFile.txt
<nil>
[.gitignore fileTest.iml Project_Default.xml inspectionProfiles modules.xml workspace.xml .idea TestDir190096320 TestDir340603911 TestDir603047065 TestFile412803285 TestFile465854906 TestFile533090596 TestDir655589838 Writer.txt WriterTest.txt destFile.txt file1.go file10.go file11.go file12.go file13.go file14.go file15.go file16.go file2.go file3.go file4.go file5.go file6.go file7.go file8.go file9.go img.png test.txt test3.txt text.txt writeFile.txt]

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

推荐阅读