赞
踩
良好的代码风格对于开发优秀的产品至关重要,本文通过分析比较三部流传甚广的Golang代码风格指南,介绍了Go代码风格要点,并介绍了通过工具实现代码检查的方式。原文: Mastering Go: In-Depth Analysis of Uber and Google’s Coding Standards[1]
在软件开发过程中,遵守代码风格指南和编码标准不仅是为了保持代码在视觉上的一致性,更重要的是为了使代码更易于理解、维护以及避免错误,以简单高效著称的 Golang 也不例外。本文通过深入研究从 Effective Go[2]、Google Go Style Guide[3] 和 Uber Go Style Guide[4] 等资料中获得的通用标准和实践,揭示 Go 编程风格指南的精髓,讨论有助于执行标准的工具,强调自动化检查的局限性,并指出开发人员应内化的关键方面。
三份指南的侧重点各有不同,但都是关于Go编码风格、格式和惯例的。
命名不仅是代码的外观特征,也是任何人阅读代码时的第一行文档,有效的命名可以让代码不言自明。
如果有其他语言的使用经验,你会发现这些指南有着几乎相同的标准,例如变量应该使用有意义的名称,常量应该全部使用大写字母,包名应该简洁明了。不过,有时"经验"恰恰相反,例如,Go语言中的Getter/Setter
方法不需要以Get
或Set
开头,而在Java中则需要;在Go语言中,最好不要在包中使用通用名称[6]。
Uber 指南中只在包名[7]、函数名[8]和错误名[9]中提到了命名约定,并且没有提供任何示例,而在 Google 指南的第一章中,命名约定就涵盖了变量名[10]、接收器名[11]、常量名[12]等命名方法,以及需要避免的重复[13]和 getter函数[14]。
Go 采用了独特的错误处理方法,鼓励开发人员检查出现的错误,并通过及时、可预测的方式进行处理。正确的错误处理还包括为错误提供上下文,使调试更加简单。
无论是 Uber 指南还是 Google 指南,关于错误处理[15]的内容都大同小异,包括错误定义、错误返回和处理,以及panic处理。
清晰一致的格式使代码具有很高的可理解性和可读性。常见的代码格式,如缩进、括号对齐、组合变量定义、行的最大长度等,不仅适用于 Go,也适用于所有语言。Google 指南中的字面量格式[16]、函数格式[17]以及条件和循环[18]提供了很好的参考。
这三部指南都涵盖了数据结构,如maps
、slices
、arrays
和channels
,每种结构都有其特定用途。
slice
不需要初始化,而是通过声明
var s []int
直接使用,那么
s := []int{}
就是"坏"代码。
new
进行
make
、声明
slice
和
map
的容量,以提高代码效率。在 Uber 指南中的
初始化结构
[19]和
初始化Maps
[20]中,可以找到更多做法。
chan
和
file
等资源的使用和回收。例如,Uber 指南中的
channel大小为一或无
[21]章节会告诉你如何确定
chan
是否需要缓冲区以及设置多少缓冲区。
此外,我们还可以在 Google 指南中了解更多有关 Go 特定数据结构(如接口[22]、goroutine、生命周期[23]和泛型[24])的代码风格。
Google 指南强调测试的清晰性和可维护性。
Test
开头,对测试功能进行描述性命名。
Uber 指南只提到了表格测试模式[25]。
并发是 Go 的固有特性,其主要特点是使用 goroutines 和 channels,使代码高效且并行。不过,需要进行谨慎的同步和通信,从而避免死锁和竞争条件等常见陷阱。
在Uber 指南中,并发相关部分穿插在使用 go.uber.org/atomic[26]、避免全局变量[27]、不要忘记goroutines[28]、接收器和接口[29]和channel大小是1或空[30]等小节中。在Google 指南中,并发性在最佳实践章节中有详细介绍。
请看下面的代码片段,其中充斥着许多常见错误,如无组织导入、命名不当、channel错误、无效代码等。
package main
import (
"time"
"fmt"
"io/ioutil"
"math/rand"
"os"
)
var globalData int
const a = 1
const 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上找到我的答案[31]。
gofmt
、govet
和 golint
的正式设计目的是促进 Go 代码的合规性。
gofmt[32] 主要用于格式化,确保代码遵循标准格式约定,例如:
govet[33] 会检查代码是否存在潜在错误,如无法实现的代码或有问题的类型断言,并与风格指南的健壮性和可维护性目标保持一致。
golint[34] 侧重于风格,标记不理想的代码模式或偏离Go风格的代码,特别是处理以下问题:
golangci-lint
包含 golint
,并在社区支持下引入了更多扩展,解决了 Effective Go、Uber 和 Google 指南中强调的各种问题。
现在我们尝试使用工具来检查"错误代码示例"。
首先使用 gofmt
、govet
和 golint
,分别运行以下脚本。
#!/bin/bash
echo "Running gofmt..."
# List & Write formatting differes and results to stdout
gofmt -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
和函数 uncalledFunc
;errorHandling
里的未处理错误;格式化问题,如注释中缺少句号和结尾的空白;API 使用问题,如使用 math/rand
而非 crypto/rand
;不可调用代码,如 unreachableCode
方法中的代码;命名问题,如 LinkedUrl
等。如果感兴趣,请查看完整问题列表[35]。
golangci-lint
在查找问题方面表现出色,但仍有局限性,更不用说其他 Go 工具了。
例如,在上面的示例中,doSomething
方法传递了 8 个参数,但却没有检测到过长的参数列表,这无疑违反了代码约定。
此外,在第 72 行中,代码使用 []int{}
来初始化切片,根据 Uber 和 Google 指南,应该避免使用这种方法,因为 nil 是有效切片[36],我们应该在声明后直接使用切片。
虽然 gofmt
、go vet
、golint
和 golangci-lint
是维护干净、可读性和标准代码库不可或缺的工具,但对 Go 最佳实践和常见陷阱的细致入微的理解才是精通 Go 的开发人员的与众不同之处。优秀的开发者会利用工具提高工作效率,但依靠自己的判断力和知识实现卓越。
你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!
Mastering Go: In-Depth Analysis of Uber and Google’s Coding Standards: https://laiyuanyuan-sg.medium.com/mastering-go-in-depth-analysis-of-uber-and-googles-coding-standards-3b3fb9391ee3
[2]Effective Go: https://go.dev/doc/effective_go
[3]Google Go Style Guide: https://google.github.io/styleguide/go/decisions
[4]Uber Go Style Guide: https://github.com/uber-go/guide/blob/master/style.md
[5]Effective Go: init: https://go.dev/doc/effective_go#init
[6]Avoid package names like base util or common: https://dave.cheney.net/2019/01/08/avoid-package-names-like-base-util-or-common
[7]Uber Go Sytle Guide: package name: https://github.com/uber-go/guide/blob/master/style.md#package-names
[8]Uber Go Style Guide: function name: https://github.com/uber-go/guide/blob/master/style.md#function-names
[9]Uber Go Style Guide: error naming: https://github.com/uber-go/guide/blob/master/style.md#error-naming
[10]Google Go Style Guide: variable names: https://google.github.io/styleguide/go/decisions#variable-names
[11]Google Go Style Guide: receiver names: https://google.github.io/styleguide/go/decisions#receiver-names
[12]Google Go Style Guide: constant names: https://google.github.io/styleguide/go/decisions#constant-names
[13]Google Go Style Guide: variable names: https://google.github.io/styleguide/go/decisions#variable-names
[14]Google Go Style Guide: getters: https://google.github.io/styleguide/go/decisions#getters
[15]Uber Go Style Guide: errors: https://github.com/uber-go/guide/blob/master/style.md#errors
[16]Google Go Style Guide: literal formatting: https://google.github.io/styleguide/go/decisions#literal-formatting
[17]Google Go Style Guide: func formatting: https://google.github.io/styleguide/go/decisions#func-formatting
[18]Google Go Style Guide: conditionals and loops: https://google.github.io/styleguide/go/decisions#conditionals-and-loops
[19]Uber Go Style Guide: initializing structs: https://github.com/uber-go/guide/blob/master/style.md#initializing-structs
[20]Uber Go Style Guide: initializing maps: https://github.com/uber-go/guide/blob/master/style.md#initializing-maps
[21]Uber Go Style Guide: channel size is one or none: https://github.com/uber-go/guide/blob/master/style.md#channel-size-is-one-or-none
[22]Google Go Style Guide: interfaces: https://google.github.io/styleguide/go/decisions#interfaces
[23]Google Go Style Guide: goroutine lifetimes: https://google.github.io/styleguide/go/decisions#goroutine-lifetimes
[24]Google Go Style Guide: generics: https://google.github.io/styleguide/go/decisions#generics
[25]Uber Go Style Guide: test tables: https://github.com/uber-go/guide/blob/master/style.md#test-tables
[26]Uber Go Style Guide: use go.uber.org/atomic: https://github.com/uber-go/guide/blob/master/style.md#use-gouberorgatomic
[27]Uber Go Style Guide: Avoid Mutable Globals: https://github.com/uber-go/guide/blob/master/style.md#avoid-mutable-globals
[28]Don't fire-and-forget goroutines: https://github.com/uber-go/guide/blob/master/style.md#dont-fire-and-forget-goroutines
[29]Receiver and Interfaces: https://github.com/uber-go/guide/blob/master/style.md#receivers-and-interfaces,
[30]Channel Size is One or None: https://github.com/uber-go/guide/blob/master/style.md#channel-size-is-one-or-none,
[31]Bad Go code example: https://gist.github.com/slaise/c473bb6ca996d5f8a627c1bdafc27fb0
[32]gofmt: https://pkg.go.dev/cmd/gofmt
[33]govet: https://pkg.go.dev/github.com/golangci/govet
[34]golint: https://pkg.go.dev/golang.org/x/lint/golint
[35]错误代码示例问题列表: https://github.com/slaise/goguides/blob/main/issues.txt
[36]Uber Go Style Guide: nil is a valid slice: https://github.com/uber-go/guide/blob/master/style.md#nil-is-a-valid-slice
本文由 mdnice 多平台发布
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。