GO语言工程实践课后作业:实现思路、代码以及路径记录| 青训营

76 阅读6分钟

文件合并

问题:把一个目录下的所有.txt文件合成一个大的.txt文件,再对这个大文件进行压缩

分析思路:

想要把一个目录下的所有txt文件合成一个大的文件big.txt,再对这个大文件压缩big.zlib

创建文件

1.先创建 big.txt ,一般都会加defer fout.Close() ,这是让文件当前函数执行完毕后关闭

    //创建文件
	fout, writer, _, err := openFileAndCreateWriter("big.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer fout.Close()

	fout2, _, writer2, err := openFileAndCreateWriter("big.zlib")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer fout2.Close()

读取目录

  1. 读取指定整个目录,用 ioutil.ReadDir(dir)
if files, err := ioutil.ReadDir(dir); err != nil {
// ioutil.ReadDir这是再读取指定目录文件
		fmt.Println(err)
		return
	} else { .....}

遍历

  1. 再 for遍历判断读取目录中每一个文件的类型,如果还是目录就直接跳过,我们只要.txt文件。使用IsDir来判断是不是目录,使用HasSuffix判断文件后缀 ,值得学习的是HasPrefix是前缀
for _, file := range files { 

       if file.IsDir() { 
        // 如果读到的是一个目录,那跳过这次
                continue
        }
        
        baseName := file.Name()                  // 获取当前文件名
        if strings.HasSuffix(baseName, ".txt") {
                inPath := filepath.Join(dir, baseName)
                readFile(inPath, writer, writer2)
        }
}

这个range遍历一个 files会返回index和value(这里用一个变量file接收),index用不到所以直接下划线

HasPrefix是前缀,HasSuffix是后缀 如果当前读到的baseName文件后缀为 .txt

判断读取

  1. 开始读取文件代码里封装了readFile(),在mergeFile里调用所以没调用一次readFile就是一个文件正在被打开使用os.Open(inPath)打开文件,打开出错直接return,没有出错就defer 让它在函数执行完毕后就关闭
if fin, err := os.Open(inPath); err != nil {
            fmt.Println(err)
            return //新建函数可以直接return了
	} else { 
            defer fin.Close()
            reader := bufio.NewReader(fin)
            ..........
        }

开始读取

  1. 怎么读?使用reader.ReadString('\n')一行一行读,用err==io.EOF判断是否达到末尾。注意这里是for里放if 然后if中一行一行读,所以reader.ReadString('\n')参数用 '\n' 读到这个说明这行读完了
for {
if line, err := reader.ReadString('\n'); err != nil { //读的过程有err,有err不一定等于出错,也可能是读到末尾了
    if err == io.EOF { //如果到了文件末尾
            if len(line) > 0 { // 文件最后一行没有换行符
            writer.WriteString(line) //把读到的内容写入到 fout(big.txt)文件里
            writer.WriteString("\n") //自己加上换行符

            writer2.Write([]byte(line)) //把读到的内容写入到 fout(big.txt)文件里
            writer2.Write([]byte{'\n'}) //自己加上换行符
         }
    }
    break
    } else { //没出错
            writer.WriteString(line) //直接写入
            writer2.Write([]byte(line))
    }

}

还有一个小细节,这个文件读完的时候可能不是空行结尾的,所以用len(line) > 0来判断文件尾有没有换行符

压缩

  1. 好了读完的内容要写到big.txt后缀big.zlib里面去 .txt的可以直接用string写,但是zlib压缩的只能用切片来写所以txt用 writer.WriteString(line)

    而zlib用writer2.Write([]byte(line))好,还要记得我们自己加上换行符,因为两个文件衔接要换行。这样才不会让前一个文件的末尾和后一个文件的开始 贴成一行

// 压缩  
func zipFile() (*zlib.Writer, *os.File, error) {
	fout, err := os.OpenFile("big.zlib", os.O_CREATE|os.O_TRUNC, os.ModePerm)

	if err != nil {
		fmt.Println(err)
		return nil, nil, err
	}
	// defer fout.Close() //   这个defer是函数执行完后关闭不是马上关闭

	writer := zlib.NewWriter(fout) // 这是创建一个写入器

	return writer, fout, nil
}

发现有些问题,可以优化这个压缩代码所以:

优化

// 压缩   //还是垃圾有问题,需要优化
func zipFile() (*zlib.Writer, *os.File, error) {
	fout, err := os.OpenFile("big.zlib", os.O_CREATE|os.O_TRUNC, os.ModePerm)

	if err != nil {
		fmt.Println(err)
		return nil, nil, err
	}
	// defer fout.Close() //   这个defer是函数执行完后关闭不是马上关闭

	writer := zlib.NewWriter(fout) // 这是创建一个写入器

	return writer, fout, nil
}

// 优化
func openFileAndCreateWriter(filename string) (*os.File, *bufio.Writer, *zlib.Writer, error) {
	fout, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC, os.ModePerm)
	if err != nil {
		return nil, nil, nil, err
	}

	writer := bufio.NewWriter(fout)
	var zlibWriter *zlib.Writer
	if strings.HasSuffix(filename, ".zlib") {
		zlibWriter = zlib.NewWriter(fout)
	}
	return fout, writer, zlibWriter, nil
}
  • 避免频繁的文件创建和关闭: 在原始版本中,每次调用 zipFile 都会创建一个新文件。在优化版本中,只在第一次调用时创建文件,后续调用重用同一个文件。
  • 使用缓冲写入: 引入了 bufio.Writer,它对写入的数据进行缓冲,减少频繁的磁盘写入操作,提高性能。
  • 条件化的压缩: 优化版本中根据文件名是否以 ".zlib" 后缀来决定是否使用 zlib.Writer 进行压缩。这样,只有需要压缩的文件才会使用压缩操作,避免不必要的开销。

刷新

  1. 最后记得刷新 文件 writer.Flush()
	//刷新
	writer.Flush()
	writer2.Flush()

完整实例代码:

package main

import (
	"bufio"
	"compress/zlib"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
)

func readFile(inPath string, writer *bufio.Writer, writer2 *zlib.Writer) {

	if fin, err := os.Open(inPath); err != nil {
		fmt.Println(err)
		//continue //这里不能用return,因为这个文件打开出错而已,其他文件可能可以
		return //新建函数可以直接return了
	} else {
		defer fin.Close()
		reader := bufio.NewReader(fin)
		for {
			if line, err := reader.ReadString('\n'); err != nil { //读的过程有err,有err不一定等于出错,也可能是读到末尾了
				if err == io.EOF { //如果到了文件末尾
					if len(line) > 0 { // 文件最后一行没有换行符
						writer.WriteString(line) //把读到的内容写入到 fout(big.txt)文件里
						writer.WriteString("\n") //自己加上换行符

						writer2.Write([]byte(line)) //把读到的内容写入到 fout(big.txt)文件里
						writer2.Write([]byte{'\n'}) //自己加上换行符
					}
				}
				break
			} else { //没出错
				writer.WriteString(line) //直接写入
				writer2.Write([]byte(line))
			}

		}
	}
}

// 压缩   //还是垃圾有问题,需要优化
func zipFile() (*zlib.Writer, *os.File, error) {
	fout, err := os.OpenFile("big.zlib", os.O_CREATE|os.O_TRUNC, os.ModePerm)

	if err != nil {
		fmt.Println(err)
		return nil, nil, err
	}
	// defer fout.Close() //   这个defer是函数执行完后关闭不是马上关闭

	writer := zlib.NewWriter(fout) // 这是创建一个写入器

	return writer, fout, nil
}

// 优化
func openFileAndCreateWriter(filename string) (*os.File, *bufio.Writer, *zlib.Writer, error) {
	fout, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC, os.ModePerm)
	if err != nil {
		return nil, nil, nil, err
	}

	writer := bufio.NewWriter(fout)
	var zlibWriter *zlib.Writer
	if strings.HasSuffix(filename, ".zlib") {
		zlibWriter = zlib.NewWriter(fout)
	}
	return fout, writer, zlibWriter, nil
}

func mergeFile(dir string) {

	// func OpenFile(name string, flag int, perm FileMode) (*File, error)
	/*
		参数一:name 是要打开或创建的文件名。

		参数二:flag 是打开文件的标志,它是一个位掩码,
		可以使用 os.O_CREATE 表示如果文件不存在则创建,
		os.O_TRUNC 表示打开时清空文件内容,
		os.O_APPEND 表示在文件末尾添加内容等等。
		多个标志可以通过按位或运算符 | 组合在一起。

		参数三:perm 是文件的权限,用于指定新创建的文件的权限位。
		它是一个八进制数,表示文件的读、写和执行权限。可以使用 os.ModePerm 表示默认的文件权限。
		perm 参数用于指定新创建的文件的权限位,即文件的读、写和执行权限。它是一个八进制数,通常以三个八进制位表示,分别对应着所有者、所属组和其他用户的权限。

			在 Go 的 os 包中,预定义了一些常用的文件权限常量,包括:

			os.ModeDir:目录权限,用于表示新创建的目录的权限。
			os.ModeAppend:追加权限,用于在文件打开时在末尾添加内容。
			os.ModeExclusive:独占权限,用于以独占方式打开文件。
			os.ModeTemporary:临时权限,用于表示临时文件的权限。
			os.ModeSymlink:符号链接权限,用于表示符号链接的权限。
			通常,我们使用 os.ModePerm 表示默认的文件权限,它是一个八进制数 0777,表示所有者、所属组和其他用户都具有读、写和执行权限。

	*/

	// fout, err := os.OpenFile("big.txt", os.O_CREATE|os.O_TRUNC, os.ModePerm)

	// if err != nil {
	// 	fmt.Println(err)
	// 	return
	// }
	// defer fout.Close() //打开成功后  这个defer是函数执行完毕后 close掉,不是马上关闭

	// writer := bufio.NewWriter(fout) // 这是创建一个写入器

	// writer2, fout2, _ := zipFile()
	// defer fout2.Close()

    //创建文件
	fout, writer, _, err := openFileAndCreateWriter("big.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer fout.Close()

	fout2, _, writer2, err := openFileAndCreateWriter("big.zlib")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer fout2.Close()

    
    // 
	if files, err := ioutil.ReadDir(dir); err != nil { // ioutil.ReadDir这是再读取指定目录文件
		fmt.Println(err)
		return
	} else {
		for _, file := range files { // 这个range遍历一个 files   会返回index和value(这里用一个变量file接收),index用不到所以直接下划线
			if file.IsDir() { // 如果读到的是一个目录,那跳过这次
				continue
			}
			baseName := file.Name()                  // 获取当前文件名
			if strings.HasSuffix(baseName, ".txt") { // HasPrefix是前缀,HasSuffix是后缀  如果当前读到的baseName文件后缀为 .txt
				inPath := filepath.Join(dir, baseName)
				readFile(inPath, writer, writer2)
			}
		}
	}
	//刷新
	writer.Flush()
	writer2.Flush()

}