go按行读写文件

本文详细介绍使用Golang进行文件读写的多种方法,包括判断文件存在性、按行读取文件内容以及向已存在文件中追加写入数据的具体实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

2019年第一篇博客

兜兜转转又回来更新go的博客了,今天来讲一下如何使用golang进行文件的读写

文件读写需要使用的包

  1. os
  2. ioutil
  3. bufio
  4. strings

文件读写总是少不了判断文件是否存在!

go里面使用os.Stat 和 os.IsNotExist 相结合来判断文件是否存在
os.Stat 是用来获取文件的相关信息的,比如文件大小,最近一次修改时间等,但是假如文件不存在就返回error
我们就需要这个error 然后IsNotExist 判断 有 error说明文件不存在

show me your code?

// 判断文件是否存在
	if _, err := os.Stat(filename); os.IsNotExist(err) {
		return err
	}

按行读文件

这里我使用了ioutil.ReadFile() 需要注意的是这个返回的是[]byte 类型的,需要转为string 才是我们可读的样子
按行读取有两种方式 1 读取整个文件然后按照\n 分割,再循环读取 2 使用ioutil.ReadLine

func Readlines(filename string) {
	// go 按行读取文件的方式有两种,
	// 第一 将读取到的整个文件内容按照 \n 分割
	// 使用bufio
	// 第一种
	lines, err := ioutil.ReadFile(filename)

	if err != nil {
		fmt.Println(err)
	} else {
		contents := string(lines)
		lines := strings.Split(contents, "\n")
		for _, line := range lines {
			fmt.Println(line)
		}
	}
	// 第二种
	fd, err := os.Open(filename)
	defer fd.Close()
	if err != nil {
		fmt.Println("read error:", err)
	}
	buff := bufio.NewReader(fd)

	for {
		data, _, eof := buff.ReadLine()
		if eof == io.EOF {
			break
		}

		fmt.Println(string(data))
	}
}

写文件

我这里的写文件是写入一个已经存在的文件,需要特别注意的是使用os.Open方式是只读的方式打开的,这样无法写入也没有报错 所以应该使用os.OpenFile 指定打开的方式为 os.O_RDWR|os.O_APPEND 这样才能使用写入,并且会追加写入,只使用os.O_RDWR,每次都是覆盖写入,os.O_APPEND 无法写入

func WriterTXT(filename, content string) error {
	// 写入文件
	// 判断文件是否存在
	if _, err := os.Stat(filename); os.IsNotExist(err) {
		return err
	}
	fd, err := os.OpenFile(filename, os.O_RDWR|os.O_APPEND, 0666)
	defer fd.Close()
	if err != nil {
		return err
	}
	w := bufio.NewWriter(fd)
	_, err2 := w.WriteString(content)
	if err2 != nil {
		return err2
	}
	w.Flush()
	fd.Sync()
	return nil
}

不要忘了close文件

在这里插入图片描述

<think>根据用户需求,需要在Go语言中实时取日志文件,并按取,同时获取每的偏移量(即该文件中的起始位置)。这涉及到文件取、处理、偏移量记录等操作。 分析: 1. 实时取:意味着需要持续监控文件的变化(如新内容的追加),类似于`tail -f`的功能。 2. 按取:每次取一内容。 3. 获取偏移量:需要记录每一开始时的文件偏移量(字节位置)。 实现思路: - 使用`os.Open`打开文件,然后使用`bufio.Reader`来按取。 - 为了实时取,需要循环取,当文件末尾时,等待一段时间再尝试(或者使用文件通知机制,但Go标准库没有直接支持,通常使用轮询)。 - 记录偏移量:在取每一之前,我们可以通过`Seek`获取当前取位置,但是注意,`bufio.Reader`会缓冲取,所以直接使用`Seek`可能不准确。因此,我们可以在取每一之前记录偏移量,然后取一,再根据取的字节数更新偏移量。但这样需要自己控制缓冲。 另一种更直接的方法: 1. 打开文件后,使用`reader := bufio.NewReader(file)`。 2. 在每次之前,我们可以通过`file.Seek(0, io.SeekCurrent)`来获取当前文件指针的位置(注意:由于有缓冲,这个位置并不是实际取的位置,而是缓冲前的文件位置,所以我们需要在取后调整)。 3. 但是,由于`bufio.Reader`有缓冲,我们无法直接获取当前开始的位置。 因此,我们可以考虑不使用缓冲,自己按字节取并分。但是这样效率较低,且复杂。 推荐方法:自己管理偏移量 - 每次取一,我们可以先记录当前偏移量(即上一结束后的位置),然后取一,然后更新偏移量为当前偏移量加上该字节数(包括换符)。 - 但是,这样在取时,我们并不知道一有多少字节,直到取到换符。 具体步骤: 1. 打开文件,使用`os.Open`,注意不要用`os.OpenFile`,因为我们只需要取。 2. 初始化一个偏移量变量`offset`,初始为0。 3. 使用`file.Seek(offset, io.SeekStart)`定位到上次取的位置。 4. 使用`bufio.NewReader`(但注意,如果文件很大,我们可能不需要缓冲整个文件,所以可以每次重新创建reader)。 5. 循环:取一,记录当前的偏移量为`offset`(即这一开始的位置),然后更新`offset`为`offset`加上这一的字节数(包括换符)。 然而,在实时取中,我们可能无法预知文件何时增长,所以需要循环检测。 但是,有一个问题:当我们取到文件末尾时,文件可能被追加,我们需要等待。此时,我们记录当前的偏移量,然后等待一段时间(比如1秒)再尝试从该偏移量继续取。 注意:文件可能会被截断或者删除,需要处理这些异常情况。 实现步骤: 1. 打开文件,记录当前文件信息(如inode等,以便在文件被移动或删除后重新打开)。 2. 设置一个初始偏移量,如果文件是第一次打开,则从0开始;如果是重新打开(比如文件被轮转),则从新文件的开头开始(或者根据需求,可能需要从旧文件的末尾继续,但通常日志轮转会生成新文件,旧文件不再入,所以新文件需要从头取)。 3. 循环取: a. 定位到当前偏移量。 b. 创建一个新的`bufio.Reader`(因为每次定位后,需要新的reader,否则旧的reader可能有缓冲数据)。 c. 取一,如果取到,则处理该,并更新偏移量(当前偏移量+该字节数)。 d. 如果取到文件末尾(EOF),则等待一段时间,并检查文件是否有变化(比如文件被截断,或者被替换)。如果文件被替换(通过inode或文件大小变小等判断),则需要重新打开文件,并重置偏移量为0(或者根据情况处理)。 4. 处理完一后,记录该的偏移量(即该开始的位置)。 关键点:如何获取每的偏移量? - 在取一之前,我们通过`file.Seek(0, io.SeekCurrent)`获取当前的文件偏移量,这个偏移量就是这一开始的位置。但是,由于我们使用了`bufio.Reader`,它内部有缓冲,所以实际文件指针已经取到了缓冲区的末尾。因此,我们需要在每次之前获取位置,然后取一,然后更新我们自己的偏移量(通过加上取的字节数)来维护下一个位置。 然而,在之后,文件指针已经移动了,我们无法再获取该开始的位置。因此,我们必须在之前获取文件指针的位置,作为该的偏移量。 但是,由于`bufio.Reader`的缓冲,我们无法在之前精确知道文件指针的位置(因为缓冲可能已经预了一部分)。所以,我们需要自己管理文件指针,不使用缓冲,或者使用`ReadBytes`或`ReadString`,并自己计算字节数。 另一种做法:不使用缓冲,每次取一个字节直到换符。这样效率低,但对于实时日志处理,通常日志不会特别频繁,可以接受。 折中方法:使用`bufio.Reader`,但每次只取一个字节,直到遇到换符。这样我们可以自己计数。但是这样效率也不高。 更好的方法:在之前,我们记录当前的文件偏移量(通过`file.Seek(0, io.SeekCurrent)`),然后取一取后,我们记录该开始的位置就是当前偏移量,然后更新偏移量为当前偏移量加上取的字节数(包括换符)。但是,由于`bufio.Reader`的缓冲,我们通过`file.Seek`获取的位置并不是当前取的位置,而是缓冲之后的位置。因此,我们无法直接使用。 因此,我们需要自己管理缓冲:即不使用`bufio.Reader`,而是使用`file.Read`取到缓冲区,然后自己解析。这样我们可以自己控制偏移量。 具体步骤(自己管理): 1. 打开文件,设置一个缓冲区(比如4096字节)。 2. 从当前偏移量开始,取一块数据到缓冲区。 3. 在缓冲区中查找换符('\n')。 4. 如果找到,则处理这一(从缓冲区中取出0到换符的位置,包括换符),然后更新偏移量:当前偏移量 + 这一的字节数(在缓冲区中的位置+1)。 5. 如果没有找到,则说明这一块数据中没有完整的,需要将剩余的数据保留(移到缓冲区的开头),然后继续取下一块,直到找到换符。 这样,我们可以记录每一开始时的偏移量(即处理该之前,我们记录的偏移量就是该的起始位置)。 但是,这样需要自己处理缓冲,稍微复杂。 考虑到标准库中`bufio.Scanner`可以按扫描,但是它不提供偏移量。我们可以修改扫描的分割函数(split function)来记录偏移量。 使用`bufio.Scanner`并自定义split函数来记录偏移量: - 我们可以创建一个结构体,包含当前偏移量,并在split函数中更新它。 - 在split函数中,我们可以记录每一开始时的偏移量,然后扫描到换符,然后返回该数据,并更新偏移量。 示例split函数: ```go type offsetTracker struct { offset int64 } func (t *offsetTracker) splitFunc(data []byte, atEOF bool) (advance int, token []byte, err error) { // 记录当前的偏移量(在返回token之前,这个偏移量就是这一的开始位置) currentOffset := t.offset // 查找换符 if i := bytes.IndexByte(data, '\n'); i >= 0 { // 我们找到了换符,返回这一(包括换符) advance = i + 1 token = data[:advance] // 更新偏移量,加上这一的字节数 t.offset += int64(advance) // 返回当前的偏移量?但是split函数无法返回额外的信息(如偏移量) // 所以,我们可以在split函数外部,通过一个闭包来捕获这个currentOffset // 但是,split函数只返回token,不返回偏移量。所以我们需要在外部记录。 // 因此,我们可以在调用scanner.Scan()之后,从tracker中获取当前的偏移量?但是注意,split函数在扫描时调用,此时我们不知道哪一是当前的。 } // ... 其他情况处理 } ``` 但是,问题在于:我们无法在split函数外部同时获取当前的偏移量。因为split函数在内部调用,我们无法在Scan()之后立即得到该对应的偏移量。 因此,我们可以这样做: - 在split函数中,每当扫描到一时,我们将该的偏移量和内容一起输出。但是,`bufio.Scanner`的Scan()只返回一个token(即内容),不能同时返回偏移量。 另一种思路:不使用`bufio.Scanner`,而是使用`bufio.Reader`的`ReadBytes`或`ReadString`,并在每次取前记录文件位置(通过`file.Seek(0, io.SeekCurrent)`获取)。但是,由于缓冲,这个位置可能不准确。 考虑到实时日志取的性能要求,以及需要精确的偏移量,我们可以采用折中方案:使用`os.Read`,自己管理缓冲和分割,并记录偏移量。 下面,我们实现一个简单的版本,使用固定大小的缓冲区,自己处理分割和偏移量记录。 实现步骤: 1. 打开文件,获取文件句柄。 2. 设置一个缓冲区(比如4096字节)。 3. 初始化变量:`offset`(当前已处理的位置,即下一开始的位置)和`pending`(上一次未完成的,即没有换符的部分)。 4. 循环: a. 从当前偏移量开始,取一块数据到缓冲区。 b. 将缓冲区中的数据与之前pending的数据合并(如果pending有数据)。 c. 在合并后的数据中查找换符。 d. 对于每个完整的(以换符结尾),记录该的偏移量(即该开始的位置),然后处理该(输出或发送等),然后更新偏移量为该结束后的位置(即下一开始的位置)。 e. 将剩余的不完整部分(最后一个换符之后的数据)保存到pending,等待下一次取。 f. 如果没有文件末尾,则继续取;如果文件末尾,则等待一段时间(比如1秒)再尝试取(同时检查文件是否被截断或移动)。 注意:文件可能被轮转,即被重命名并创建了新的文件(相同的文件名)。我们需要检测这种情况(通过比较文件的inode或大小等),然后重新打开文件,并从新文件的开头开始取。 为了检测文件是否被轮转,我们可以记录文件的inode和大小,每次取前检查。 由于实现较为复杂,这里提供一个简化版本,不考虑文件轮转,只考虑文件追加的情况。 示例代码(简化版): ```go package main import ( "bytes" "fmt" "io" "log" "os" "time" ) func main() { // 打开日志文件 file, err := os.Open("example.log") if err != nil { log.Fatal(err) } defer file.Close() // 初始化变量 const bufferSize = 4096 buf := make([]byte, bufferSize) var offset int64 = 0 // 当前已取的偏移量(即下一开始的位置) var pending []byte // 暂存的不完整 // 如果文件不是新文件,可以从文件末尾开始(实时取新增内容),这里我们从文件开头取 // 如果要从末尾开始,则 offset, _ = file.Seek(0, io.SeekEnd) for { // 从当前偏移量开始取 n, err := file.Read(buf) if err != nil && err != io.EOF { log.Fatal(err) } if n == 0 { // 没有到数据,等待 time.Sleep(1 * time.Second) continue } // 处理取的数据 data := buf[:n] if len(pending) > 0 { // 将之前pending的数据和当前数据合并 data = append(pending, data...) pending = nil } // 在data中查找换符 start := 0 for { idx := bytes.IndexByte(data[start:], '\n') if idx < 0 { // 没有找到换符,将剩余数据存入pending pending = data[start:] break } // 找到换符,结束位置 end := start + idx + 1 // 包括换符 line := data[start:end] // 该的偏移量:offset + start lineOffset := offset + int64(start) // 处理这一 fmt.Printf("offset: %d, line: %s", lineOffset, line) // 更新start start = end } // 更新偏移量:已经处理了len(data) - len(pending)个字节 offset += int64(len(data) - len(pending)) // 如果文件末尾,等待 if err == io.EOF { time.Sleep(1 * time.Second) } } } ``` 注意:上述代码简化了文件轮转的处理,实际应用中需要检测文件是否被轮转(通过os.Stat获取文件信息,比较inode和修改时间等)。 如何检测文件轮转? - 在每次取前或取后,检查文件描述符对应的文件是否被替换(通过os.SameFile判断,或者比较inode)。 - 或者,在取到EOF时,尝试检查文件的大小,如果文件大小小于当前记录的偏移量,说明文件被截断或轮转(新文件),则需要重新打开文件,并将偏移量重置为0。 改进:在每次取前,检查文件是否被轮转(通过inode和device id),如果被轮转,则重新打开文件,并重置偏移量为0。 获取文件inode的方法(在Linux下): ```go fileInfo, err := file.Stat() if err != nil { // 处理错误 } sysInfo := fileInfo.Sys().(*syscall.Stat_t) inode := sysInfo.Ino ``` 但是,为了跨平台,我们可能需要使用条件编译。 由于实时日志取通常运在Linux环境下,我们可以使用条件编译。 这里,我们提供一个简单的检测方法:在每次取前,检查当前文件的大小,如果当前文件的大小小于我们记录的偏移量,则说明文件被轮转(新文件开始入,大小小于之前的偏移量)。注意,这种方法在文件被截断时也会触发。 在循环中,在取之前,我们可以加入: ```go fileInfo, err := file.Stat() if err != nil { // 处理错误,例如文件被删除,则重新打开 file.Close() file, err = os.Open("example.log") if err != nil { log.Fatal(err) } offset = 0 pending = nil continue } if fileInfo.Size() < offset { // 文件大小小于当前偏移量,说明文件被轮转或截断 // 重新打开文件,从0开始取 file.Close() file, err = os.Open("example.log") if err != nil { log.Fatal(err) } offset = 0 pending = nil continue } ``` 但是,注意:在取过程中,文件大小可能大于我们记录的偏移量,这是正常的(有新内容追加)。只有当文件大小小于偏移量时,才说明文件被重置。 另外,如果文件被删除,我们重新打开可能会失败,需要处理。 综上所述,我们可以实现一个实时取日志文件并记录每偏移量的程序。 相关问题: 1. 如何检测日志文件被轮转(log rotation)? 2. 在Go语言中,如何高效地取大文件并分? 3. 除了自己实现,有没有现成的库可以实现实时日志取并获取偏移量? 4. 如何将取的日志和偏移量发送到Kafka?</think>在 Go 语言中实时取日志文件并获取每偏移量,可以通过以下方法实现: ### 核心实现方案 ```go package main import ( "bufio" "fmt" "os" "time" ) func tailFile(filename string) { file, err := os.Open(filename) if err != nil { panic(err) } defer file.Close() // 获取初始偏移量(从文件末尾开始) offset, _ := file.Seek(0, os.SEEK_END) reader := bufio.NewReader(file) for { // 获取当前起始偏移量 currentOffset := offset line, err := reader.ReadBytes('\n') if err != nil { // 到达文件末尾,等待新内容 time.Sleep(500 * time.Millisecond) // 检查文件是否被轮转 (大小变小) if stat, _ := file.Stat(); stat.Size() < offset { file.Close() file, _ = os.Open(filename) offset = 0 reader = bufio.NewReader(file) } continue } // 更新偏移量 (包括换符) offset += int64(len(line)) // 输出结果:内容 + 起始偏移量 fmt.Printf("[%d] %s", currentOffset, line) } } func main() { tailFile("/var/log/app.log") } ``` ### 关键实现点说明 1. **偏移量获取原理** - 使用 `file.Seek(0, os.SEEK_END)` 初始定位到文件末尾 - 每取前记录 `currentOffset := offset` - 取后更新偏移量:`offset += int64(len(line))` - 这样 `currentOffset` 即为当前的起始字节位置 2. **实时取机制** - 取到 EOF 时休眠等待 (`time.Sleep`) - 使用 `bufio.Reader` 缓冲提高效率 - 循环持续检查新内容 3. **日志轮转处理** ```go // 检查文件大小是否小于当前偏移量 if stat, _ := file.Stat(); stat.Size() < offset { file.Close() file, _ = os.Open(filename) // 重新打开文件 offset = 0 // 重置偏移量 reader = bufio.NewReader(file) } ``` 当日志文件被轮转(如 logrotate)时,文件会被截断或替换,通过比较文件大小和记录的偏移量检测到这种变化 4. **性能优化** - 调整 `time.Sleep` 间隔平衡实时性和 CPU 消耗 - 使用 `bufio.Reader` 减少系统调用 - 避免频繁的 Stat 调用(可缓存文件信息) ### 处理注意事项 1. **跨日志**:日志系统可能生成跨多的条目,需额外处理(如基于时间戳或特殊标记识别) 2. **编码问题**:确保日志文件编码与取编码一致(特别是多语言日志) 3. **错误处理**:增加文件重试机制和错误报警 4. **资源释放**:使用 `defer` 确保文件描述符关闭 ### 完整示例增强版 ```go func enhancedTail(filename string) { var file *os.File var err error offset := int64(0) for { if file == nil { file, err = os.Open(filename) if err != nil { time.Sleep(2 * time.Second) continue } // 获取当前文件大小 if stat, err := file.Stat(); err == nil { offset = stat.Size() } file.Seek(offset, os.SEEK_SET) } reader := bufio.NewReader(file) line, err := reader.ReadBytes('\n') if err != nil { if err != io.EOF { file.Close() file = nil } time.Sleep(1 * time.Second) continue } currentOffset := offset offset += int64(len(line)) processLog(currentOffset, line) // 处理日志 } } func processLog(offset int64, line []byte) { // 发送到Kafka/ES或进其他处理 fmt.Printf("[%d] %s", offset, line) } ``` ### 典型应用场景 1. **实时日志分析**:结合 ELK 或 Splunk 实现实时监控 2. **日志告警系统**:检测特定错误模式并触发警报 3. **数据管道**:将日志流式传输到 Kafka/Pulsar 4. **审计跟踪**:精确记录日志位置用于合规审计
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值