【Go语言学习系列12】第一阶段项目实战:命令行工具

📚 原创系列: “Go语言学习系列”

🔄 转载说明: 本文最初发布于"Gopher部落"微信公众号,经原作者授权转载。

🔗 关注原创: 欢迎扫描文末二维码,关注"Gopher部落"微信公众号获取第一手Go技术文章。

📑 Go语言学习系列导航

本文是【Go语言学习系列】的第12篇,当前位于第一阶段(入门篇)

🚀 第一阶段:入门篇
  1. Go语言简介与环境搭建
  2. Go开发工具链介绍
  3. Go基础语法(一):变量与数据类型
  4. Go基础语法(二):流程控制
  5. Go基础语法(三):函数
  6. Go基础语法(四):数组与切片
  7. Go基础语法(五):映射
  8. Go基础语法(六):结构体
  9. Go基础语法(七):指针
  10. Go基础语法(八):接口
  11. 错误处理与异常
  12. 第一阶段项目实战:命令行工具 👈 当前位置

📚 查看完整Go语言学习系列导航

📖 文章导读

在本文中,您将学习:

  • 如何设计和构建实用的Go命令行应用
  • 项目规范组织与模块划分的最佳实践
  • 命令行参数解析与用户交互设计
  • 文件系统操作与数据处理技巧
  • 并发模式在实际项目中的应用
  • 单元测试编写与代码质量保障

通过实战开发一个文件分析和处理工具,本文将帮助您将前面学到的所有Go语言基础知识融会贯通,掌握从需求分析到功能实现的完整开发流程。

Go命令行项目架构


第一阶段项目实战:命令行工具开发指南

在学习了Go语言的基础语法后,最好的巩固方式就是实战项目开发。本文将带您开发一个实用的文件处理命令行工具,它能帮助用户快速分析目录结构、搜索文件内容、批量处理文件等。通过这个项目,我们将综合运用前面所学的知识,体验真实的Go项目开发流程。

一、项目需求与设计

1.1 项目概述

我们将开发一个名为gofiles的命令行工具,它具有以下核心功能:

  1. 文件查找:根据名称、大小、修改时间等条件搜索文件
  2. 内容搜索:在文件内容中搜索指定文本或正则表达式
  3. 目录分析:统计目录中各类文件的数量、大小分布等信息
  4. 批量操作:对符合条件的文件执行重命名、移动等批量操作

这个工具将帮助用户在日常工作中快速处理文件相关任务,特别适合开发人员、系统管理员等技术用户。

1.2 技术选型

我们将使用以下技术和库来实现这个项目:

  1. 标准库

    • ospath/filepath:文件系统操作
    • regexp:正则表达式匹配
    • syncsync/atomic:并发控制
    • context:操作超时控制
    • testing:单元测试
  2. 第三方库

    • github.com/spf13/cobra:命令行界面构建
    • github.com/spf13/viper:配置管理
    • github.com/fatih/color:彩色终端输出
    • github.com/schollz/progressbar/v3:进度条展示

1.3 系统架构

我们将按照以下架构组织代码:

gofiles/
├── cmd/            # 命令行入口
│   ├── root.go     # 主命令
│   ├── find.go     # 查找命令
│   ├── search.go   # 搜索命令
│   ├── stats.go    # 统计命令
│   └── batch.go    # 批处理命令
├── internal/       # 内部包
│   ├── finder/     # 文件查找逻辑
│   ├── searcher/   # 内容搜索逻辑
│   ├── analyzer/   # 文件分析逻辑
│   ├── processor/  # 文件处理逻辑
│   └── utils/      # 通用工具函数
├── main.go         # 应用入口
├── go.mod          # 模块定义
├── go.sum          # 依赖版本锁定
└── README.md       # 项目文档

这种架构遵循了Go项目的常见最佳实践:

  • 使用cmd目录组织命令行界面
  • 使用internal目录存放不对外暴露的包
  • 核心逻辑按功能模块划分为不同包
  • 清晰的依赖管理

1.4 工作流程

该工具的基本工作流程如下:

  1. 用户通过命令行参数指定操作类型和参数
  2. 程序解析命令行参数并验证
  3. 根据参数确定遍历范围和过滤条件
  4. 并发扫描文件系统,对符合条件的文件执行操作
  5. 实时反馈处理进度和结果
  6. 完成后输出汇总信息

下面是一个简化的流程图:

用户输入 -> 参数解析 -> 确定扫描范围 -> 并发文件遍历 -> 条件过滤 -> 执行操作 -> 结果输出

二、项目初始化与基础设施

2.1 创建Go模块

首先,我们需要初始化一个Go模块:

# 创建项目目录
mkdir -p gofiles
cd gofiles

# 初始化Go模块
go mod init github.com/yourusername/gofiles

# 创建基础目录结构
mkdir -p cmd internal

2.2 添加依赖

接下来,添加项目依赖:

# 添加cobra库用于命令行处理
go get github.com/spf13/cobra

# 添加viper库用于配置管理
go get github.com/spf13/viper

# 添加color库用于彩色输出
go get github.com/fatih/color

# 添加进度条库
go get github.com/schollz/progressbar/v3

2.3 创建主程序入口

创建main.go文件:

package main

import (
	"fmt"
	"os"
	
	"github.com/yourusername/gofiles/cmd"
)

func main() {
	// 执行根命令
	if err := cmd.Execute(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

2.4 定义根命令

创建cmd/root.go文件,定义应用的根命令:

package cmd

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

var cfgFile string
var verbose bool

// rootCmd 代表没有子命令时的基础命令
var rootCmd = &cobra.Command{
	Use:   "gofiles",
	Short: "一个强大的文件处理工具",
	Long: `gofiles 是一个功能丰富的文件处理命令行工具,
它可以帮助您查找文件、搜索内容、分析目录和批处理文件。

示例用法:
  gofiles find -n "*.go" -d ./src     查找所有Go源文件
  gofiles search -p "TODO" -d ./src   搜索所有包含"TODO"的文件
  gofiles stats -d ./project          分析项目目录结构`,
}

// Execute 将所有子命令添加到根命令并设置标志。
func Execute() error {
	return rootCmd.Execute()
}

func init() {
	cobra.OnInitialize(initConfig)

	// 全局标志
	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "配置文件路径 (默认为 $HOME/.gofiles.yaml)")
	rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "启用详细输出")
	
	// 本地标志
	rootCmd.Flags().BoolP("version", "V", false, "显示版本信息")
	
	// 将标志绑定到viper
	viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose"))
}

// initConfig 读取配置文件和环境变量
func initConfig() {
	if cfgFile != "" {
		// 使用指定的配置文件
		viper.SetConfigFile(cfgFile)
	} else {
		// 查找主目录
		home, err := os.UserHomeDir()
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}

		// 在主目录中查找名为 ".gofiles" 的配置
		viper.AddConfigPath(home)
		viper.SetConfigType("yaml")
		viper.SetConfigName(".gofiles")
	}

	// 读取环境变量
	viper.AutomaticEnv()

	// 读取配置文件
	if err := viper.ReadInConfig(); err == nil {
		if verbose {
			fmt.Println("使用配置文件:", viper.ConfigFileUsed())
		}
	}
}

这个基础框架为我们的命令行工具提供了:

  • 基本命令结构
  • 配置文件支持
  • 详细模式标志
  • 版本信息支持
  • 帮助文档

三、核心功能实现

接下来,我们将实现工具的核心功能模块。我们先从文件查找功能开始。

3.1 文件查找功能

3.1.1 定义查找命令

创建cmd/find.go文件:

package cmd

import (
	"fmt"
	"time"

	"github.com/spf13/cobra"
	"github.com/yourusername/gofiles/internal/finder"
)

var (
	findDir       string
	findName      string
	findExt       string
	findMinSize   string
	findMaxSize   string
	findOlderThan string
	findNewerThan string
	findMaxDepth  int
)

// findCmd 表示 find 命令
var findCmd = &cobra.Command{
	Use:   "find",
	Short: "查找匹配指定条件的文件",
	Long: `查找命令用于在指定目录中搜索符合条件的文件。
您可以根据文件名、扩展名、大小和修改时间等条件进行过滤。

示例:
  gofiles find -d ./src -n "*.go"               查找所有Go源文件
  gofiles find -d ./docs -e ".pdf" --min-size 1M  查找大于1MB的PDF文件
  gofiles find -d ./logs --older-than 30d         查找30天前的日志文件`,
	RunE: func(cmd *cobra.Command, args []string) error {
		// 创建查找选项
		opts := finder.Options{
			Directory:  findDir,
			Name:       findName,
			Extension:  findExt,
			MinSize:    findMinSize,
			MaxSize:    findMaxSize,
			OlderThan:  findOlderThan,
			NewerThan:  findNewerThan,
			MaxDepth:   findMaxDepth,
			Verbose:    verbose,
		}

		// 创建查找器
		f := finder.New(opts)

		// 执行查找
		results, err := f.Find()
		if err != nil {
			return err
		}

		// 显示结果
		fmt.Printf("找到 %d 个匹配文件:\n", len(results))
		for _, file := range results {
			fmt.Printf("%s (%s, %s)\n", 
				file.Path, 
				formatSize(file.Size), 
				file.ModTime.Format(time.RFC3339))
		}

		return nil
	},
}

func init() {
	rootCmd.AddCommand(findCmd)

	// 添加查找相关标志
	findCmd.Flags().StringVarP(&findDir, "directory", "d", ".", "要搜索的目录")
	findCmd.Flags().StringVarP(&findName, "name", "n", "", "按文件名匹配 (支持通配符)")
	findCmd.Flags().StringVarP(&findExt, "ext", "e", "", "按文件扩展名匹配")
	findCmd.Flags().StringVar(&findMinSize, "min-size", "", "最小文件大小 (如: 10K, 5M, 1G)")
	findCmd.Flags().StringVar(&findMaxSize, "max-size", "", "最大文件大小 (如: 10K, 5M, 1G)")
	findCmd.Flags().StringVar(&findOlderThan, "older-than", "", "早于指定时间 (如: 30m, 24h, 7d)")
	findCmd.Flags().StringVar(&findNewerThan, "newer-than", "", "晚于指定时间 (如: 30m, 24h, 7d)")
	findCmd.Flags().IntVar(&findMaxDepth, "max-depth", 0, "最大递归深度 (0表示无限)")
}

// 格式化文件大小
func formatSize(size int64) string {
	const unit = 1024
	if size < unit {
		return fmt.Sprintf("%d B", size)
	}
	div, exp := int64(unit), 0
	for n := size / unit; n >= unit; n /= unit {
		div *= unit
		exp++
	}
	return fmt.Sprintf("%.1f %cB", float64(size)/float64(div), "KMGTPE"[exp])
}
3.1.2 实现文件查找逻辑

创建internal/finder/finder.go文件:

package finder

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/schollz/progressbar/v3"
)

// FileInfo 包含有关找到的文件的信息
type FileInfo struct {
	Path    string    // 文件路径
	Size    int64     // 文件大小(字节)
	ModTime time.Time // 修改时间
}

// Options 包含文件查找选项
type Options struct {
	Directory  string // 搜索目录
	Name       string // 文件名模式(支持通配符)
	Extension  string // 文件扩展名
	MinSize    string // 最小文件大小
	MaxSize    string // 最大文件大小
	OlderThan  string // 早于指定时间
	NewerThan  string // 晚于指定时间
	MaxDepth   int    // 最大递归深度
	Verbose    bool   // 详细输出
}

// Finder 实现文件查找功能
type Finder struct {
	opts        Options
	minSize     int64
	maxSize     int64
	olderThan   time.Time
	newerThan   time.Time
	namePattern *regexp.Regexp
	progressBar *progressbar.ProgressBar
}

// New 创建一个新的文件查找器
func New(opts Options) *Finder {
	return &Finder{
		opts: opts,
	}
}

// Find 执行文件查找
func (f *Finder) Find() ([]FileInfo, error) {
	// 验证选项并设置内部字段
	if err := f.parseOptions(); err != nil {
		return nil, err
	}

	// 检查目录是否存在
	info, err := os.Stat(f.opts.Directory)
	if err != nil {
		return nil, fmt.Errorf("无法访问目录 %s: %w", f.opts.Directory, err)
	}
	if !info.IsDir() {
		return nil, fmt.Errorf("%s 不是一个目录", f.opts.Directory)
	}

	// 首先计算文件总数以显示进度条
	var totalFiles int
	if f.opts.Verbose {
		fmt.Println("正在计算文件总数...")
		filepath.Walk(f.opts.Directory, func(path string, info os.FileInfo, err error) error {
			if err != nil {
				return nil
			}
			if !info.IsDir() {
				totalFiles++
			}
			return nil
		})
		f.progressBar = progressbar.Default(int64(totalFiles))
	}

	// 存储结果
	var results []FileInfo
	var mutex sync.Mutex
	var wg sync.WaitGroup

	// 队列用于存储待处理的目录
	dirs := []string{f.opts.Directory}
	currentDepth := 0

	// 处理目录直到所有目录都被处理
	for len(dirs) > 0 && (f.opts.MaxDepth == 0 || currentDepth <= f.opts.MaxDepth) {
		currentDirs := dirs
		dirs = []string{}

		// 处理当前级别的所有目录
		for _, dir := range currentDirs {
			wg.Add(1)
			go func(d string) {
				defer wg.Done()

				// 读取目录内容
				entries, err := os.ReadDir(d)
				if err != nil {
					if f.opts.Verbose {
						fmt.Fprintf(os.Stderr, "无法读取目录 %s: %v\n", d, err)
					}
					return
				}

				// 处理所有条目
				for _, entry := range entries {
					path := filepath.Join(d, entry.Name())

					// 如果是目录且未达到最大深度,则添加到队列
					if entry.IsDir() {
						if f.opts.MaxDepth == 0 || currentDepth < f.opts.MaxDepth {
							mutex.Lock()
							dirs = append(dirs, path)
							mutex.Unlock()
						}
						continue
					}

					// 获取文件信息
					info, err := entry.Info()
					if err != nil {
						continue
					}

					// 更新进度条
					if f.progressBar != nil {
						f.progressBar.Add(1)
					}

					// 检查文件是否匹配条件
					if f.matchesFilter(path, info) {
						fileInfo := FileInfo{
							Path:    path,
							Size:    info.Size(),
							ModTime: info.ModTime(),
						}
						mutex.Lock()
						results = append(results, fileInfo)
						mutex.Unlock()
					}
				}
			}(dir)
		}

		// 等待所有goroutine完成当前级别的处理
		wg.Wait()
		currentDepth++
	}

	return results, nil
}

// parseOptions 解析和验证选项
func (f *Finder) parseOptions() error {
	// 解析文件大小
	var err error
	if f.opts.MinSize != "" {
		f.minSize, err = parseSize(f.opts.MinSize)
		if err != nil {
			return fmt.Errorf("无效的最小大小: %w", err)
		}
	}

	if f.opts.MaxSize != "" {
		f.maxSize, err = parseSize(f.opts.MaxSize)
		if err != nil {
			return fmt.Errorf("无效的最大大小: %w", err)
		}
	}

	// 解析时间
	now := time.Now()
	if f.opts.OlderThan != "" {
		duration, err := parseDuration(f.opts.OlderThan)
		if err != nil {
			return fmt.Errorf("无效的时间跨度: %w", err)
		}
		f.olderThan = now.Add(-duration)
	}

	if f.opts.NewerThan != "" {
		duration, err := parseDuration(f.opts.NewerThan)
		if err != nil {
			return fmt.Errorf("无效的时间跨度: %w", err)
		}
		f.newerThan = now.Add(-duration)
	}

	// 解析文件名模式
	if f.opts.Name != "" {
		pattern := wildcardToRegexp(f.opts.Name)
		f.namePattern, err = regexp.Compile(pattern)
		if err != nil {
			return fmt.Errorf("无效的文件名模式: %w", err)
		}
	}

	return nil
}

// matchesFilter 检查文件是否匹配过滤条件
func (f *Finder) matchesFilter(path string, info os.FileInfo) bool {
	// 检查文件名
	if f.namePattern != nil && !f.namePattern.MatchString(filepath.Base(path)) {
		return false
	}

	// 检查扩展名
	if f.opts.Extension != "" {
		ext := filepath.Ext(path)
		if !strings.EqualFold(ext, f.opts.Extension) {
			if !strings.HasPrefix(f.opts.Extension, ".") {
				// 尝试添加点号前缀
				if !strings.EqualFold(ext, "."+f.opts.Extension) {
					return false
				}
			} else {
				return false
			}
		}
	}

	// 检查文件大小
	size := info.Size()
	if f.minSize > 0 && size < f.minSize {
		return false
	}
	if f.maxSize > 0 && size > f.maxSize {
		return false
	}

	// 检查修改时间
	modTime := info.ModTime()
	if !f.olderThan.IsZero() && modTime.After(f.olderThan) {
		return false
	}
	if !f.newerThan.IsZero() && modTime.Before(f.newerThan) {
		return false
	}

	return true
}

// parseSize 将人类可读的大小字符串解析为字节数
func parseSize(sizeStr string) (int64, error) {
	sizeStr = strings.TrimSpace(sizeStr)
	if sizeStr == "" {
		return 0, nil
	}

	// 捕获数字部分和单位
	re := regexp.MustCompile(`^(\d+(?:\.\d+)?)([KMGTPE]?B?)?$`)
	matches := re.FindStringSubmatch(strings.ToUpper(sizeStr))
	if matches == nil {
		return 0, errors.New("格式无效")
	}

	value, err := strconv.ParseFloat(matches[1], 64)
	if err != nil {
		return 0, err
	}

	unit := matches[2]
	if unit == "" || unit == "B" {
		return int64(value), nil
	}

	// 计算乘数
	multiplier := 1.0
	switch unit[0] {
	case 'K':
		multiplier = 1024
	case 'M':
		multiplier = 1024 * 1024
	case 'G':
		multiplier = 1024 * 1024 * 1024
	case 'T':
		multiplier = 1024 * 1024 * 1024 * 1024
	case 'P':
		multiplier = 1024 * 1024 * 1024 * 1024 * 1024
	case 'E':
		multiplier = 1024 * 1024 * 1024 * 1024 * 1024 * 1024
	}

	return int64(value * multiplier), nil
}

// parseDuration 解析类似 "1d", "30m", "24h" 的持续时间
func parseDuration(durationStr string) (time.Duration, error) {
	durationStr = strings.TrimSpace(durationStr)
	if durationStr == "" {
		return 0, nil
	}

	// 检查是否有"d"代表天
	if strings.HasSuffix(durationStr, "d") {
		days, err := strconv.Atoi(durationStr[:len(durationStr)-1])
		if err != nil {
			return 0, err
		}
		return time.Hour * 24 * time.Duration(days), nil
	}

	// 使用内置的duration解析
	return time.ParseDuration(durationStr)
}

// wildcardToRegexp 将文件系统通配符转换为正则表达式
func wildcardToRegexp(pattern string) string {
	pattern = regexp.QuoteMeta(pattern)
	pattern = strings.ReplaceAll(pattern, "\\*", ".*")
	pattern = strings.ReplaceAll(pattern, "\\?", ".")
	return "^" + pattern + "$"
}

这段代码实现了文件查找功能,包括:

  • 支持按名称、扩展名过滤
  • 支持按文件大小范围过滤
  • 支持按修改时间过滤
  • 支持限制递归深度
  • 并发处理以提高性能
  • 显示进度条

3.2 文件内容搜索功能

接下来,我们将实现在文件内容中搜索指定模式的功能。

3.2.1 定义搜索命令

创建cmd/search.go文件:

package cmd

import (
	"fmt"

	"github.com/spf13/cobra"
	"github.com/yourusername/gofiles/internal/searcher"
)

var (
	searchDir       string
	searchPattern   string
	searchIgnoreCase bool
	searchFileTypes []string
	searchMaxResults int
	searchContext   int
)

// searchCmd 表示 search 命令
var searchCmd = &cobra.Command{
	Use:   "search",
	Short: "在文件内容中搜索指定模式",
	Long: `search 命令在指定目录中搜索包含特定文本或匹配正则表达式的文件。

示例:
  gofiles search -p "func main" -d ./src            搜索包含"func main"的文件
  gofiles search -p "TODO|FIXME" -d ./src --regex   使用正则表达式搜索待办事项
  gofiles search -p "error" -d ./logs -t .log       在日志文件中搜索错误`,
	RunE: func(cmd *cobra.Command, args []string) error {
		// 创建搜索选项
		opts := searcher.Options{
			Directory:   searchDir,
			Pattern:     searchPattern,
			IgnoreCase:  searchIgnoreCase,
			FileTypes:   searchFileTypes,
			MaxResults:  searchMaxResults,
			Context:     searchContext,
			Verbose:     verbose,
		}

		// 创建搜索器
		s := searcher.New(opts)

		// 执行搜索
		results, err := s.Search()
		if err != nil {
			return err
		}

		// 显示结果
		fmt.Printf("在 %d 个文件中找到 %d 个匹配项\n", 
			len(results.Files), results.TotalMatches)
		
		for _, file := range results.Files {
			fmt.Printf("\n%s (%d 个匹配):\n", file.Path, len(file.Matches))
			for _, match := range file.Matches {
				// 打印匹配行前的上下文
				for _, ctx := range match.BeforeContext {
					fmt.Printf("  %d: %s\n", ctx.LineNum, ctx.Content)
				}
				
				// 打印匹配行(突出显示)
				fmt.Printf("→ %d: %s\n", match.LineNum, match.Line)
				
				// 打印匹配行后的上下文
				for _, ctx := range match.AfterContext {
					fmt.Printf("  %d: %s\n", ctx.LineNum, ctx.Content)
				}
				fmt.Println()
			}
		}

		return nil
	},
}

func init() {
	rootCmd.AddCommand(searchCmd)

	// 添加搜索相关标志
	searchCmd.Flags().StringVarP(&searchDir, "directory", "d", ".", "要搜索的目录")
	searchCmd.Flags().StringVarP(&searchPattern, "pattern", "p", "", "要搜索的模式")
	searchCmd.Flags().BoolVarP(&searchIgnoreCase, "ignore-case", "i", false, "忽略大小写")
	searchCmd.Flags().StringSliceVarP(&searchFileTypes, "type", "t", nil, "要搜索的文件类型 (如: .go, .txt)")
	searchCmd.Flags().IntVar(&searchMaxResults, "max-results", 100, "最大结果数")
	searchCmd.Flags().IntVarP(&searchContext, "context", "c", 2, "显示匹配行周围的行数")

	// 设置必需的标志
	searchCmd.MarkFlagRequired("pattern")
}
3.2.2 实现内容搜索逻辑

创建internal/searcher/searcher.go文件:

package searcher

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"sync"

	"github.com/schollz/progressbar/v3"
)

// Options 包含搜索选项
type Options struct {
	Directory   string   // 搜索目录
	Pattern     string   // 搜索模式
	IgnoreCase  bool     // 是否忽略大小写
	FileTypes   []string // 文件类型过滤
	MaxResults  int      // 最大结果数
	Context     int      // 显示匹配行周围的行数
	Verbose     bool     // 详细输出
}

// ContextLine 表示匹配行周围的上下文行
type ContextLine struct {
	LineNum int    // 行号
	Content string // 行内容
}

// Match 表示在文件中找到的匹配
type Match struct {
	LineNum       int           // 匹配行号
	Line          string        // 匹配行内容
	BeforeContext []ContextLine // 匹配行之前的行
	AfterContext  []ContextLine // 匹配行之后的行
}

// FileResult 表示在单个文件中的搜索结果
type FileResult struct {
	Path    string  // 文件路径
	Matches []Match // 匹配列表
}

// SearchResults 包含搜索的总体结果
type SearchResults struct {
	Files        []FileResult // 包含匹配的文件
	TotalMatches int          // 匹配总数
}

// Searcher 实现文件内容搜索功能
type Searcher struct {
	opts        Options
	pattern     *regexp.Regexp
	progressBar *progressbar.ProgressBar
}

// New 创建一个新的内容搜索器
func New(opts Options) *Searcher {
	return &Searcher{
		opts: opts,
	}
}

// Search 执行内容搜索
func (s *Searcher) Search() (*SearchResults, error) {
	// 编译搜索模式
	var err error
	patternStr := s.opts.Pattern
	if s.opts.IgnoreCase {
		patternStr = "(?i)" + patternStr
	}
	s.pattern, err = regexp.Compile(patternStr)
	if err != nil {
		return nil, fmt.Errorf("无效的搜索模式: %w", err)
	}

	// 计算要搜索的文件
	filesToSearch, err := s.findFilesToSearch()
	if err != nil {
		return nil, err
	}

	if s.opts.Verbose {
		fmt.Printf("找到 %d 个文件进行搜索\n", len(filesToSearch))
		s.progressBar = progressbar.Default(int64(len(filesToSearch)))
	}

	// 创建结果对象
	results := &SearchResults{
		Files: []FileResult{},
	}

	// 限制并发数以避免打开太多文件
	semaphore := make(chan struct{}, 8)
	var wg sync.WaitGroup
	var resultsMutex sync.Mutex

	// 搜索文件
	for _, file := range filesToSearch {
		if results.TotalMatches >= s.opts.MaxResults {
			break
		}

		wg.Add(1)
		go func(filePath string) {
			defer wg.Done()
			
			// 获取信号量
			semaphore <- struct{}{}
			defer func() { <-semaphore }()

			// 搜索单个文件
			fileResult, err := s.searchFile(filePath)
			if err != nil {
				if s.opts.Verbose {
					fmt.Fprintf(os.Stderr, "搜索文件 %s 时出错: %v\n", filePath, err)
				}
				return
			}

			// 更新进度条
			if s.progressBar != nil {
				s.progressBar.Add(1)
			}

			// 如果找到匹配项,添加到结果
			if fileResult != nil && len(fileResult.Matches) > 0 {
				resultsMutex.Lock()
				results.Files = append(results.Files, *fileResult)
				results.TotalMatches += len(fileResult.Matches)
				resultsMutex.Unlock()
			}
		}(file)
	}

	// 等待所有搜索完成
	wg.Wait()

	return results, nil
}

// findFilesToSearch 找出需要搜索的所有文件
func (s *Searcher) findFilesToSearch() ([]string, error) {
	var filesToSearch []string

	err := filepath.Walk(s.opts.Directory, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return nil // 跳过无法访问的项
		}

		// 跳过目录
		if info.IsDir() {
			return nil
		}

		// 检查文件类型
		if len(s.opts.FileTypes) > 0 {
			matched := false
			ext := filepath.Ext(path)
			for _, fileType := range s.opts.FileTypes {
				// 添加点号前缀(如果需要)
				if !strings.HasPrefix(fileType, ".") {
					fileType = "." + fileType
				}
				if strings.EqualFold(ext, fileType) {
					matched = true
					break
				}
			}
			if !matched {
				return nil
			}
		}

		filesToSearch = append(filesToSearch, path)
		return nil
	})

	if err != nil {
		return nil, fmt.Errorf("在遍历目录时出错: %w", err)
	}

	return filesToSearch, nil
}

// searchFile 在单个文件中搜索模式
func (s *Searcher) searchFile(filePath string) (*FileResult, error) {
	// 打开文件
	file, err := os.Open(filePath)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	// 创建文件结果
	fileResult := &FileResult{
		Path: filePath,
	}

	// 创建扫描器
	scanner := bufio.NewScanner(file)
	
	// 用于存储上下文的缓冲区
	contextBuffer := make([]ContextLine, 0, s.opts.Context*2)
	
	lineNum := 0
	inMatch := false
	afterContext := 0
	
	// 逐行读取文件
	for scanner.Scan() {
		lineNum++
		line := scanner.Text()
		
		// 检查是否匹配
		isMatch := s.pattern.MatchString(line)
		
		// 处理匹配和上下文
		if isMatch {
			// 创建匹配对象
			match := Match{
				LineNum: lineNum,
				Line:    line,
			}
			
			// 添加之前的上下文
			if len(contextBuffer) > 0 {
				startIdx := 0
				if len(contextBuffer) > s.opts.Context {
					startIdx = len(contextBuffer) - s.opts.Context
				}
				for i := startIdx; i < len(contextBuffer); i++ {
					match.BeforeContext = append(match.BeforeContext, contextBuffer[i])
				}
			}
			
			// 添加匹配到结果
			fileResult.Matches = append(fileResult.Matches, match)
			inMatch = true
			afterContext = s.opts.Context
			
			// 清空上下文缓冲区
			contextBuffer = make([]ContextLine, 0, s.opts.Context*2)
		} else if inMatch && afterContext > 0 {
			// 添加匹配后的上下文
			lastMatchIdx := len(fileResult.Matches) - 1
			fileResult.Matches[lastMatchIdx].AfterContext = append(
				fileResult.Matches[lastMatchIdx].AfterContext,
				ContextLine{
					LineNum: lineNum,
					Content: line,
				},
			)
			afterContext--
			if afterContext == 0 {
				inMatch = false
			}
		} else {
			// 添加到上下文缓冲区
			contextBuffer = append(contextBuffer, ContextLine{
				LineNum: lineNum,
				Content: line,
			})
			if len(contextBuffer) > s.opts.Context*2 {
				contextBuffer = contextBuffer[1:]
			}
		}
		
		// 检查是否达到最大结果数
		if len(fileResult.Matches) >= s.opts.MaxResults {
			break
		}
	}
	
	// 检查扫描错误
	if err := scanner.Err(); err != nil {
		return nil, err
	}
	
	return fileResult, nil
}

这段代码实现了文件内容搜索功能,包括:

  • 支持正则表达式搜索
  • 支持忽略大小写搜索
  • 按文件类型过滤
  • 显示匹配行周围的上下文
  • 限制最大结果数
  • 并发搜索多个文件

3.3 目录分析功能

接下来,我们将实现目录分析功能,帮助用户统计目录结构和文件分布情况。

3.3.1 定义统计命令

创建cmd/stats.go文件:

package cmd

import (
	"fmt"
	"sort"
	"strings"

	"github.com/spf13/cobra"
	"github.com/yourusername/gofiles/internal/analyzer"
)

var (
	statsDir      string
	statsMaxDepth int
)

// statsCmd 表示 stats 命令
var statsCmd = &cobra.Command{
	Use:   "stats",
	Short: "分析目录结构和文件统计信息",
	Long: `stats 命令分析目录结构并提供关于文件大小、类型分布等统计信息。

示例:
  gofiles stats -d ./project                  分析项目目录
  gofiles stats -d ./src --max-depth 2        仅分析前两级目录`,
	RunE: func(cmd *cobra.Command, args []string) error {
		// 创建分析选项
		opts := analyzer.Options{
			Directory: statsDir,
			MaxDepth:  statsMaxDepth,
			Verbose:   verbose,
		}

		// 创建分析器
		a := analyzer.New(opts)

		// 执行分析
		result, err := a.Analyze()
		if err != nil {
			return err
		}

		// 显示结果
		fmt.Printf("目录: %s\n\n", result.Directory)
		
		// 目录统计
		fmt.Printf("总计:\n")
		fmt.Printf("  文件数量: %d\n", result.TotalFiles)
		fmt.Printf("  目录数量: %d\n", result.TotalDirs)
		fmt.Printf("  总大小: %s\n\n", formatSize(result.TotalSize))
		
		// 扩展名分布
		fmt.Println("按文件类型:")
		
		// 将扩展名排序
		var extensions []string
		for ext := range result.ExtensionStats {
			extensions = append(extensions, ext)
		}
		sort.Slice(extensions, func(i, j int) bool {
			return result.ExtensionStats[extensions[i]].Count > result.ExtensionStats[extensions[j]].Count
		})
		
		// 显示排名前10的文件类型
		count := 0
		for _, ext := range extensions {
			stats := result.ExtensionStats[ext]
			extName := ext
			if extName == "" {
				extName = "(无扩展名)"
			}
			fmt.Printf("  %s: %d 个文件, %s (%.1f%%)\n", 
				extName, 
				stats.Count, 
				formatSize(stats.Size),
				float64(stats.Size) / float64(result.TotalSize) * 100)
			
			count++
			if count >= 10 && len(extensions) > 10 {
				fmt.Printf("  ... 另外 %d 种文件类型\n", len(extensions) - 10)
				break
			}
		}
		fmt.Println()
		
		// 大小分布
		fmt.Println("按文件大小:")
		fmt.Printf("  < 10 KB: %d 个文件\n", result.SizeDistribution.Small)
		fmt.Printf("  10 KB - 100 KB: %d 个文件\n", result.SizeDistribution.Medium)
		fmt.Printf("  100 KB - 1 MB: %d 个文件\n", result.SizeDistribution.Large)
		fmt.Printf("  1 MB - 10 MB: %d 个文件\n", result.SizeDistribution.VeryLarge)
		fmt.Printf("  > 10 MB: %d 个文件\n\n", result.SizeDistribution.Huge)
		
		// 最大的文件
		fmt.Println("最大的文件:")
		for i, file := range result.LargestFiles {
			if i >= 5 {
				break
			}
			fmt.Printf("  %s (%s)\n", file.Path, formatSize(file.Size))
		}
		
		return nil
	},
}

func init() {
	rootCmd.AddCommand(statsCmd)

	// 添加统计相关标志
	statsCmd.Flags().StringVarP(&statsDir, "directory", "d", ".", "要分析的目录")
	statsCmd.Flags().IntVar(&statsMaxDepth, "max-depth", 0, "最大递归深度 (0表示无限)")
}
3.3.2 实现目录分析逻辑

创建internal/analyzer/analyzer.go文件:

package analyzer

import (
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"

	"github.com/schollz/progressbar/v3"
)

// Options 包含目录分析选项
type Options struct {
	Directory string // 分析目录
	MaxDepth  int    // 最大递归深度
	Verbose   bool   // 详细输出
}

// FileInfo 包含文件信息
type FileInfo struct {
	Path string // 文件路径
	Size int64  // 文件大小(字节)
}

// ExtensionStat 包含特定扩展名的统计信息
type ExtensionStat struct {
	Count int   // 文件数量
	Size  int64 // 总大小(字节)
}

// SizeDistribution 包含文件大小分布
type SizeDistribution struct {
	Small     int // < 10 KB
	Medium    int // 10 KB - 100 KB
	Large     int // 100 KB - 1 MB
	VeryLarge int // 1 MB - 10 MB
	Huge      int // > 10 MB
}

// AnalyzeResult 包含分析结果
type AnalyzeResult struct {
	Directory       string                   // 分析的目录
	TotalFiles      int                      // 文件总数
	TotalDirs       int                      // 目录总数
	TotalSize       int64                    // 总大小(字节)
	ExtensionStats  map[string]ExtensionStat // 按扩展名统计
	SizeDistribution SizeDistribution        // 文件大小分布
	LargestFiles    []FileInfo               // 最大的文件列表
}

// Analyzer 实现目录分析功能
type Analyzer struct {
	opts        Options
	progressBar *progressbar.ProgressBar
}

// New 创建一个新的目录分析器
func New(opts Options) *Analyzer {
	return &Analyzer{
		opts: opts,
	}
}

// Analyze 执行目录分析
func (a *Analyzer) Analyze() (*AnalyzeResult, error) {
	// 检查目录是否存在
	info, err := os.Stat(a.opts.Directory)
	if err != nil {
		return nil, fmt.Errorf("无法访问目录 %s: %w", a.opts.Directory, err)
	}
	if !info.IsDir() {
		return nil, fmt.Errorf("%s 不是一个目录", a.opts.Directory)
	}

	// 创建分析结果
	result := &AnalyzeResult{
		Directory:      a.opts.Directory,
		ExtensionStats: make(map[string]ExtensionStat),
		LargestFiles:   make([]FileInfo, 0),
	}

	// 统计文件总数(用于进度条)
	if a.opts.Verbose {
		fmt.Println("正在计算文件总数...")
		totalFiles := 0
		filepath.Walk(a.opts.Directory, func(path string, info os.FileInfo, err error) error {
			if err != nil {
				return nil
			}
			if !info.IsDir() {
				totalFiles++
			}
			return nil
		})
		
		a.progressBar = progressbar.Default(int64(totalFiles))
		fmt.Printf("找到 %d 个文件进行分析\n", totalFiles)
	}

	// 使用计数器和互斥锁跟踪最大文件
	var mutex sync.Mutex
	var largestFilesMutex sync.Mutex
	largestFiles := make([]FileInfo, 0, 100) // 临时存储最大文件

	// 使用 WalkDir 遍历目录
	err = filepath.Walk(a.opts.Directory, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return nil // 跳过无法访问的项
		}

		// 检查是否超过最大深度
		if a.opts.MaxDepth > 0 {
			relPath, err := filepath.Rel(a.opts.Directory, path)
			if err != nil {
				return nil
			}
			
			// 根据路径分隔符数量计算深度
			depth := 0
			if relPath != "." {
				depth = strings.Count(relPath, string(os.PathSeparator)) + 1
			}
			
			if depth > a.opts.MaxDepth {
				if info.IsDir() {
					return filepath.SkipDir
				}
				return nil
			}
		}

		// 更新统计信息
		if info.IsDir() {
			if path != a.opts.Directory { // 不计算根目录
				mutex.Lock()
				result.TotalDirs++
				mutex.Unlock()
			}
		} else {
			size := info.Size()
			
			mutex.Lock()
			result.TotalFiles++
			result.TotalSize += size
			
			// 按扩展名统计
			ext := strings.ToLower(filepath.Ext(path))
			stats := result.ExtensionStats[ext]
			stats.Count++
			stats.Size += size
			result.ExtensionStats[ext] = stats
			
			// 更新大小分布
			switch {
			case size < 10*1024: // 10 KB
				result.SizeDistribution.Small++
			case size < 100*1024: // 100 KB
				result.SizeDistribution.Medium++
			case size < 1024*1024: // 1 MB
				result.SizeDistribution.Large++
			case size < 10*1024*1024: // 10 MB
				result.SizeDistribution.VeryLarge++
			default:
				result.SizeDistribution.Huge++
			}
			mutex.Unlock()
			
			// 收集最大的文件
			largestFilesMutex.Lock()
			largestFiles = append(largestFiles, FileInfo{
				Path: path,
				Size: size,
			})
			largestFilesMutex.Unlock()
			
			// 更新进度条
			if a.progressBar != nil {
				a.progressBar.Add(1)
			}
		}

		return nil
	})

	if err != nil {
		return nil, fmt.Errorf("分析目录时出错: %w", err)
	}

	// 找出最大的文件
	sort.Slice(largestFiles, func(i, j int) bool {
		return largestFiles[i].Size > largestFiles[j].Size
	})
	
	// 取前20个最大文件
	result.LargestFiles = largestFiles
	if len(largestFiles) > 20 {
		result.LargestFiles = largestFiles[:20]
	}

	return result, nil
}

3.4 批量文件操作功能

最后,我们将实现批量文件操作功能,支持对符合条件的文件执行重命名、移动等操作。

3.4.1 定义批处理命令

创建cmd/batch.go文件:

package cmd

import (
	"fmt"
	"io"
	"os"
	"path/filepath"

	"github.com/spf13/cobra"
	"github.com/yourusername/gofiles/internal/processor"
)

var (
	batchDir      string
	batchPattern  string
	batchOperation string
	batchDryRun   bool
	batchRename   string
	batchMove     string
)

// batchCmd 表示 batch 命令
var batchCmd = &cobra.Command{
	Use:   "batch",
	Short: "对符合条件的文件执行批量操作",
	Long: `batch 命令对匹配指定模式的文件执行批量操作,如重命名或移动。

示例:
  gofiles batch -d ./logs -p "*.log" --rename "{name}_old{ext}"  重命名日志文件
  gofiles batch -d ./src -p "*.tmp" --move ./backup              移动临时文件到备份目录
  gofiles batch -d ./docs -p "*.txt" --dry-run --rename "{name}.md"  预览重命名操作`,
	RunE: func(cmd *cobra.Command, args []string) error {
		// 检查是否指定了操作
		if batchRename == "" && batchMove == "" {
			return fmt.Errorf("必须指定操作类型 (--rename 或 --move)")
		}

		// 确定操作类型
		var operationType processor.OperationType
		var operationValue string

		if batchRename != "" {
			operationType = processor.OperationRename
			operationValue = batchRename
		} else if batchMove != "" {
			operationType = processor.OperationMove
			operationValue = batchMove
			
			// 检查目标目录是否存在
			if !batchDryRun {
				targetDir := filepath.Clean(operationValue)
				if err := ensureDirectoryExists(targetDir); err != nil {
					return err
				}
			}
		}

		// 创建处理选项
		opts := processor.Options{
			Directory:      batchDir,
			Pattern:        batchPattern,
			OperationType:  operationType,
			OperationValue: operationValue,
			DryRun:         batchDryRun,
			Verbose:        verbose,
		}

		// 创建处理器
		p := processor.New(opts)

		// 执行批处理
		results, err := p.Process()
		if err != nil {
			return err
		}

		// 显示结果
		if batchDryRun {
			fmt.Println("预览模式 (未执行实际操作):")
		}

		fmt.Printf("处理了 %d 个文件\n", len(results))
		for _, result := range results {
			if result.Error != nil {
				fmt.Printf("❌ %s: %v\n", result.SourcePath, result.Error)
			} else {
				fmt.Printf("✓ %s -> %s\n", result.SourcePath, result.TargetPath)
			}
		}

		return nil
	},
}

// 确保目录存在
func ensureDirectoryExists(dir string) error {
	info, err := os.Stat(dir)
	if err != nil {
		if os.IsNotExist(err) {
			// 创建目录
			if err := os.MkdirAll(dir, 0755); err != nil {
				return fmt.Errorf("无法创建目录 %s: %w", dir, err)
			}
			return nil
		}
		return fmt.Errorf("无法访问目录 %s: %w", dir, err)
	}
	
	if !info.IsDir() {
		return fmt.Errorf("%s 不是一个目录", dir)
	}
	
	return nil
}

func init() {
	rootCmd.AddCommand(batchCmd)

	// 添加批处理相关标志
	batchCmd.Flags().StringVarP(&batchDir, "directory", "d", ".", "要处理的目录")
	batchCmd.Flags().StringVarP(&batchPattern, "pattern", "p", "", "文件名模式 (支持通配符)")
	batchCmd.Flags().BoolVar(&batchDryRun, "dry-run", false, "预览模式,不执行实际操作")
	batchCmd.Flags().StringVar(&batchRename, "rename", "", "重命名模式 (如: {name}_new{ext})")
	batchCmd.Flags().StringVar(&batchMove, "move", "", "移动目标目录")

	// 设置必需的标志
	batchCmd.MarkFlagRequired("pattern")
}
3.4.2 实现批处理逻辑

创建internal/processor/processor.go文件:

package processor

import (
	"fmt"
	"io"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/schollz/progressbar/v3"
)

// OperationType 表示批处理操作类型
type OperationType int

const (
	OperationRename OperationType = iota // 重命名操作
	OperationMove                        // 移动操作
)

// Options 包含批处理选项
type Options struct {
	Directory      string        // 处理目录
	Pattern        string        // 文件名模式(支持通配符)
	OperationType  OperationType // 操作类型
	OperationValue string        // 操作值(重命名模式或移动目标)
	DryRun         bool          // 预览模式
	Verbose        bool          // 详细输出
}

// ProcessResult 表示单个文件的处理结果
type ProcessResult struct {
	SourcePath string // 源文件路径
	TargetPath string // 目标文件路径
	Error      error  // 处理错误(如果有)
}

// Processor 实现批量文件处理功能
type Processor struct {
	opts        Options
	pattern     *regexp.Regexp
	progressBar *progressbar.ProgressBar
}

// New 创建一个新的批处理器
func New(opts Options) *Processor {
	return &Processor{
		opts: opts,
	}
}

// Process 执行批处理操作
func (p *Processor) Process() ([]ProcessResult, error) {
	// 转换通配符为正则表达式
	pattern := wildcardToRegexp(p.opts.Pattern)
	var err error
	p.pattern, err = regexp.Compile(pattern)
	if err != nil {
		return nil, fmt.Errorf("无效的文件模式: %w", err)
	}

	// 找到匹配的文件
	files, err := p.findMatchingFiles()
	if err != nil {
		return nil, err
	}

	if p.opts.Verbose {
		fmt.Printf("找到 %d 个匹配文件\n", len(files))
		p.progressBar = progressbar.Default(int64(len(files)))
	}

	// 处理文件
	results := make([]ProcessResult, 0, len(files))
	for _, file := range files {
		var result ProcessResult
		result.SourcePath = file

		// 根据操作类型处理文件
		switch p.opts.OperationType {
		case OperationRename:
			result.TargetPath, err = p.renameFile(file)
		case OperationMove:
			result.TargetPath, err = p.moveFile(file)
		}

		if err != nil {
			result.Error = err
		}

		results = append(results, result)

		// 更新进度条
		if p.progressBar != nil {
			p.progressBar.Add(1)
		}
	}

	return results, nil
}

// findMatchingFiles 找出匹配模式的文件
func (p *Processor) findMatchingFiles() ([]string, error) {
	var matchingFiles []string

	err := filepath.Walk(p.opts.Directory, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return nil // 跳过无法访问的项
		}

		// 仅处理文件
		if !info.IsDir() {
			// 检查文件名是否匹配模式
			if p.pattern.MatchString(filepath.Base(path)) {
				matchingFiles = append(matchingFiles, path)
			}
		}

		return nil
	})

	if err != nil {
		return nil, fmt.Errorf("查找文件时出错: %w", err)
	}

	return matchingFiles, nil
}

// renameFile 重命名文件
func (p *Processor) renameFile(sourcePath string) (string, error) {
	dir := filepath.Dir(sourcePath)
	oldName := filepath.Base(sourcePath)
	ext := filepath.Ext(oldName)
	nameWithoutExt := oldName[:len(oldName)-len(ext)]

	// 应用重命名模式
	newName := p.opts.OperationValue
	newName = strings.ReplaceAll(newName, "{name}", nameWithoutExt)
	newName = strings.ReplaceAll(newName, "{ext}", ext)

	// 构建目标路径
	targetPath := filepath.Join(dir, newName)

	// 如果不是预览模式,执行实际重命名
	if !p.opts.DryRun {
		if err := os.Rename(sourcePath, targetPath); err != nil {
			return "", fmt.Errorf("重命名文件失败: %w", err)
		}
	}

	return targetPath, nil
}

// moveFile 移动文件
func (p *Processor) moveFile(sourcePath string) (string, error) {
	fileName := filepath.Base(sourcePath)
	targetPath := filepath.Join(p.opts.OperationValue, fileName)

	// 如果不是预览模式,执行实际移动
	if !p.opts.DryRun {
		if err := os.Rename(sourcePath, targetPath); err != nil {
			// 尝试复制然后删除
			if err := copyFile(sourcePath, targetPath); err != nil {
				return "", fmt.Errorf("移动文件失败: %w", err)
			}
			
			// 删除源文件
			if err := os.Remove(sourcePath); err != nil {
				return targetPath, fmt.Errorf("移动后删除源文件失败: %w", err)
			}
		}
	}

	return targetPath, nil
}

// copyFile 复制文件
func copyFile(src, dst string) error {
	// 打开源文件
	sourceFile, err := os.Open(src)
	if err != nil {
		return err
	}
	defer sourceFile.Close()

	// 创建目标文件
	destFile, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer destFile.Close()

	// 复制内容
	_, err = io.Copy(destFile, sourceFile)
	return err
}

// wildcardToRegexp 将文件系统通配符转换为正则表达式
func wildcardToRegexp(pattern string) string {
	pattern = regexp.QuoteMeta(pattern)
	pattern = strings.ReplaceAll(pattern, "\\*", ".*")
	pattern = strings.ReplaceAll(pattern, "\\?", ".")
	return "^" + pattern + "$"
}

四、项目优化与完善

4.1 单元测试

为了确保我们的代码质量,需要编写单元测试。下面是finder包的测试示例:

package finder

import (
	"os"
	"path/filepath"
	"testing"
	"time"
)

func TestParseSize(t *testing.T) {
	tests := []struct {
		input    string
		expected int64
		hasError bool
	}{
		{"1K", 1024, false},
		{"1KB", 1024, false},
		{"1.5MB", 1572864, false},
		{"2G", 2147483648, false},
		{"abc", 0, true},
	}

	for _, test := range tests {
		result, err := parseSize(test.input)
		if test.hasError && err == nil {
			t.Errorf("parseSize(%s): 期望错误,但得到 nil", test.input)
		}
		if !test.hasError && err != nil {
			t.Errorf("parseSize(%s): 期望无错误,但得到 %v", test.input, err)
		}
		if result != test.expected {
			t.Errorf("parseSize(%s): 期望 %d,但得到 %d", test.input, test.expected, result)
		}
	}
}

func TestParseDuration(t *testing.T) {
	tests := []struct {
		input    string
		expected time.Duration
		hasError bool
	}{
		{"1d", 24 * time.Hour, false},
		{"2h", 2 * time.Hour, false},
		{"30m", 30 * time.Minute, false},
		{"xyz", 0, true},
	}

	for _, test := range tests {
		result, err := parseDuration(test.input)
		if test.hasError && err == nil {
			t.Errorf("parseDuration(%s): 期望错误,但得到 nil", test.input)
		}
		if !test.hasError && err != nil {
			t.Errorf("parseDuration(%s): 期望无错误,但得到 %v", test.input, err)
		}
		if result != test.expected {
			t.Errorf("parseDuration(%s): 期望 %v,但得到 %v", test.input, test.expected, result)
		}
	}
}

func TestWildcardToRegexp(t *testing.T) {
	tests := []struct {
		pattern  string
		testStr  string
		expected bool
	}{
		{"*.go", "main.go", true},
		{"*.go", "main.txt", false},
		{"test?.txt", "test1.txt", true},
		{"test?.txt", "test12.txt", false},
	}

	for _, test := range tests {
		pattern := wildcardToRegexp(test.pattern)
		matched, _ := regexp.MatchString(pattern, test.testStr)
		if matched != test.expected {
			t.Errorf("wildcard %s matching %s: 期望 %v,但得到 %v", 
				test.pattern, test.testStr, test.expected, matched)
		}
	}
}

4.2 错误处理优化

我们可以优化错误处理,使其更加用户友好:

// 创建自定义错误类型
type ErrInvalidArgument struct {
	Arg     string
	Message string
}

func (e *ErrInvalidArgument) Error() string {
	return fmt.Sprintf("无效的参数 %s: %s", e.Arg, e.Message)
}

// 使用自定义错误
func parseOptions() error {
	if opts.MinSize != "" {
		size, err := parseSize(opts.MinSize)
		if err != nil {
			return &ErrInvalidArgument{
				Arg:     "min-size",
				Message: err.Error(),
			}
		}
		minSize = size
	}
	// ... 其他代码
}

4.3 性能优化

为了提高大目录处理的性能,我们可以:

  1. 使用工作池限制并发goroutine数量:
// 创建工作池
type job struct {
	path string
}

func processDirectory(rootDir string) {
	// 创建任务队列和工作池
	jobs := make(chan job, 100)
	results := make(chan result, 100)
	
	// 启动工作池
	for w := 1; w <= runtime.NumCPU(); w++ {
		go worker(jobs, results)
	}
	
	// 发送任务
	go func() {
		// 遍历目录并发送任务
		filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
			jobs <- job{path: path}
			return nil
		})
		close(jobs)
	}()
	
	// 收集结果
	// ...
}
  1. 使用 filepath.WalkDir 代替 filepath.Walk 提高性能:
filepath.WalkDir(rootDir, func(path string, d fs.DirEntry, err error) error {
    // 使用 d.Type() 比获取完整的 FileInfo 更快
    if d.IsDir() {
        // 处理目录
    } else {
        // 处理文件
    }
    return nil
})

4.4 用户界面改进

为了改善用户体验,我们可以使用彩色输出:

import "github.com/fatih/color"

// 设置不同的颜色
errorColor := color.New(color.FgRed, color.Bold)
successColor := color.New(color.FgGreen)
infoColor := color.New(color.FgCyan)

// 使用彩色输出
errorColor.Printf("错误: %v\n", err)
successColor.Printf("成功处理 %d 个文件\n", count)
infoColor.Println("处理完成")

五、使用示例

5.1 查找最近修改的大文件

# 查找过去24小时内修改的大于10MB的文件
gofiles find -d /home/user/projects --newer-than 24h --min-size 10M

5.2 搜索代码中的TODO注释

# 搜索所有Go源文件中的TODO注释
gofiles search -d ./src -p "TODO|FIXME" -t .go --ignore-case

5.3 分析项目目录

# 分析项目目录结构和大小分布
gofiles stats -d ./myapp

5.4 批量重命名文件

# 将所有.txt文件重命名为.md文件
gofiles batch -d ./docs -p "*.txt" --rename "{name}.md"

六、总结与扩展方向

通过这个项目,我们实践了Go语言的核心概念和功能,包括:

  1. 基础语法:变量、函数、控制结构
  2. 错误处理:使用返回值而非异常传播错误
  3. 并发编程:使用goroutine和同步原语
  4. 模块化设计:按功能组织代码
  5. 文件系统操作:使用标准库处理文件和目录
  6. 命令行界面:使用cobra库构建CLI
  7. 正则表达式:使用regexp包解析和匹配模式

这个项目还有很多可以扩展的方向:

  1. 添加更多功能

    • 文件内容替换
    • 文件比较
    • 重复文件检测
    • 文件元数据编辑
  2. 改进用户界面

    • 交互式模式
    • Web界面
    • 配置文件支持
  3. 增强性能

    • 缓存搜索结果
    • 使用更高效的算法
    • 实现增量扫描
  4. 分布式支持

    • 处理远程文件系统
    • 分布式处理大型目录

通过这个项目,我们不仅学习了Go语言的核心概念,还体验了一个完整的软件开发流程,从需求分析、设计到实现和测试。这些经验将帮助你在未来的Go项目开发中更加得心应手。

七、参考资源

  1. Go语言官方文档:https://golang.org/doc/
  2. Cobra库文档:https://github.com/spf13/cobra
  3. 文件系统操作指南:https://golang.org/pkg/os/
  4. Go正则表达式教程:https://golang.org/pkg/regexp/
  5. Go并发编程模式:https://blog.golang.org/pipelines

希望这个项目能帮助你巩固Go语言的基础知识,培养良好的编程习惯,并为你提供实用的文件处理工具。


系列总结

至此,我们已经完成了Go语言基础系列的全部12篇文章,从基本语法介绍到实战项目开发。这个系列全面涵盖了Go语言从入门到进阶的核心内容:

  1. Go简介与环境搭建
  2. 变量、常量与基本数据类型
  3. 控制结构(条件和循环)
  4. 函数与方法
  5. 数组与切片
  6. 映射(Map)
  7. 指针
  8. 结构体
  9. 接口
  10. 并发编程
  11. 错误处理与异常机制
  12. 项目实战:命令行工具

通过这个系列,我们不仅学习了Go语言的语法特性,还深入了解了Go的设计理念和实践方法。Go语言以其简洁、高效和强大的并发支持著称,非常适合构建现代化的服务端应用、云原生应用和微服务系统。

在实战项目中,我们综合运用了前面学习的各种知识点,体验了一个完整的Go项目开发流程。这种从理论到实践的学习方式,能够帮助你更好地掌握Go语言,并在实际工作中灵活应用。

希望这个系列能帮助你快速掌握Go语言,构建高效、可靠的应用程序。如果你有任何问题或建议,欢迎在评论区留言,我们将不断完善和更新这个系列。

下一个系列,我们将深入探讨Go的高级主题,包括Web开发、微服务架构、性能优化等内容,敬请期待!


👨‍💻 关于作者与Gopher部落

"Gopher部落"专注于Go语言技术分享,致力于为开发者提供从入门到精通的完整学习路线。我们提供:

  • 📚 系统化的Go语言学习教程
  • 🔥 最新Go生态技术动态
  • 💡 实用开发技巧与最佳实践
  • 🚀 大厂项目实战经验分享

🎁 读者福利

关注"Gopher部落"微信公众号,即可获得:

  1. 完整Go学习路线图:从入门到高级的完整学习路径
  2. 面试题集锦:精选Go语言面试题及答案解析
  3. 项目源码:实战项目完整源码及详细注释
  4. 个性化学习计划:根据你的水平定制专属学习方案

如果您觉得这篇文章有帮助,请点赞、收藏并关注,这是对我们最大的支持!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Gopher部落

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值