> 欢迎大家到我的博客浏览 <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 文件的简单使用,希望对大家能够提供帮助。
在平时的学习生活中,大家也可以像我这样,把遇到的一些问题,试着抽象出来,看看能不能实现一个工具,去便捷地完成它们。
有疑问加站长微信联系(非本文作者)