写点什么

Uber/Google Golang 编码标准深度分析

作者:俞凡
  • 2024-03-03
    上海
  • 本文字数:4878 字

    阅读完需:约 16 分钟

良好的代码风格对于开发优秀的产品至关重要,本文通过分析比较三部流传甚广的 Golang 代码风格指南,介绍了 Go 代码风格要点,并介绍了通过工具实现代码检查的方式。原文: Mastering Go: In-Depth Analysis of Uber and Google’s Coding Standards


题图来自 Unsplash


在软件开发过程中,遵守代码风格指南和编码标准不仅是为了保持代码在视觉上的一致性,更重要的是为了使代码更易于理解、维护以及避免错误,以简单高效著称的 Golang 也不例外。本文通过深入研究从 Effective GoGoogle Go Style GuideUber Go Style Guide 等资料中获得的通用标准和实践,揭示 Go 编程风格指南的精髓,讨论有助于执行标准的工具,强调自动化检查的局限性,并指出开发人员应内化的关键方面。

简化 Go:顶级风格指南的启示

三份指南的侧重点各有不同,但都是关于 Go 编码风格、格式和惯例的。


  • Effective Go 包含所有数据结构的基本使用、初始化、控制结构、并发和错误处理,对初学者更友好。例如,介绍了什么是 init()

  • Uber Go Style Guide 深入探讨了现实中的 Go 编码实践。例如,它建议避免使用 init(),并解释了原因。

  • Google Go Style Guide 是 Uber 指南的升级版,进一步扩展了详细示例和最佳实践。例如,介绍了注释,并在格式、包注释和文档注释部分给出了示例。

关于编码约定的重要启示

命名约定

命名不仅是代码的外观特征,也是任何人阅读代码时的第一行文档,有效的命名可以让代码不言自明。


如果有其他语言的使用经验,你会发现这些指南有着几乎相同的标准,例如变量应该使用有意义的名称,常量应该全部使用大写字母,包名应该简洁明了。不过,有时"经验"恰恰相反,例如,Go 语言中的Getter/Setter方法不需要以GetSet开头,而在 Java 中则需要;在 Go 语言中,最好不要在包中使用通用名称


Uber 指南中只在包名函数名错误名中提到了命名约定,并且没有提供任何示例,而在 Google 指南的第一章中,命名约定就涵盖了变量名接收器名常量名等命名方法,以及需要避免的重复getter函数

错误处理

Go 采用了独特的错误处理方法,鼓励开发人员检查出现的错误,并通过及时、可预测的方式进行处理。正确的错误处理还包括为错误提供上下文,使调试更加简单。


无论是 Uber 指南还是 Google 指南,关于错误处理的内容都大同小异,包括错误定义、错误返回和处理,以及 panic 处理。

格式化

清晰一致的格式使代码具有很高的可理解性和可读性。常见的代码格式,如缩进、括号对齐、组合变量定义、行的最大长度等,不仅适用于 Go,也适用于所有语言。Google 指南中的字面量格式函数格式以及条件和循环提供了很好的参考。

数据结构的构建和使用

这三部指南都涵盖了数据结构,如mapsslicesarrayschannels,每种结构都有其特定用途。


  • 不同数据类型的零值。例如,slice 不需要初始化,而是通过声明 var s []int 直接使用,那么 s := []int{} 就是"坏"代码。

  • 不同数据类型的初始化。例如通过new进行make、声明 slicemap 的容量,以提高代码效率。在 Uber 指南中的初始化结构初始化Maps中,可以找到更多做法。

  • chanfile 等资源的使用和回收。例如,Uber 指南中的channel大小为一或无章节会告诉你如何确定 chan 是否需要缓冲区以及设置多少缓冲区。


此外,我们还可以在 Google 指南中了解更多有关 Go 特定数据结构(如接口goroutine、生命周期泛型)的代码风格。

测试

Google 指南强调测试的清晰性和可维护性。


  • 为输出和关键功能编写测试。

  • 针对多种场景使用表格驱动测试。

  • Test 开头,对测试功能进行描述性命名。

  • 保持测试简单,避免测试 Go 标准库。

  • 记录复杂的测试逻辑,以便更好理解。


Uber 指南只提到了表格测试模式

并发性

并发是 Go 的固有特性,其主要特点是使用 goroutines 和 channels,使代码高效且并行。不过,需要进行谨慎的同步和通信,从而避免死锁和竞争条件等常见陷阱。


在 Uber 指南中,并发相关部分穿插在使用 go.uber.org/atomic避免全局变量不要忘记goroutines接收器和接口channel大小是1或空等小节中。在 Google 指南中,并发性在最佳实践章节中有详细介绍。

错误代码示例

请看下面的代码片段,其中充斥着许多常见错误,如无组织导入、命名不当、channel 错误、无效代码等。


package main
import ( "time"
"fmt" "io/ioutil" "math/rand" "os")

var globalData int
const a = 1const b = 2
type user struct { id int `json: "id"` // BadSyntax: should be `json:"id"`(no extra space) nameStr string data *userData LinkedUrl string }
type userData struct { description string detailsID int}

func processdata(u *user, params ...string) { if len(params) > 10 { fmt.Println("Too many parameters") return }
file, err := os.Open("data.txt") if err != nil { fmt.Println(err) return } defer file.Close()
content, _ := ioutil.ReadAll(file) fmt.Println("File content:", string(content))
src := rand.NewSource(time.Now().UnixNano()) rnd := rand.New(src) fmt.Println("Random number:", rnd.Intn(100))
go func() { fmt.Println("Asynchronous operation") // Assume more complex logic... }()
if u.id > 100 { fmt.Println("ID is high") } else { fmt.Println("ID is normal") }
u.data = &userData{description: "", detailsID: 1}
dataSlice := []int{} for i := 0; i < 100; i++ { dataSlice = append(dataSlice, i) }
ch := make(chan int, 10) ch <- 1 if u.nameStr == "" { fmt.Println("Name is empty") } else { fmt.Println("Name is not empty") } unreachableCode()
if err := doSomething(1, 2, "", 3, "", 4, "", 5); err != nil { panic(err) }}
func unreachableCode() { return fmt.Println("This will never be called")}
func uncalledFunc() { return}
func doSomething(p1 int, p2 int, p3 string, p4 int, p5 string, p6 int, p7 string, p8 int) error { return nil}
func main() { u := &user{id: 1, nameStr: "John Doe"} processdata(u, "param1", "param2", "param3")}
复制代码


你可以发现多少问题?可以在 Github 上找到我的答案

工具化的合规之路

Go 工具

gofmtgovetgolint 的正式设计目的是促进 Go 代码的合规性。


gofmt 主要用于格式化,确保代码遵循标准格式约定,例如:


  • 一致的缩进和间距:与推荐的代码结构和可读性做法保持一致。

  • 正确的换行和括号位置:遵守控制结构和复合类型的惯例。

  • 有组织的导入:与建议的导入语句分组和排序保持一致。


govet 会检查代码是否存在潜在错误,如无法实现的代码或有问题的类型断言,并与风格指南的健壮性和可维护性目标保持一致。


  • 错误处理:检测无法访问的代码或可能被绕过的检查。

  • 并发:识别 goroutine 和 channel 使用中的常见错误。

  • 代码正确性:标记可疑结构,如格式字符串不正确的 Printf 调用。

  • 变量声明:警告变量可能无意中被覆盖。


golint 侧重于风格,标记不理想的代码模式或偏离 Go 风格的代码,特别是处理以下问题:


  • 命名约定:确保变量、常量、函数和其他标识符按照 Go 的大小写敏感规则正确命名。

  • 注释格式化:检查导出类型、函数和方法的注释是否格式正确、位置恰当。

  • 导出实体:验证导出的函数、变量和类型是否有正确的文档记录。

  • 代码简化:标记可简化的不必要的复杂结构。

golangci-lint

golangci-lint 包含 golint,并在社区支持下引入了更多扩展,解决了 Effective Go、Uber 和 Google 指南中强调的各种问题。


  • 错误处理:确保正确检查和处理错误。

  • 代码复杂性:标记过于复杂的函数,提高可读性和可维护性。

  • 并发问题:检测并发原语的竞争条件和不当使用。

  • 性能优化:识别低效代码模式,加以改进以提高性能。

  • 编码风格:执行命名约定和其他与风格相关的准则,与惯用的 Go 实践保持一致。

实践

现在我们尝试使用工具来检查"错误代码示例"。


首先使用 gofmtgovetgolint,分别运行以下脚本。


#!/bin/bash
echo "Running gofmt..."# List & Write formatting differes and results to stdoutgofmt -l -w .
echo "Running go vet..."go vet ./...
echo "Running golint..."# Show as many warnings as possible (default threshold min_confidence=0.8)golint -min_confidence=0.1 ./...
复制代码



gofmt 没有输出。原因是我们在使用集成开发环境(IDE)时没有额外添加格式化功能,例如,当我使用 VSCode 和 Golang 扩展时,一些 gofmt 功能(如缩进和导入排序)会默认提供,而下面的导入问题超出了 gofmt 的能力范围。


import ( "time" // 额外的空行,gofmt无法解决 "fmt"       // BadImportOrdering: "fmt"应该与其他标准库导入分组 "io/ioutil" // 弃用api: io/ioutil自Go 1.19起已弃用 "math/rand" "os")
复制代码


govet 只能发现两个问题,即 JSON 标记语法和无法访问的代码。


golint 还发现了两个小的编码规范问题,在将阈值调整到最低后,软件包注释和命令都不见了。


golangci-lint 性能怎么样?


首先,我们配置一下 golangci-lint 的执行,有两种方法:一种是通过命令行启用参数 --enable-all ,然后执行以下命令将所有警告和错误信息导入 issues.txt 文件。


golangci-lint run --enable-all --out-format=json ./... | jq 'del(.Report)' > issues.txt
复制代码


另一种方法是配置 .golangci.yml 文件以启用所有检查,然后执行 golangci-lint run --out-format=json ./...| jq 'del(.Report)' > issues.txt


run:  timeout: 5m  modules-download-mode: readonly
linters: enable-all: true
issues: exclude-use-default: false max-issues-per-linter: 0 max-same-issues: 0
复制代码


太棒了!golangci-lint 返回了 37 个问题,例如无效代码,如 struct 中的 globalData、全局常量 a、b 和函数 uncalledFuncerrorHandling里的未处理错误;格式化问题,如注释中缺少句号和结尾的空白;API 使用问题,如使用 math/rand 而非 crypto/rand;不可调用代码,如 unreachableCode 方法中的代码;命名问题,如 LinkedUrl 等。如果感兴趣,请查看完整问题列表

超越工具

工具有局限性

golangci-lint 在查找问题方面表现出色,但仍有局限性,更不用说其他 Go 工具了。


例如,在上面的示例中,doSomething 方法传递了 8 个参数,但却没有检测到过长的参数列表,这无疑违反了代码约定。


此外,在第 72 行中,代码使用 []int{} 来初始化切片,根据 Uber 和 Google 指南,应该避免使用这种方法,因为 nil 是有效切片,我们应该在声明后直接使用切片。

工具无法做到的
  • 命名的上下文:工具无法判断名称是否反映了变量的目的,选择有意义的名称取决于开发者。

  • 正确处理错误:虽然工具可以捕捉被忽略的错误,但提供适当的上下文和优雅的处理错误是开发人员的责任。

  • 优化数据结构:了解复杂性和选择正确的数据结构超出了自动化工具的范畴。

  • 并发模式:要正确实现并发模式、避免死锁并确保 goroutines 之间的高效通信,就必须深入了解 Go 的并发模型。

  • 设计选择:何时使用接口、指针或特定数据结构等决定取决于开发人员的判断。

总结

虽然 gofmtgo vetgolintgolangci-lint 是维护干净、可读性和标准代码库不可或缺的工具,但对 Go 最佳实践和常见陷阱的细致入微的理解才是精通 Go 的开发人员的与众不同之处。优秀的开发者会利用工具提高工作效率,但依靠自己的判断力和知识实现卓越。




你好,我是俞凡,在 Motorola 做过研发,现在在 Mavenir 做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI 等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

发布于: 刚刚阅读数: 5
用户头像

俞凡

关注

公众号:DeepNoMind 2017-10-18 加入

俞凡,Mavenir Systems研发总监,关注高可用架构、高性能服务、5G、人工智能、区块链、DevOps、Agile等。公众号:DeepNoMind

评论

发布
暂无评论
Uber/Google Golang编码标准深度分析_golang_俞凡_InfoQ写作社区