引言
文件操作是任何编程语言都必须掌握的基础技能,Go 语言在这方面的设计简洁而强大。Go 的 I/O 操作主要围绕 io、os、ioutil、bufio 和 fmt 这几个核心包展开。标准库的设计遵循 Unix 哲学:一个工具做好一件事,通过组合实现复杂功能。
本文将系统性地介绍 Go 中的各类文件与 I/O 操作,从基础的读写文件,到高级的缓冲 I/O 和目录操作,再到实际生产环境中的日志分析工具实现。
一、os 包基础文件操作
1.1 文件创建与打开
Go 的 os 包提供了最底层的文件操作函数:
package main
import (
"os"
"fmt"
)
func main() {
// Create 创建文件(如果存在则截断)
file, err := os.Create("test.txt")
if err != nil {
panic(err)
}
defer file.Close()
fmt.Println("文件创建成功")
// Open 以只读方式打开文件
file, err = os.Open("test.txt")
if err != nil {
panic(err)
}
defer file.Close()
// OpenFile 更通用的打开方式
// 第二个参数是模式:O_RDONLY, O_WRONLY, O_RDWR, O_APPEND, O_CREATE, O_EXCL, O_TRUNC
file, err = os.OpenFile("test.txt", os.O_RDWR|os.O_APPEND, 0644)
if err != nil {
panic(err)
}
defer file.Close()
}
1.2 文件读写
使用 os.File 的 Read 和 Write 方法:
func fileReadWrite() error {
// 写入数据
file, err := os.Create("data.txt")
if err != nil {
return err
}
defer file.Close()
data := []byte("Hello, Go文件操作!\n第二行数据")
n, err := file.Write(data)
if err != nil {
return err
}
fmt.Printf("成功写入 %d 字节\n", n)
// 使用 WriteString 写入字符串
n, err = file.WriteString("第三行数据\n")
if err != nil {
return err
}
fmt.Printf("成功写入字符串 %d 字节\n", n)
// 读取数据
file.Seek(0, 0) // 将指针移到文件开头
content := make([]byte, 1024)
n, err = file.Read(content)
if err != nil && err != io.EOF {
return err
}
fmt.Printf("读取到: %s\n", content[:n])
return nil
}
1.3 文件指针操作
os.File 内部维护一个文件指针,表示当前读写位置:
func fileSeeking() error {
file, err := os.Open("data.txt")
if err != nil {
return err
}
defer file.Close()
// 获取当前指针位置
offset, err := file.Seek(0, io.SeekCurrent)
if err != nil {
return err
}
fmt.Printf("当前偏移: %d\n", offset)
// Seek 设置指针位置
// 第二个参数:0=起始位置, 1=当前位置, 2=结束位置
_, err = file.Seek(10, io.SeekStart)
if err != nil {
return err
}
// 获取新的偏移量
offset, err = file.Seek(0, io.SeekCurrent)
if err != nil {
return err
}
fmt.Printf("移动后偏移: %d\n", offset)
return nil
}
二、ioutil 与便捷读写函数
2.1 完整读取文件
ioutil 包提供了更便捷的高级函数:
import (
"io/ioutil"
"fmt"
)
func readAllFile() error {
// ReadFile 读取整个文件(适合小文件)
content, err := ioutil.ReadFile("data.txt")
if err != nil {
return err
}
fmt.Printf("文件内容:\n%s\n", string(content))
return nil
}
ReadFile 底层原理:
// ioutil.ReadFile 的简化实现
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
// 尝试获取文件大小
stat, err := f.Stat()
if err != nil {
return nil, err
}
// 如果文件大小已知,预分配内存
size := stat.Size()
if size == 0 {
return []byte{}, nil
}
data := make([]byte, size)
for {
n, err := f.Read(data)
if n > 0 {
data = append(data, data[:n]...)
}
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
}
return data, nil
}
2.2 完整写入文件
func writeFile() error {
data := []byte("这是要写入的内容\n第二行")
// WriteFile 写入整个文件(如果文件存在则覆盖)
err := ioutil.WriteFile("output.txt", data, 0644)
if err != nil {
return err
}
fmt.Println("文件写入成功")
return nil
}
2.3 临时文件与目录
func tempFileDemo() error {
// TempDir 创建临时目录
dir, err := ioutil.TempDir("", "myapp-*")
if err != nil {
return err
}
defer os.RemoveAll(dir) // 使用完清理
fmt.Printf("创建临时目录: %s\n", dir)
// TempFile 创建临时文件
file, err := ioutil.TempFile(dir, "data-*.txt")
if err != nil {
return err
}
defer os.Remove(file.Name()) // 清理文件
// 写入临时文件
file.WriteString("临时数据")
file.Close()
fmt.Printf("创建临时文件: %s\n", file.Name())
return nil
}
三、缓冲 I/O(bufio)
3.1 为什么需要缓冲 I/O
直接的文件读写每次都会触发系统调用,对于大量小数据量的操作性能很差。bufio 通过在内存中维护缓冲区来减少系统调用次数:
无缓冲 I/O:
应用 -> [read() syscall] -> 内核 -> 磁盘 (每次读取一个字节)
应用 -> [read() syscall] -> 内核 -> 磁盘 (每次读取一个字节)
...
有缓冲 I/O:
应用 -> 读取缓冲区 -> [read() syscall] -> 内核 -> 磁盘 (一次性读取大量数据)
应用 -> 读取缓冲区 (内存操作,极快)
应用 -> 读取缓冲区 (内存操作,极快)
...
3.2 bufio.Reader
func bufferedRead() error {
file, err := os.Open("largefile.txt")
if err != nil {
return err
}
defer file.Close()
// 创建缓冲读取器,8KB 缓冲区
reader := bufio.NewReaderSize(file, 8*1024)
// 按行读取
for {
line, err := reader.ReadString('\n')
if err != nil && err != io.EOF {
return err
}
fmt.Print(line)
if err == io.EOF {
break
}
}
return nil
}
bufio.Reader 的主要方法:
// Read 从缓冲区读取数据
func (b *Reader) Read(p []byte) (n int, err error)
// ReadByte 读取单个字节
func (b *Reader) ReadByte() (byte, error)
// ReadBytes 读取直到指定分隔符
func (b *Reader) ReadBytes(delim byte) ([]byte, error)
// ReadString 读取直到指定分隔符,返回字符串
func (b *Reader) ReadString(delim byte) (string, error)
// ReadLine 读取一行(不建议使用,更推荐 ReadBytes)
func (b *Reader) ReadLine() ([]byte, bool, error)
// ReadSlice 读取直到分隔符
func (b *Reader) ReadSlice(delim byte) ([]byte, error)
// Peek 返回缓冲区中的前 n 个字节,不移动指针
func (b *Reader) Peek(n int) ([]byte, error)
// Discard 跳过前 n 个字节
func (b *Reader) Discard(n int) (discarded int, err error)
3.3 bufio.Writer
func bufferedWrite() error {
file, err := os.Create("output.txt")
if err != nil {
return err
}
defer file.Close()
// 创建缓冲写入器
writer := bufio.NewWriterSize(file, 8*1024)
// 写入数据
for i := 0; i < 1000; i++ {
_, err := writer.WriteString(fmt.Sprintf("第 %d 行数据\n", i))
if err != nil {
return err
}
}
// 重要:刷新缓冲区,确保所有数据写入文件
err = writer.Flush()
if err != nil {
return err
}
return nil
}
3.4 Scanner 逐行处理
对于按行分割的数据,bufio.Scanner 是最简洁的 API:
func lineCounter(filename string) (int, error) {
file, err := os.Open(filename)
if err != nil {
return 0, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
// 可选:设置缓冲区大小(默认 64KB)
const maxCapacity = 1024 * 1024 // 1MB
buf := make([]byte, maxCapacity)
scanner.Buffer(buf, maxCapacity)
// 可选:设置分割函数(默认按行分割)
// scanner.Split(bufio.ScanLines)
count := 0
for scanner.Scan() {
count++
}
if err := scanner.Err(); err != nil {
return 0, err
}
return count, nil
}
Scanner 工作原理:
数据流向:
磁盘 -> 内核缓冲区 -> 用户缓冲区(64KB) -> Scanner缓冲区(<=64KB) -> 应用程序
Scanner 内部维护两个缓冲区:
1. 用户缓冲区(Read Bytes):从内核读取的大块数据
2. Token 缓冲区:当前正在处理的行
当 token 超过 64KB 时,需要使用 Buffer() 扩展
四、格式化 I/O
4.1 fmt 包的格式化输出
func fmtDemo() {
// 基础格式化
fmt.Printf("字符串: %s, 整数: %d, 浮点: %.2f\n", "hello", 42, 3.14159)
// 常用动词
// %v 默认格式
// %+v 结构体时显示字段名
// %#v Go 语法表示
// %T 类型
// %% 转义百分号
type User struct {
Name string
Age int
}
user := User{"Alice", 30}
fmt.Printf("%v\n", user) // {Alice 30}
fmt.Printf("%+v\n", user) // {Name:Alice Age:30}
fmt.Printf("%#v\n", user) // main.User{Name:"Alice", Age:30}
fmt.Printf("%T\n", user) // main.User
// 宽度和对齐
fmt.Printf("|%6s|%6d|%6.2f|\n", "Hello", 42, 3.14) // | Hello| 42| 3.14|
fmt.Printf("|%-6s|%-6d|%-6.2f|\n", "Hello", 42, 3.14) // |Hello |42 |3.14 |
// 进制转换
fmt.Printf("十进制: %d\n", 255)
fmt.Printf("二进制: %b\n", 255)
fmt.Printf("十六进制: %x\n", 255)
fmt.Printf("八进制: %o\n", 255)
}
4.2 fmt 包的格式化输入
func fmtScanDemo() {
// 从标准输入扫描
var name string
var age int
var salary float64
fmt.Print("请输入姓名、年龄、薪资: ")
// Scan 从空白分隔的输入中读取
fmt.Scan(&name, &age, &salary)
fmt.Printf("姓名: %s, 年龄: %d, 薪资: %.2f\n", name, age, salary)
// Scanf 按格式解析
fmt.Print("请按格式输入(姓名,年龄,薪资): ")
fmt.Scanf("%s,%d,%f", &name, &age, &salary)
fmt.Printf("姓名: %s, 年龄: %d, 薪资: %.2f\n", name, age, salary)
// Scanln 读取一行(知道行尾)
fmt.Println("请输入姓名和年龄:")
fmt.Scanln(&name, &age)
fmt.Printf("姓名: %s, 年龄: %d\n", name, age)
}
4.3 bufio + fmt 扫描文件
func scanFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
var name string
var age int
var score float64
var totalScore float64
var count int
for scanner.Scan() {
line := scanner.Text()
// 跳过空行和注释
if len(line) == 0 || line[0] == '#' {
continue
}
// 解析数据
_, err := fmt.Sscanf(line, "%s %d %f", &name, &age, &score)
if err != nil {
continue
}
totalScore += score
count++
fmt.Printf("学生: %s, 年龄: %d, 成绩: %.2f\n", name, age, score)
}
if count > 0 {
fmt.Printf("平均成绩: %.2f\n", totalScore/float64(count))
}
return scanner.Err()
}
五、目录操作与文件遍历
5.1 目录基本操作
import (
"os"
"path/filepath"
)
func dirOperations() error {
// 创建目录
err := os.Mkdir("testdir", 0755)
if err != nil && !os.IsExist(err) {
return err
}
// 创建多层目录
err = os.MkdirAll("a/b/c/d", 0755)
if err != nil {
return err
}
// 读取目录内容
entries, err := os.ReadDir(".")
if err != nil {
return err
}
for _, entry := range entries {
fmt.Printf("%s\t", entry.Name())
if entry.IsDir() {
fmt.Printf("[DIR]")
} else {
info, _ := entry.Info()
fmt.Printf("[FILE] %d bytes", info.Size())
}
fmt.Println()
}
// 删除目录
err = os.Remove("testdir")
if err != nil && !os.IsNotExist(err) {
return err
}
// 删除目录树
err = os.RemoveAll("a")
if err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
5.2 递归遍历目录
使用 filepath.Walk 或 filepath.WalkDir:
// Walk 遍历目录树
func walkDemo() error {
return filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 获取相对路径
relPath, _ := filepath.Rel(".", path)
// 跳过隐藏文件和目录
if strings.HasPrefix(filepath.Base(path), ".") {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
// 打印结构
indent := strings.Count(relPath, string(filepath.Separator))
for i := 0; i < indent; i++ {
fmt.Print(" ")
}
if info.IsDir() {
fmt.Printf("📁 %s/\n", info.Name())
} else {
fmt.Printf("📄 %s (%d bytes)\n", info.Name(), info.Size())
}
return nil
})
}
// WalkDir 更高效(不调用 Stat 对于目录)
func walkDirDemo() error {
return filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
info, err := d.Info()
if err != nil {
return err
}
fmt.Printf("%s: %d bytes\n", path, info.Size())
return nil
})
}
5.3 查找特定文件
func findFiles(root, pattern string) ([]string, error) {
var matches []string
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 匹配文件名模式
matched, err := filepath.Match(pattern, info.Name())
if err != nil {
return err
}
if matched {
absPath, _ := filepath.Abs(path)
matches = append(matches, absPath)
}
return nil
})
return matches, err
}
六、文件权限与属性
6.1 文件权限
Go 使用 Unix 风格的权限模型:
func permissionDemo() error {
// 创建文件并设置权限
file, err := os.OpenFile("secure.txt", os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
return err
}
file.Close()
// 修改文件权限
err = os.Chmod("secure.txt", 0644)
if err != nil {
return err
}
// 修改目录权限
err = os.Mkdir("rwx", 0755)
if err != nil {
return err
}
err = os.Chmod("rwx", 0700)
return nil
}
6.2 文件属性获取
func fileInfoDemo(filename string) error {
info, err := os.Stat(filename)
if err != nil {
return err
}
fmt.Printf("文件名: %s\n", info.Name())
fmt.Printf("大小: %d bytes\n", info.Size())
fmt.Printf("权限: %o\n", info.Mode().Perm())
fmt.Printf("是否目录: %t\n", info.IsDir())
fmt.Printf("修改时间: %s\n", info.ModTime().Format("2006-01-02 15:04:05"))
// 获取更详细的权限信息
mode := info.Mode()
fmt.Printf("是常规文件: %t\n", mode.IsRegular())
fmt.Printf("是目录: %t\n", mode.IsDir())
fmt.Printf("是符号链接: %t\n", mode&os.ModeSymlink != 0)
return nil
}
6.3 文件时间戳
func fileTimeDemo() error {
// 获取文件访问和修改时间
info, err := os.Stat("data.txt")
if err != nil {
return err
}
modTime := info.ModTime()
accessTime := statAtime(info.Sys().(*syscall.Stat_t))
fmt.Printf("修改时间: %s\n", modTime)
fmt.Printf("访问时间: %s\n", accessTime)
// 设置文件时间戳(仅 Unix)
// os.Chtimes("data.txt", time.Now(), time.Now())
return nil
}
七、实战案例:日志分析工具
7.1 需求分析
我们需要实现一个日志分析工具,具备以下功能:
-
多格式支持:支持 Nginx、Apache JSON 格式日志
-
实时统计:访问量、状态码分布、Top N IP
-
模式匹配:支持正则表达式过滤
-
性能优化:使用 goroutine 并行处理
7.2 完整实现
package main
import (
"bufio"
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"mime"
"mime/quotedprintable"
"net"
"os"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
)
// 日志条目
type LogEntry struct {
Timestamp time.Time
Method string
Path string
Status int
Size int64
ClientIP string
UserAgent string
Referer string
Protocol string
ResponseTime float64
}
// 统计信息
type Stats struct {
TotalRequests int64
TotalBytes int64
StatusCounts map[int]int64
MethodCounts map[string]int64
TopIPs map[string]int64
TopPaths map[string]int64
mu sync.RWMutex
}
func NewStats() *Stats {
return &Stats{
StatusCounts: make(map[int]int64),
MethodCounts: make(map[string]int64),
TopIPs: make(map[string]int64),
TopPaths: make(map[string]int64),
}
}
func (s *Stats) Increment(status int, method, ip, path string, size int64) {
atomic.AddInt64(&s.TotalRequests, 1)
atomic.AddInt64(&s.TotalBytes, size)
s.mu.Lock()
s.StatusCounts[status]++
s.MethodCounts[method]++
s.TopIPs[ip]++
s.TopPaths[path]++
s.mu.Unlock()
}
type LogParser struct {
stats *Stats
pattern *regexp.Regexp
workers int
entryChan chan *LogEntry
resultChan chan *Stats
ctx context.Context
cancel context.CancelFunc
}
func NewLogParser(workers int) *LogParser {
ctx, cancel := context.WithCancel(context.Background())
return &LogParser{
stats: NewStats(),
workers: workers,
entryChan: make(chan *LogEntry, 10000),
resultChan: make(chan *Stats, workers),
ctx: ctx,
cancel: cancel,
}
}
// Nginx 日志格式解析
// 192.168.1.1 - - [10/Oct/2026:13:55:36 +0000] "GET /api/users HTTP/1.1" 200 1234 "http://example.com" "Mozilla/5.0"
var nginxPattern = regexp.MustCompile(`(?P<ip>[\d\.]+) - \S+ \[(?P<timestamp>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) (?P<protocol>\S+)" (?P<status>\d+) (?P<size>\d+) "(?P<referer>[^"]*)" "(?P<user_agent>[^"]*)"`)
// Apache 日志格式解析
// 127.0.0.1 - frank [10/Oct/2026:13:55:36 +0000] "GET /apache_pb.gif HTTP/1.0" 200 2326
var apachePattern = regexp.MustCompile(`(?P<ip>[\d\.]+) \S+ \S+ \[(?P<timestamp>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) (?P<protocol>\S+)" (?P<status>\d+) (?P<size>\d+)`)
// JSON 日志格式解析
func parseJSONLog(line []byte) (*LogEntry, error) {
var entry LogEntry
if err := json.Unmarshal(line, &entry); err != nil {
return nil, err
}
return &entry, nil
}
func (p *LogParser) parseNginxLog(line string) (*LogEntry, error) {
matches := nginxPattern.FindStringSubmatch(line)
if matches == nil {
return nil, fmt.Errorf("无法解析日志行")
}
timestamp, err := time.Parse("02/Jan/2006:15:04:05 -0700", nginxPattern.SubexpNames()[1])
if err != nil {
// 尝试 RFC3339 格式
timestamp = time.Now()
}
entry := &LogEntry{
Timestamp: timestamp,
Method: nginxPattern.SubexpNames()[2],
Path: nginxPattern.SubexpNames()[3],
Protocol: nginxPattern.SubexpNames()[4],
}
fmt.Sscanf(nginxPattern.SubexpNames()[5], "%d", &entry.Status)
fmt.Sscanf(nginxPattern.SubexpNames()[6], "%d", &entry.Size)
entry.ClientIP = nginxPattern.SubexpNames()[0]
entry.Referer = nginxPattern.SubexpNames()[7]
entry.UserAgent = nginxPattern.SubexpNames()[8]
return entry, nil
}
func (p *LogParser) parseLine(line string) (*LogEntry, error) {
// JSON 格式检测
if len(line) > 0 && line[0] == '{' {
return parseJSONLog([]byte(line))
}
// 尝试 Nginx 格式
if strings.Contains(line, "[") && strings.Contains(line, "\"GET") {
return p.parseNginxLog(line)
}
return nil, fmt.Errorf("未知格式")
}
func (p *LogParser) processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return fmt.Errorf("打开文件失败 %s: %w", filename, err)
}
defer file.Close()
// 使用 Scanner 按行处理
scanner := bufio.NewScanner(file)
buf := make([]byte, 0, 64*1024)
scanner.Buffer(buf, 1024*1024)
lineNum := 0
for scanner.Scan() {
lineNum++
line := scanner.Text()
// 跳过空行
if len(strings.TrimSpace(line)) == 0 {
continue
}
// 正则过滤
if p.pattern != nil && !p.pattern.MatchString(line) {
continue
}
entry, err := p.parseLine(line)
if err != nil {
log.Printf("解析错误 [文件: %s, 行: %d]: %v", filename, lineNum, err)
continue
}
p.stats.Increment(entry.Status, entry.Method, entry.ClientIP, entry.Path, entry.Size)
}
return scanner.Err()
}
func (p *LogParser) processFileWorker(files <-chan string) {
localStats := NewStats()
for file := range files {
err := p.parseFileStats(file, localStats)
if err != nil {
log.Printf("处理文件失败: %s, 错误: %v", file, err)
}
}
p.resultChan <- localStats
}
func (p *LogParser) parseFileStats(filename string, stats *Stats) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if len(strings.TrimSpace(line)) == 0 {
continue
}
if p.pattern != nil && !p.pattern.MatchString(line) {
continue
}
entry, err := p.parseLine(line)
if err != nil {
continue
}
stats.Increment(entry.Status, entry.Method, entry.ClientIP, entry.Path, entry.Size)
}
return scanner.Err()
}
func (p *LogParser) mergeStats(workerStats []*Stats) {
for _, ws := range workerStats {
p.stats.mu.Lock()
for k, v := range ws.StatusCounts {
p.stats.StatusCounts[k] += v
}
for k, v := range ws.MethodCounts {
p.stats.MethodCounts[k] += v
}
for k, v := range ws.TopIPs {
p.stats.TopIPs[k] += v
}
for k, v := range ws.TopPaths {
p.stats.TopPaths[k] += v
}
p.stats.mu.Unlock()
}
}
func (p *LogParser) Parse(pattern string, paths []string) error {
// 编译过滤模式
if pattern != "" {
p.pattern = regexp.MustCompile(pattern)
}
// 收集所有日志文件
var files []string
for _, path := range paths {
info, err := os.Stat(path)
if err != nil {
return err
}
if info.IsDir() {
dirFiles, err := collectLogFiles(path)
if err != nil {
return err
}
files = append(files, dirFiles...)
} else {
files = append(files, path)
}
}
if len(files) == 0 {
return fmt.Errorf("没有找到日志文件")
}
log.Printf("找到 %d 个日志文件,使用 %d 个工作协程", len(files), p.workers)
// 创建文件通道
fileChan := make(chan string, len(files))
for _, f := range files {
fileChan <- f
}
close(fileChan)
// 启动工作协程
var wg sync.WaitGroup
for i := 0; i < p.workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
p.processFileWorker(fileChan)
}()
}
// 等待所有工作完成并收集结果
go func() {
wg.Wait()
close(p.resultChan)
}()
var workerStats []*Stats
for stats := range p.resultChan {
workerStats = append(workerStats, stats)
}
p.mergeStats(workerStats)
return nil
}
func collectLogFiles(dir string) ([]string, error) {
var files []string
pattern := filepath.Join(dir, "*.log")
matches, err := filepath.Glob(pattern)
if err != nil {
return nil, err
}
files = append(files, matches...)
// 递归处理子目录
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
for _, entry := range entries {
if entry.IsDir() {
subFiles, err := collectLogFiles(filepath.Join(dir, entry.Name()))
if err != nil {
continue
}
files = append(files, subFiles...)
}
}
return files, nil
}
func (s *Stats) Print() {
s.mu.Lock()
defer s.mu.Unlock()
fmt.Println("\n" + strings.Repeat("=", 60))
fmt.Println(" 日志分析报告")
fmt.Println(strings.Repeat("=", 60))
fmt.Printf("\n总请求数: %d\n", atomic.LoadInt64(&s.TotalRequests))
fmt.Printf("总流量: %s\n", formatBytes(atomic.LoadInt64(&s.TotalBytes)))
fmt.Println("\n--- 状态码分布 ---")
var statusCodes []int
for code := range s.StatusCounts {
statusCodes = append(statusCodes, code)
}
sort.Ints(statusCodes)
for _, code := range statusCodes {
count := s.StatusCounts[code]
pct := float64(count) / float64(atomic.LoadInt64(&s.TotalRequests)) * 100
fmt.Printf(" %d: %d (%.1f%%) %s\n", code, count, pct, statusCodeDesc(code))
}
fmt.Println("\n--- 请求方法分布 ---")
for method, count := range s.MethodCounts {
pct := float64(count) / float64(atomic.LoadInt64(&s.TotalRequests)) * 100
fmt.Printf(" %s: %d (%.1f%%)\n", method, count, pct)
}
fmt.Println("\n--- Top 10 IP 地址 ---")
printTopN(s.TopIPs, 10)
fmt.Println("\n--- Top 10 请求路径 ---")
printTopN(s.TopPaths, 10)
fmt.Println(strings.Repeat("=", 60))
}
func printTopN(m map[string]int64, n int) {
type kv struct {
Key string
Value int64
}
var ss []kv
for k, v := range m {
ss = append(ss, kv{k, v})
}
sort.Slice(ss, func(i, j int) bool {
return ss[i].Value > ss[j].Value
})
for i := 0; i < n && i < len(ss); i++ {
fmt.Printf(" %s: %d\n", ss[i].Key, ss[i].Value)
}
}
func statusCodeDesc(code int) string {
switch {
case code >= 200 && code < 300:
return "成功"
case code >= 300 && code < 400:
return "重定向"
case code >= 400 && code < 500:
return "客户端错误"
case code >= 500:
return "服务端错误"
default:
return "未知"
}
}
func formatBytes(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
}
func main() {
workers := flag.Int("w", runtime.NumCPU(), "工作协程数量")
pattern := flag.String("filter", "", "正则表达式过滤")
flag.Parse()
paths := flag.Args()
if len(paths) == 0 {
fmt.Println("用法: loganalyzer [选项] <日志文件或目录>")
flag.PrintDefaults()
os.Exit(1)
}
parser := NewLogParser(*workers)
start := time.Now()
if err := parser.Parse(*pattern, paths); err != nil {
log.Fatalf("分析失败: %v", err)
}
parser.stats.Print()
fmt.Printf("\n分析耗时: %v\n", time.Since(start))
}
7.3 使用示例与测试
创建测试日志文件:
# 创建测试数据
mkdir -p logs
cat > logs/access.log << 'EOF'
192.168.1.1 - - [10/Oct/2026:13:55:36 +0000] "GET /api/users HTTP/1.1" 200 1234 "http://example.com" "Mozilla/5.0"
192.168.1.2 - - [10/Oct/2026:13:55:37 +0000] "POST /api/login HTTP/1.1" 200 512 "http://example.com" "Mozilla/5.0"
192.168.1.1 - - [10/Oct/2026:13:55:38 +0000] "GET /api/users/123 HTTP/1.1" 404 256 "http://example.com" "Mozilla/5.0"
192.168.1.3 - - [10/Oct/2026:13:55:39 +0000] "GET /static/app.js HTTP/1.1" 200 10240 "http://example.com" "Mozilla/5.0"
192.168.1.1 - - [10/Oct/2026:13:55:40 +0000] "GET /api/products HTTP/1.1" 500 128 "http://example.com" "Mozilla/5.0"
EOF
运行分析工具:
go run main.go logs/
# 带过滤条件的分析
go run main.go -filter "/api/" logs/
# 指定工作协程数
go run main.go -w 8 logs/
7.4 性能优化要点
1. 缓冲区大小选择
// 小文件:默认缓冲区足够
scanner := bufio.NewScanner(file)
// 大文件:增加缓冲区避免内存压力
scanner := bufio.NewScanner(file)
buf := make([]byte, 0, 64*1024) // 64KB
scanner.Buffer(buf, 1024*1024) // 最大 token 1MB
2. 并行处理策略
文件级并行:
File1 -> Worker1 -> Stats1
File2 -> Worker2 -> Stats2
File3 -> Worker3 -> Stats3
↓
Merge Results
行级并行(需要更复杂的实现):
Scanner -> Channel -> Workers -> Reduce
3. 内存优化
// 避免在循环中分配大对象
for scanner.Scan() {
// 复用buffer
line := scanner.Bytes()
process(line) // 使用字节切片而非字符串拷贝
}
八、实战案例:配置文件的读写
8.1 INI 配置文件
package config
import (
"bufio"
"fmt"
"os"
"regexp"
"strings"
)
type Config struct {
sections map[string]map[string]string
}
func NewConfig() *Config {
return &Config{
sections: make(map[string]map[string]string),
}
}
func (c *Config) Load(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
var currentSection string
sectionRE := regexp.MustCompile(`^\[(.+)\]$`)
kvRE := regexp.MustCompile(`^([^=]+)=(.*)$`)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
// 跳过空行和注释
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
continue
}
// 解析节
if matches := sectionRE.FindStringSubmatch(line); matches != nil {
currentSection = matches[1]
if _, ok := c.sections[currentSection]; !ok {
c.sections[currentSection] = make(map[string]string)
}
continue
}
// 解析键值对
if matches := kvRE.FindStringSubmatch(line); matches != nil {
key := strings.TrimSpace(matches[1])
value := strings.TrimSpace(matches[2])
value = strings.Trim(value, "\"")
if currentSection == "" {
currentSection = "default"
c.sections[currentSection] = make(map[string]string)
}
c.sections[currentSection][key] = value
}
}
return scanner.Err()
}
func (c *Config) Save(filename string) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
writer := bufio.NewWriter(file)
for section, kv := range c.sections {
fmt.Fprintf(writer, "[%s]\n", section)
for k, v := range kv {
fmt.Fprintf(writer, "%s = %q\n", k, v)
}
fmt.Fprintln(writer)
}
return writer.Flush()
}
func (c *Config) Get(section, key string) string {
if s, ok := c.sections[section]; ok {
if v, ok := s[key]; ok {
return v
}
}
return ""
}
func (c *Config) Set(section, key, value string) {
if _, ok := c.sections[section]; !ok {
c.sections[section] = make(map[string]string)
}
c.sections[section][key] = value
}
总结
Go 的 I/O 系统设计得非常优雅,通过组合不同的包和接口,我们可以构建出高效、灵活的文件处理方案:
-
基础 I/O :使用
os包进行底层的文件操作 -
便捷函数 :
ioutil包提供常用的高层操作 -
缓冲 I/O :
bufio包优化大量小读写的性能 -
格式化 I/O :
fmt包处理格式化的输入输出 -
目录操作 :
os和path/filepath包处理目录和路径
在实际项目中,选择合适的 I/O 方式需要考虑:
-
数据量大小:大文件使用流式处理,避免一次性加载
-
性能要求 :频繁小读写使用
bufio,偶尔一次用ioutil -
并发安全 :文件操作本身非线程安全,需要加锁或使用
sync.Mutex -
错误处理:始终检查返回值,特别是 I/O 操作