Golang代码审查求指导

Golang代码审查求指导 我编写了第一个较大的Go程序来学习Go:

GitHub

GitHub - FrankBuss/golinktest: 测试将文件链接和提取到Go可执行文件中

测试将文件链接和提取到Go可执行文件中 - GitHub - FrankBuss/golinktest: 测试将文件链接和提取到Go可执行文件中

如果有人能看一下并告诉我这是否是地道的Go代码,或者我可以改进什么,那就太好了。我还认为,与其使用“action=name”,不如直接使用“name”更好。使用“flag”包可以实现这一点吗?

我还注意到我可以忽略函数返回的错误。这在我看来很奇怪,因为Go在其他方面非常严格,比如对未使用变量的编译器错误。但仅仅忽略返回的错误是可以的吗?


更多关于Golang代码审查求指导的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

谢谢,我已经更新了。我认为现在它是一个不错的小工具。与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在其他方面非常严格,比如对未使用变量会报编译错误。但仅仅忽略返回的错误是可以的吗?

可以忽略错误,但通常你不应该这样做,因为如果确实出了问题并报告了错误,却被忽略了,那么程序会像什么都没发生一样继续运行,你可能会得到错误的结果。

我有几点建议:

  1. 正如你已经问到的,我建议不要忽略错误。即使你只是到处写 if err != nil { return err },至少你会知道什么时候出了问题需要修复。对于命令行应用程序,有时我只会写 if err != nil { panic(err) },虽然有些人认为这不地道,但对于非守护进程的命令行应用程序来说很有用。

  2. goto 是不地道的,应该避免使用。我见过在复杂的循环中使用它,当嵌套循环中的某个条件被触发,需要跳出几层到外部循环然后继续外部循环等情况,但你在这里的用法并不需要它。我建议将 Usage 标签重构为独立的函数,并在出现问题时返回:

    func main() {
        // ...
        if *output_filename == "" {
            usage()
            return
        }
    }
    
    func usage() { /* ... */ }
    
  3. 你也可以反转 if len(os.Args) > 1 条件:

    if len(os.Args) <= 1 {
        usage()
        return
    }
    

    这样你就可以减少 main 函数中其余代码的缩进层级。

  4. 你可以使用 io.Copyio.CopyN 来将数据从一个文件复制到另一个文件,而不是自己写 appendFile 函数。

  5. 我建议使用 io 包中的常量,例如,从文件末尾查找时使用 io.SeekEnd 而不是“魔术数字” 2

  6. 这可能对你的用例来说不是问题,但请注意:math/rand 不是加密安全的,如果你需要安全的东西,应该使用 crypto/rand 包。math/rand 是确定性的,并且每次都会生成相同的伪随机数,因为它的源使用相同的种子。

  7. 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. 其他改进点

  1. 使用log包代替fmt:对于错误输出,使用log包可以提供时间戳和更好的格式化
  2. 结构化错误处理:创建自定义错误类型以便更好的错误处理
  3. 添加测试:为关键函数添加单元测试
  4. 使用常量:将硬编码的字符串提取为常量
const (
    defaultAction = "list"
    extractAction = "extract"
)

你的项目整体结构很好,展示了Go中嵌入文件的基本概念。主要需要改进的是错误处理和命令行参数解析。

回到顶部