欢迎大家到我的博客浏览 YinKai 's Blog | 手把手带你写一个小工具
这篇文章带大家动手实现一个小工具,能够将一个 Go 文件中的注释内容删除。
起因是这样的,我在写文章的时候,最后需要附上项目的源码,然后就发现我在写代码的时候加了很多注释,然后需要自己手动注释很麻烦,于是就想着写这样一个工具,去代替手动删除注释这一项工作。
当然我知道可以用 AI 来做这件事,但我就是想写一个工具,别烦!
需求分析
首先我们要清楚,Go 语言中的注释分为两种:单行注释和多行注释。
单行注释,是采用 // ....
的形式,将注释写在 //
的后面;
而多行注释,是采用 /* ... */
的方式,将注释写在 /*
和 */
之间的。
我们的目的是将一个 Go 文件的注释删除,并出到另一个文件中。为什么这里不直接修改源文件?因为避免程序出错,导致源文件的代码丢失。
那这个过程中就会涉及到文件的打开关闭以及写入、如何正确识别单行注释和多行注释、如何处理一些特殊情况等问题,下面我们会一一展开说明。
代码实现
由于这只是一个小工具,我们就把所有的代码全部写在一个 main.go 文件就足够了。
我们的实现过程,大概是这样的:
通过命令行指定需要处理的 Go 文件
根据文件名调用对应的功能函数去进行处理
a. 打开文件
b. 删除注释和空行
c. 重新构建一个新的文件,并将处理后的结果写入文件中
d. 控制台输出打印成功
如果出现错误,就在控制台中打印信息
下面我们就根据上面的步骤一步一步实现我们的小工具。
获取文件
首先,我们在项目根目录下创建一个 main.go 文件和 todo.go 文件。可以提前在 todo.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("文件保存成功!")
}
然后就可以写我们的主函数代码了:
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)
}
}
- 首先判断是否指定了待处理的文件:这里直接通过判断命令行参数的个数判断即可
- 然后将文件的路径取出,传入
removeComments
函数即可 - 如果出错了,就打印对应的信息
然后我们来看看 removeComments
函数的实现逻辑:
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
}
这里相当于一个代理层,并没有将核心的删注释逻辑写在这里,这个函数只是做一个文件的打开关闭,以及将处理后的文件写入新文件的操作:
- 打开文件,并在检查是否发生错误
- 调用
removeNote
函数删除注释和空行,并返回一个字符串 - 将字符串转换为字节数组,写入新文件
- 最后打印操作成功的提示信息
核心逻辑
我们的核心处理逻辑,就在 removeNote
函数中:
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
}
首先我们创建一个扫描器,从传入的文件指针开始逐行读取文件
创建一个标志位,用于跟踪是否存在多行注释
开始逐行扫描内容
a. 先对多行注释进行处理,因为可能在本行的前面几行就已经开启了多行注释,所以需要先判断多行注释的结尾。判断的逻辑就是利用正则表达式寻找本行是否存在
*/
,存在的话,就通过下标截取*/
之前的部分,并更新标志位b. 然后再对单行注释和多行注释的处理
c. 处理完成之后,将非空行添加到输出
output
中d. 最后再检查一下扫描文件时,是否发生错误即可
最后我们再来看看 processLine
函数,这里就是对单行注释和多行注释的判断,其实本质都是一样的,都是通过正则表达式找到对应的符合 //
和 /*
,然后再进行内容的截取,最后将截取的内容返回即可,代码如下:
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
}
完整代码
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 run main.go todo.go
文件保存成功!
请提供要处理的Go文件
出现了意外情况,这里并没有获取到 todo.go 文件,而是将 todo.go 文件当做需要运行的程序去运行了。
解决的办法有两种:
- 一个是将需要删除注释的代码放在 .txt 文件中
- 先编译 main.go 文件,然后再将需要处理的文件当做参数运行 main.exe 文件
两个办法都很容易理解,第一个的话,你把后缀名改掉,go run
命令自然就不会去执行该文件了。第二个办法的话, 我们这里可以再引入一个 Makefile 文件,将多部操作进行一个合并,我们只需要使用 make
命令就可以执行我们预习定义好的命令了:
# Makefile
.PHONY: all
all: build run
build:
go build main.go
run:
./main.exe todo.go
我们来解释一下这个文件:
.PHONY: all
:声明all
是一个伪目标。伪目标通常是一些不产生实际文件的任务,而只是执行其他任务的别名。这里的.PHONY
告诉 make 工具all
是一个伪目标,不要去检查是否有一个文件名为all
。all: build run
:定义了一个名为all
的目标,它依赖于build
和run
两个目标。当执行make all
时,它将首先执行build
,然后执行run
。build:
:定义了一个名为build
的目标。当执行make build
时,它将执行后面的命令,即go build main.go
。run:
:定义了一个名为run
的目标。当执行make run
时,它将执行后面的命令,即./main.exe todo.go
。
然后我们直接使用 make all
和 make run
就能这个小工具了
小结
这篇文章,我从实际出发,带大家手把手写了一个小工具,还涉及到 Makefile 文件的简单使用,希望对大家能够提供帮助。
在平时的学习生活中,大家也可以像我这样,把遇到的一些问题,试着抽象出来,看看能不能实现一个工具,去便捷地完成它们。
有疑问加站长微信联系(非本文作者)

学习了,能下载系统源码吗,有go开发的小程序案例吗,或者能免费下载源码能也行