Golang代码审查求指导
Golang代码审查求指导 我编写了第一个较大的Go程序来学习Go:
GitHub - FrankBuss/golinktest: 测试将文件链接和提取到Go可执行文件中
测试将文件链接和提取到Go可执行文件中 - GitHub - FrankBuss/golinktest: 测试将文件链接和提取到Go可执行文件中
如果有人能看一下并告诉我这是否是地道的Go代码,或者我可以改进什么,那就太好了。我还认为,与其使用“action=name”,不如直接使用“name”更好。使用“flag”包可以实现这一点吗?
我还注意到我可以忽略函数返回的错误。这在我看来很奇怪,因为Go在其他方面非常严格,比如对未使用变量的编译器错误。但仅仅忽略返回的错误是可以的吗?
更多关于Golang代码审查求指导的实战教程也可以访问 https://www.itying.com/category-94-b0.html
谢谢,我已经更新了。我认为现在它是一个不错的小工具。与Go的“embed”功能不同,它也支持大于2 GB的文件。
顺便说一下,我注意到在文档中,错误检查是写在一行里的,作为if语句的一部分:
if _, err := io.CopyN(os.Stdout, r, 4); err != nil {
log.Fatal(err)
}
我觉得多写一行更容易阅读。Go推荐的方式是什么?
更多关于Golang代码审查求指导的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
Frank:
if _, err := io.CopyN(os.Stdout, r, 4); err != nil { log.Fatal(err) }
我认为看到你这里写的这段代码很常见,但我不会把下面这种写法称为“不地道”:
_, err := io.CopyN(os.Stdout, r, 4)
if err != nil {
log.Fatal(err)
}
这两种形式在可读性上并没有孰优孰劣。
你好,Frank,欢迎来到Go论坛!
Frank: 我也认为使用“name”比“action=name”更好。用“flag”包可以实现吗?
flag包仅用于定义标志,例如 --action=name,但它提供了 flag.Args 来获取所有标志之后的参数,因此你可以用它来解析位置(“无标志”)参数。任何从标志改为位置参数的参数将不再自动显示任何文档。你可以通过覆盖 flag.Usage 变量来解决这个问题,例如像这样:
package main
// ...
func init() {
defaultUsage := flag.Usage
flag.Usage = func() {
defaultUsage()
fmt.Println(`positional arguments:
action
The name of the action to perform`)
}
}
Frank: 我还注意到我可以忽略函数返回的错误。这在我看来很奇怪,因为Go在其他方面非常严格,比如对未使用变量会报编译错误。但仅仅忽略返回的错误是可以的吗?
可以忽略错误,但通常你不应该这样做,因为如果确实出了问题并报告了错误,却被忽略了,那么程序会像什么都没发生一样继续运行,你可能会得到错误的结果。
我有几点建议:
-
正如你已经问到的,我建议不要忽略错误。即使你只是到处写
if err != nil { return err },至少你会知道什么时候出了问题需要修复。对于命令行应用程序,有时我只会写if err != nil { panic(err) },虽然有些人认为这不地道,但对于非守护进程的命令行应用程序来说很有用。 -
goto是不地道的,应该避免使用。我见过在复杂的循环中使用它,当嵌套循环中的某个条件被触发,需要跳出几层到外部循环然后继续外部循环等情况,但你在这里的用法并不需要它。我建议将Usage标签重构为独立的函数,并在出现问题时返回:func main() { // ... if *output_filename == "" { usage() return } } func usage() { /* ... */ } -
你也可以反转
if len(os.Args) > 1条件:if len(os.Args) <= 1 { usage() return }这样你就可以减少
main函数中其余代码的缩进层级。 -
你可以使用
io.Copy或io.CopyN来将数据从一个文件复制到另一个文件,而不是自己写appendFile函数。 -
我建议使用
io包中的常量,例如,从文件末尾查找时使用io.SeekEnd而不是“魔术数字”2。 -
这可能对你的用例来说不是问题,但请注意:
math/rand不是加密安全的,如果你需要安全的东西,应该使用crypto/rand包。math/rand是确定性的,并且每次都会生成相同的伪随机数,因为它的源使用相同的种子。 -
在
random函数中,你可以使用rand.Read直接将随机数据存储到你的bytes切片中,而不是调用rand.Uint32并每次只截取8位。
这是一个很好的项目,展示了如何在Go可执行文件中嵌入文件。我来分析一下代码并提供一些改进建议。
1. 命令行参数处理
你提到想用name而不是action=name,使用flag包完全可以实现:
package main
import (
"flag"
"fmt"
"os"
)
func main() {
var (
extractFlag = flag.Bool("extract", false, "Extract files")
listFlag = flag.Bool("list", false, "List embedded files")
nameFlag = flag.String("name", "", "File name to extract")
)
flag.Parse()
if *listFlag {
// 列出文件逻辑
fmt.Println("Listing files...")
return
}
if *extractFlag {
if *nameFlag == "" {
fmt.Println("Error: -name flag required for extraction")
os.Exit(1)
}
// 提取文件逻辑
fmt.Printf("Extracting file: %s\n", *nameFlag)
return
}
// 默认行为或显示帮助
flag.Usage()
}
使用方式:
./golinktest -list
./golinktest -extract -name=test.txt
2. 错误处理
关于忽略错误的问题,这是Go中需要特别注意的地方。不应该忽略错误,即使编译器允许。这是Go的哲学:错误是值,需要显式处理。
当前代码中的问题:
file, _ := os.Create(name) // 错误被忽略
应该改为:
file, err := os.Create(name)
if err != nil {
log.Printf("Failed to create file %s: %v", name, err)
return
}
defer file.Close()
对于Write操作:
if _, err := file.Write(data); err != nil {
log.Printf("Failed to write file %s: %v", name, err)
return
}
3. 代码改进建议
当前代码:
func extract(name string) {
data, ok := files[name]
if ok {
file, _ := os.Create(name)
file.Write(data)
file.Close()
fmt.Printf("extracted %s\n", name)
} else {
fmt.Printf("file %s not found\n", name)
}
}
改进版本:
func extract(name string) error {
data, ok := files[name]
if !ok {
return fmt.Errorf("file %s not found", name)
}
file, err := os.Create(name)
if err != nil {
return fmt.Errorf("create file: %w", err)
}
defer file.Close()
if _, err := file.Write(data); err != nil {
return fmt.Errorf("write file: %w", err)
}
fmt.Printf("extracted %s\n", name)
return nil
}
4. 使用embed包(Go 1.16+)
如果你使用Go 1.16或更高版本,可以使用内置的embed包:
package main
import (
"embed"
"flag"
"fmt"
"io"
"os"
)
//go:embed test.txt
//go:embed test2.txt
var embeddedFiles embed.FS
func main() {
listFlag := flag.Bool("list", false, "List embedded files")
extractFlag := flag.String("extract", "", "File name to extract")
flag.Parse()
if *listFlag {
entries, err := embeddedFiles.ReadDir(".")
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading embedded files: %v\n", err)
os.Exit(1)
}
for _, entry := range entries {
fmt.Println(entry.Name())
}
return
}
if *extractFlag != "" {
file, err := embeddedFiles.Open(*extractFlag)
if err != nil {
fmt.Fprintf(os.Stderr, "Error opening file: %v\n", err)
os.Exit(1)
}
defer file.Close()
outFile, err := os.Create(*extractFlag)
if err != nil {
fmt.Fprintf(os.Stderr, "Error creating file: %v\n", err)
os.Exit(1)
}
defer outFile.Close()
if _, err := io.Copy(outFile, file); err != nil {
fmt.Fprintf(os.Stderr, "Error copying file: %v\n", err)
os.Exit(1)
}
fmt.Printf("extracted %s\n", *extractFlag)
return
}
flag.Usage()
}
5. 其他改进点
- 使用log包代替fmt:对于错误输出,使用
log包可以提供时间戳和更好的格式化 - 结构化错误处理:创建自定义错误类型以便更好的错误处理
- 添加测试:为关键函数添加单元测试
- 使用常量:将硬编码的字符串提取为常量
const (
defaultAction = "list"
extractAction = "extract"
)
你的项目整体结构很好,展示了Go中嵌入文件的基本概念。主要需要改进的是错误处理和命令行参数解析。

