Golang编码规范之指导原则05
一 避免使用内置名称
Go 语言规范 概述了几个内置的, 不应在 Go 项目中使用的 预先声明的标识符。
根据上下文的不同,将这些标识符作为名称重复使用, 将在当前作用域(或任何嵌套作用域)中隐藏原始标识符,或者混淆代码。 在最好的情况下,编译器会报错;在最坏的情况下,这样的代码可能会引入潜在的、难以恢复的错误。
- Bad
var error string
// `error` 作用域隐式覆盖
// or
func handleErrorMessage(error string) {
// `error` 作用域隐式覆盖
}
type Foo struct {
// 虽然这些字段在技术上不构成阴影,但`error`或`string`字符串的重映射现在是不明确的。
error error
string string
}
func (f Foo) Error() error {
// `error` 和 `f.error` 在视觉上是相似的
return f.error
}
func (f Foo) String() string {
// `string` and `f.string` 在视觉上是相似的
return f.string
}
- Good
var errorMessage string
// `error` 指向内置的非覆盖
// or
func handleErrorMessage(msg string) {
// `error` 指向内置的非覆盖
}
type Foo struct {
// `error` and `string` 现在是明确的。
err error
str string
}
func (f Foo) Error() error {
return f.err
}
func (f Foo) String() string {
return f.str
}
注意,编译器在使用预先分隔的标识符时不会生成错误, 但是诸如go vet
之类的工具会正确地指出这些和其他情况下的隐式问题。
二 避免使用 init()
尽可能避免使用init()
。当init()
是不可避免或可取的,代码应先尝试:
- 无论程序环境或调用如何,都要完全确定。
- 避免依赖于其他
init()
函数的顺序或副作用。虽然init()
顺序是明确的,但代码可以更改, 因此init()
函数之间的关系可能会使代码变得脆弱和容易出错。 - 避免访问或操作全局或环境状态,如机器信息、环境变量、工作目录、程序参数/输入等。
- 避免
I/O
,包括文件系统、网络和系统调用。
不能满足这些要求的代码可能属于要作为main()
调用的一部分(或程序生命周期中的其他地方), 或者作为main()
本身的一部分写入。特别是,打算由其他程序使用的库应该特别注意完全确定性, 而不是执行“init magic”
- Bad
type Foo struct {
// ...
}
var _defaultFoo Foo
func init() {
_defaultFoo = Foo{
// ...
}
}
type Config struct {
// ...
}
var _config Config
func init() {
// Bad: 基于当前目录
cwd, _ := os.Getwd()
// Bad: I/O
raw, _ := ioutil.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
yaml.Unmarshal(raw, &_config)
}
- Good
var _defaultFoo = Foo{
// ...
}
// or,为了更好的可测试性:
var _defaultFoo = defaultFoo()
func defaultFoo() Foo {
return Foo{
// ...
}
}
type Config struct {
// ...
}
func loadConfig() Config {
cwd, err := os.Getwd()
// handle err
raw, err := ioutil.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
// handle err
var config Config
yaml.Unmarshal(raw, &config)
return config
}
考虑到上述情况,在某些情况下,init()
可能更可取或是必要的,可能包括:
- 不能表示为单个赋值的复杂表达式。
- 可插入的钩子,如
database/sql
、编码类型注册表等。 - 对 Google Cloud Functions 和其他形式的确定性预计算的优化。
三 追加时优先指定切片容量
追加时优先指定切片容量
在尽可能的情况下,在初始化要追加的切片时为make()
提供一个容量值。
- Bad
for n := 0; n < b.N; n++ {
data := make([]int, 0)
for k := 0; k < size; k++{
data = append(data, k)
}
}
BenchmarkBad-4 100000000 2.48s
- Good
for n := 0; n < b.N; n++ {
data := make([]int, 0, size)
for k := 0; k < size; k++{
data = append(data, k)
}
}
BenchmarkGood-4 100000000 0.21s
四 主函数退出方式 (Exit)
4.1 主函数退出
Go 程序使用 os.Exit
或者 log.Fatal*
立即退出 (使用panic
不是退出程序的好方法,请 不要使用 panic。)
仅在main() 中调用其中一个 os.Exit
或者 log.Fatal*
。所有其他函数应将错误返回到信号失败中。
- Bad
func main() {
body := readFile(path)
fmt.Println(body)
}
func readFile(path string) string {
f, err := os.Open(path)
if err != nil {
log.Fatal(err)
}
b, err := ioutil.ReadAll(f)
if err != nil {
log.Fatal(err)
}
return string(b)
}
- Good
func main() {
body, err := readFile(path)
if err != nil {
log.Fatal(err)
}
fmt.Println(body)
}
func readFile(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
b, err := ioutil.ReadAll(f)
if err != nil {
return "", err
}
return string(b), nil
}
原则上:退出的具有多种功能的程序存在一些问题:
- 不明显的控制流:任何函数都可以退出程序,因此很难对控制流进行推理。
- 难以测试:退出程序的函数也将退出调用它的测试。这使得函数很难测试,并引入了跳过
go test
尚未运行的其他测试的风险。 - 跳过清理:当函数退出程序时,会跳过已经进入
defer
队列里的函数调用。这增加了跳过重要清理任务的风险。
4.2 一次性退出
如果可能的话,你的main()
函数中 最多一次 调用 os.Exit
或者log.Fatal
。如果有多个错误场景停止程序执行,请将该逻辑放在单独的函数下并从中返回错误。 这会缩短 main()
函数,并将所有关键业务逻辑放入一个单独的、可测试的函数中。
- Bad
package main
func main() {
args := os.Args[1:]
if len(args) != 1 {
log.Fatal("missing file")
}
name := args[0]
f, err := os.Open(name)
if err != nil {
log.Fatal(err)
}
defer f.Close()
// 如果我们调用 log.Fatal 在这条线之后
// f.Close 将会被执行。
b, err := ioutil.ReadAll(f)
if err != nil {
log.Fatal(err)
}
// ...
}
- Good
package main
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
args := os.Args[1:]
if len(args) != 1 {
return errors.New("missing file")
}
name := args[0]
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
b, err := ioutil.ReadAll(f)
if err != nil {
return err
}
// ...
}
- 点赞
- 收藏
- 关注作者
评论(0)