这篇文章带大家动手实现一个小工具,能够将一个 Go 文件中的注释。
起因是这样的,我在写文章的时候,最后需要附上项目的源码,然后就发现我在写代码的时候加了很多注释,然后需要自己手动注释很麻烦,于是就想着写这样一个工具,去代替手动删除注释这一项工作。
当然我知道可以用 AI 来做这件事,但我就是想写一个工具,别烦!
需求分析
首先我们要清楚,Go 语言中的注释分为两种:单行注释和多行注释。
单行注释,是采用 // ....
的形式,将注释写在 //
的后面;
而多行注释,是采用 /* ... */
的方式,将注释写在 /*
和 */
之间的。
我们的目的是将一个 Go 文件的注释删除,并出到另一个文件中。为什么这里不直接修改源文件?因为避免程序出错,导致源文件的代码丢失。
那这个过程中就会涉及到文件的打开关闭以及写入、如何正确识别单行注释和多行注释、如何处理一些特殊情况等问题,下面我们会一一展开说明。
代码实现
由于这只是一个小工具,我们就把所有的代码全部写在一个 main.go 文件就足够了。
我们的实现过程,大概是这样的:
通过命令行指定需要处理的 Go 文件
根据文件名调用对应的功能函数去进行处理
a. 打开文件
b. 删除注释和空行
c. 重新构建一个新的文件,并将处理后的结果写入文件中
d. 控制台输出打印成功
如果出现错误,就在控制台中打印信息
下面我们就根据上面的步骤一步一步实现我们的小工具。
获取文件
首先,我们在项目根目录下创建一个 main.go 文件和 todo.go 文件。可以提前在 todo.go 文件中放一段带有注释的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| package main
import ( "fmt" "os" )
func saveToFile(filePath string, data string) error { err := os.WriteFile(filePath, []byte(data), 0644) if err != nil { return fmt.Errorf("保存文件失败:%v", err) }
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("文件保存成功!") }
|
然后就可以写我们的主函数代码了:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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
函数的实现逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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
函数中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| 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 }
|
首先我们创建一个扫描器,从传入的文件指针开始逐行读取文件
创建一个标志位,用于跟踪是否存在多行注释
开始逐行扫描内容
a. 先对多行注释进行处理,因为可能在本行的前面几行就已经开启了多行注释,所以需要先判断多行注释的结尾。判断的逻辑就是利用正则表达式寻找本行是否存在 */
,存在的话,就通过下标截取*/
之前的部分,并更新标志位
b. 然后再对单行注释和多行注释的处理
c. 处理完成之后,将非空行添加到输出output
中
d. 最后再检查一下扫描文件时,是否发生错误即可
最后我们再来看看 processLine
函数,这里就是对单行注释和多行注释的判断,其实本质都是一样的,都是通过正则表达式找到对应的符合 //
和 /*
,然后再进行内容的截取,最后将截取的内容返回即可,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| 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 }
|
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
| 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
命令,就可以删除注释了?
我们可以来试一下:
1 2 3
| go run main.go todo.go 文件保存成功! 请提供要处理的Go文件
|
出现了意外情况,这里并没有获取到 todo.go 文件,而是将 todo.go 文件当做需要运行的程序去运行了。
解决的办法有两种:
- 一个是将需要删除注释的代码放在 .txt 文件中
- 先编译 main.go 文件,然后再将需要处理的文件当做参数运行 main.exe 文件
两个办法都很容易理解,第一个的话,你把后缀名改掉,go run
命令自然就不会去执行该文件了。第二个办法的话, 我们这里可以再引入一个 Makefile 文件,将多部操作进行一个合并,我们只需要使用 make
命令就可以执行我们预习定义好的命令了:
1 2 3 4 5 6 7 8 9 10 11
|
.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 文件的简单使用,希望对大家能够提供帮助。
在平时的学习生活中,大家也可以像我这样,把遇到的一些问题,试着抽象出来,看看能不能实现一个工具,去便捷地完成它们。