手把手用 Go 带你写一个小工具

oYto · · 40967 次点击 · 开始浏览    置顶

> 欢迎大家到我的博客浏览 <a href="https://www.yinkai.cc/post/af0e9d38c0f30cc51c0bcc25e449709e">YinKai 's Blog | 手把手带你写一个小工具</a> ​ 这篇文章带大家动手实现一个小工具,能够将一个 Go 文件中的注释内容删除。<!--more--> ​ 起因是这样的,我在写文章的时候,最后需要附上项目的源码,然后就发现我在写代码的时候加了很多注释,然后需要自己手动注释很麻烦,于是就想着写这样一个工具,去代替手动删除注释这一项工作。 ​ 当然我知道可以用 AI 来做这件事,但我就是想写一个工具,别烦! #### 需求分析 ​ 首先我们要清楚,Go 语言中的注释分为两种:单行注释和多行注释。 ​ 单行注释,是采用 `// ....` 的形式,将注释写在 `//` 的后面; ​ 而多行注释,是采用 `/* ... */` 的方式,将注释写在 `/*` 和 `*/` 之间的。 ​ 我们的目的是将一个 Go 文件的注释删除,并出到另一个文件中。为什么这里不直接修改源文件?因为避免程序出错,导致源文件的代码丢失。 ​ 那这个过程中就会涉及到文件的打开关闭以及写入、如何正确识别单行注释和多行注释、如何处理一些特殊情况等问题,下面我们会一一展开说明。 #### 代码实现 ​ 由于这只是一个小工具,我们就把所有的代码全部写在一个 main.go 文件就足够了。 ​ 我们的实现过程,大概是这样的: 1. 通过命令行指定需要处理的 Go 文件 2. 根据文件名调用对应的功能函数去进行处理 a. 打开文件 b. 删除注释和空行 c. 重新构建一个新的文件,并将处理后的结果写入文件中 d. 控制台输出打印成功 3. 如果出现错误,就在控制台中打印信息 ​ 下面我们就根据上面的步骤一步一步实现我们的小工具。 ##### 获取文件 ​ 首先,我们在项目根目录下创建一个 main.go 文件和 todo.go 文件。可以提前在 todo.go 文件中放一段带有注释的代码: ```go package main import ( "fmt" "os" ) // saveToFile 保存数据到文件 // 参数: // filePath: 要保存的文件路径 // data: 要保存的数据 // 返回值: // error: 如果保存成功,则为nil;否则为保存失败的错误信息 func saveToFile(filePath string, data string) error { // 使用 os.WriteFile 函数将数据写入文件 err := os.WriteFile(filePath, []byte(data), 0644) if err != nil { // 如果写入文件出错,返回错误信息 return fmt.Errorf("保存文件失败:%v", err) } // 文件保存成功,返回nil表示没有错误 return nil } func main() { // 要保存的数据 dataToSave := "Hello, this is some data to be saved to a file." // 保存数据到文件 err := saveToFile("output.txt", dataToSave) if err != nil { // 如果保存失败,打印错误信息 fmt.Println(err) return } // 文件保存成功 fmt.Println("文件保存成功!") } ``` ​ 然后就可以写我们的主函数代码了: ```go func main() { if len(os.Args) < 2 { fmt.Println("请提供要处理的Go文件") return } filePath := os.Args[1] err := removeComments(filePath) if err != nil { fmt.Printf("错误: %v\n", err) } } ``` 1. 首先判断是否指定了待处理的文件:这里直接通过判断命令行参数的个数判断即可 2. 然后将文件的路径取出,传入 `removeComments` 函数即可 3. 如果出错了,就打印对应的信息 ​ 然后我们来看看 `removeComments` 函数的实现逻辑: ```go func removeComments(filePath string) error { // 打开文件 file, err := os.Open(filePath) if err != nil { return err } defer file.Close() // 删除注释 和 空行 并保存 output, err := removeNote(file) if err != nil { return err } // 将结果保存到新文件中 outputFilePath := "output.txt" err = os.WriteFile(outputFilePath, []byte(output), 0644) if err != nil { return err } // 打印操作成功的消息 fmt.Printf("注释已删除,并已保存到 %s 文件中。\n", outputFilePath) return err } ``` ​ 这里相当于一个代理层,并没有将核心的删注释逻辑写在这里,这个函数只是做一个文件的打开关闭,以及将处理后的文件写入新文件的操作: 1. 打开文件,并在检查是否发生错误 2. 调用 `removeNote` 函数删除注释和空行,并返回一个字符串 3. 将字符串转换为字节数组,写入新文件 4. 最后打印操作成功的提示信息 ##### 核心逻辑 ​ 我们的核心处理逻辑,就在 `removeNote` 函数中: ```go func removeNote(file *os.File) (string, error) { // 创建一个新的扫描器,用于逐行读取文件内容 scanner := bufio.NewScanner(file) // 用于跟踪是否在多行注释中 inMultilineComment := false var output string // 逐行扫描文件内容 for scanner.Scan() { line := scanner.Text() // 处理多行注释 if inMultilineComment { // 查找多行注释结束标记 "*/" endIndex := regexp.MustCompile("\\*/").FindStringIndex(line) if endIndex != nil { // 多行注释结束,更新标志并截取剩余部分 inMultilineComment = false //isDelete = true line = line[endIndex[1]:] } else { // 如果当前行还在多行注释中,跳过处理并继续下一行 continue } } // 处理单行注释和多行注释的开始 lineWithoutComments, _ := processLine(line, &inMultilineComment) if hasNonSpaceCharacters(lineWithoutComments) { // 将处理后的行添加到结果字符串 output += lineWithoutComments + "\n" } } // 检查扫描文件时是否发生错误 if err := scanner.Err(); err != nil { return "", err } return output, nil } ``` 1. 首先我们创建一个扫描器,从传入的文件指针开始逐行读取文件 2. 创建一个标志位,用于跟踪是否存在多行注释 3. 开始逐行扫描内容 a. 先对多行注释进行处理,因为可能在本行的前面几行就已经开启了多行注释,所以需要先判断多行注释的结尾。判断的逻辑就是利用正则表达式寻找本行是否存在 `*/`,存在的话,就通过下标截取`*/`之前的部分,并更新标志位 b. 然后再对单行注释和多行注释的处理 c. 处理完成之后,将非空行添加到输出`output` 中 d. 最后再检查一下扫描文件时,是否发生错误即可 ​ 最后我们再来看看 `processLine` 函数,这里就是对单行注释和多行注释的判断,其实本质都是一样的,都是通过正则表达式找到对应的符合 `//` 和 `/*` ,然后再进行内容的截取,最后将截取的内容返回即可,代码如下: ```go func processLine(line string, inMultilineComment *bool) (string, bool) { // 该行是否需要删除:单行注释 或者 多行注释的时候 // 处理单行注释 index := regexp.MustCompile("//").FindStringIndex(line) if index != nil { // 截取注释之前的部分 if index[0] != 0 && line[index[0]-1] == '"' { } else { line = line[:index[0]] } } // 处理多行注释的开始 startIndex := regexp.MustCompile("/\\*").FindStringIndex(line) if startIndex != nil { // 进入多行注释状态,并截取注释之前的部分 *inMultilineComment = true // 查找本行是不是就结束了 isEnd := false endIndex := regexp.MustCompile("\\*/").FindStringIndex(line) if endIndex != nil { // 多行注释结束,更新标志并截取剩余部分 *inMultilineComment = false isEnd = true } if isEnd { line = line[:startIndex[0]] + line[endIndex[1]:] } else { line = line[:startIndex[0]] } } // 返回处理后的行和更新后的多行注释状态 return line, *inMultilineComment } ``` ##### 完整代码 ```go package main import ( "bufio" "fmt" "os" "regexp" "strings" ) func main() { if len(os.Args) < 2 { fmt.Println("请提供要处理的Go文件") return } filePath := os.Args[1] err := removeComments(filePath) if err != nil { fmt.Printf("错误: %v\n", err) } } func removeComments(filePath string) error { file, err := os.Open(filePath) if err != nil { return err } defer file.Close() output, err := removeNote(file) if err != nil { return err } outputFilePath := "output.txt" err = os.WriteFile(outputFilePath, []byte(output), 0644) if err != nil { return err } fmt.Printf("注释已删除,并已保存到 %s 文件中。\n", outputFilePath) return err } func removeNote(file *os.File) (string, error) { scanner := bufio.NewScanner(file) inMultilineComment := false var output string for scanner.Scan() { line := scanner.Text() if inMultilineComment { endIndex := regexp.MustCompile("\\*/").FindStringIndex(line) if endIndex != nil { inMultilineComment = false line = line[endIndex[1]:] } else { continue } } lineWithoutComments, _ := processLine(line, &inMultilineComment) if hasNonSpaceCharacters(lineWithoutComments) { output += lineWithoutComments + "\n" } } if err := scanner.Err(); err != nil { return "", err } return output, nil } func hasNonSpaceCharacters(line string) bool { trimmed := strings.TrimSpace(line) return trimmed != "" } func processLine(line string, inMultilineComment *bool) (string, bool) { index := regexp.MustCompile("//").FindStringIndex(line) if index != nil { if index[0] != 0 && line[index[0]-1] == '"' { } else { line = line[:index[0]] } } startIndex := regexp.MustCompile("/\\*").FindStringIndex(line) if startIndex != nil { *inMultilineComment = true isEnd := false endIndex := regexp.MustCompile("\\*/").FindStringIndex(line) if endIndex != nil { *inMultilineComment = false isEnd = true } if isEnd { line = line[:startIndex[0]] + line[endIndex[1]:] } else { line = line[:startIndex[0]] } } return line, *inMultilineComment } ``` #### 代码执行 ​ 我们利用提前创建好的 todo.go 文件,是不是使用 `go run main.go todo.go` 命令,就可以删除注释了? ​ 我们可以来试一下: ```go go run main.go todo.go 文件保存成功! 请提供要处理的Go文件 ``` ​ 出现了意外情况,这里并没有获取到 todo.go 文件,而是将 todo.go 文件当做需要运行的程序去运行了。 ​ 解决的办法有两种: 1. 一个是将需要删除注释的代码放在 .txt 文件中 2. 先编译 main.go 文件,然后再将需要处理的文件当做参数运行 main.exe 文件 ​ 两个办法都很容易理解,第一个的话,你把后缀名改掉,`go run`命令自然就不会去执行该文件了。第二个办法的话, 我们这里可以再引入一个 Makefile 文件,将多部操作进行一个合并,我们只需要使用 `make` 命令就可以执行我们预习定义好的命令了: ```makefile # Makefile .PHONY: all all: build run build: go build main.go run: ./main.exe todo.go ``` ​ 我们来解释一下这个文件: 1. `.PHONY: all`:声明 `all` 是一个伪目标。伪目标通常是一些不产生实际文件的任务,而只是执行其他任务的别名。这里的 `.PHONY` 告诉 make 工具 `all` 是一个伪目标,不要去检查是否有一个文件名为 `all`。 2. `all: build run`:定义了一个名为 `all` 的目标,它依赖于 `build` 和 `run` 两个目标。当执行 `make all` 时,它将首先执行 `build`,然后执行 `run`。 3. `build:`:定义了一个名为 `build` 的目标。当执行 `make build` 时,它将执行后面的命令,即 `go build main.go`。 4. `run:`:定义了一个名为 `run` 的目标。当执行 `make run` 时,它将执行后面的命令,即 `./main.exe todo.go`。 ​ 然后我们直接使用 `make all` 和 `make run` 就能这个小工具了 #### 小结 ​ 这篇文章,我从实际出发,带大家手把手写了一个小工具,还涉及到 Makefile 文件的简单使用,希望对大家能够提供帮助。 ​ 在平时的学习生活中,大家也可以像我这样,把遇到的一些问题,试着抽象出来,看看能不能实现一个工具,去便捷地完成它们。

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

40967 次点击  ∙  1 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传