uber go code 规范(规范)

前言

从接触 Golang 到现在, 感觉到的很深的一点是, go 的代码无论是大佬还是菜鸟写出的代码, 都有着大体统一的 格式/流程, 这也是 Go 被开发者喜爱的一个原因, 但是还有一些, 比如变量的命名方式等, 可以称之为 风格 的东西, 却不尽相同, 我在开发中, 其实也希望有一个相对权威的指导意见, 后来就找到了 uber 团队出品的开发规范.
uber 是众多公司中, 比较早使用 go 语言的了, 其本身也开源了一些优质的模块, 有机会的话希望也能向大家展示一下, 而在 uber 内部开发中, 经过持续的迭代, 开源了自己的代码规范, 这里给大家解读一下
需要特别指出的是, 下面的内容并不是一定需要遵守, 这里你可以选择自己认为正确的可行的规范.
团队内使用统一的风格, 可以提高代码的可读性
本篇记录规范部分
推荐大家使用VsCode的官方golang插件

避免过长的行

避免需要读者水平滚动或者过度转动头部的代码行.
建议将行长度限制为99字符以内, 注意只是建议.

将相似的声明放置在一组

声明指的是 导入/常量定义/变量定义等
将其分组可以提高可读性, 分组依据是作用或者意思相近
错误示例

import "a"
import "b"


const a = 1
const b = 2

var a = 1
var b = 2

type Area float64
type Volume float64

正确示例

import (
  "a"
  "b"
)

const (
  a = 1
  b = 2
)

var (
  a = 1
  b = 2
)

type (
  Area float64
  Volume float64
)

需要注意不要强行放在一组, 如果是不相干的声明要分开, 不要强行挤在一起
错误示例

type Operation int

// 强行挤到一起
const (
  Add Operation = iota + 1
  Subtract
  Multiply
  EnvVar = "MY_ENV"
)

正确示例

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

const EnvVar = "MY_ENV"

函数的内部也可以进行分组

func f() string {
  var (
    red   = color.New(0xff0000)
    green = color.New(0x00ff00)
    blue  = color.New(0x0000ff)
  )

  ...
}

对于函数内部的变量分组, 如果是语义不达到分组标准但是与其他变量相近, 也可以放到一起

func (c *client) request() {
  var (
    caller  = c.name
    format  = "json"
    timeout = 5*time.Second
    err error
  )
  // ...
}

import 分组

import也需要分组, 中间以空行分隔, 可分为三组

  • 标准库
  • 引入的第三方库
  • 本项目的其他代码包
    使用VsCode也可以自己自定义分组, 注意, 默认时VsCode只能分出标准和非标准, 也就是说第三方库和本项目其他代码挤在一起, 想要开启区分, 需要添加VsCode配置
// 项目文件夹/.vscode/settings.json
{
    "gopls": {
        "formatting.local": "server-api"  // 本项目包名 
    }
}

包名

包名规则符合以下几点

函数名

使用 MixedCaps 命名法(驼峰), 不要包含下划线.
有一个例外, 测试代码可以包含下划线, 来区分测试函数的多个用例, 例如: TestMyFunction_WhatIsBeingTested

导入别名

如果包名称与导入的路径的最后一个名字不匹配则必须使用导入别名, VsCode会自己处理

import (
  "net/http"

  client "example.com/client-go"
  trace "example.com/trace/v2"
)

其他情况下, 除非导入有冲突, 不然不要使用别名
错误示例

import (
  "fmt"
  "os"

  nettrace "golang.net/x/trace"
)

正确示例

import (
  "fmt"
  "os"
  "runtime/trace"

  nettrace "golang.net/x/trace"
)

函数的分组和顺序

  • 函数应该按照粗略的调用顺序排序
  • 同一个文件中的函数应该按照调用者进行分组
    公开的函数应该先出现在文件中, 只放在 struct/const/var 的后面
    常用的场景是定义一个结构体并对外暴露方法, 此时结构体的初始化应该放置在方法前
    普通的工具函数放置在文件末尾
    错误示例
func (s *something) Cost() {
  return calcCost(s.weights)
}

type something struct{ ... }

func calcCost(n []int) int {...}

func (s *something) Stop() {...}

func newSomething() *something {
    return &something{}
}

正确示例

type something struct{ ... }

func newSomething() *something {
    return &something{}
}

func (s *something) Cost() {
  return calcCost(s.weights)
}

func (s *something) Stop() {...}

func calcCost(n []int) int {...}

减少嵌套

函数应该优先处理错误和特殊情况, 减少错误时代码嵌套, 这样有利于代码可读性
错误示例

for _, v := range data {
  if v.F1 == 1 {
    v = process(v)
    if err := v.Call(); err == nil {
      v.Send()
    } else {
      return err
    }
  } else {
    log.Printf("Invalid v: %v", v)
  }
}

正确示例

for _, v := range data {
  if v.F1 != 1 {
    log.Printf("Invalid v: %v", v)
    continue
  }

  v = process(v)
  if err := v.Call(); err != nil {
    return err
  }
  v.Send()
}

不必要的else

如果在 if 的两个分支中都设置了变量, 则可以将其替换为单个 if
错误示例

var a int
if b {
  a = 100
} else {
  a = 10
}

正确示例

a := 10
if b {
  a = 100
}

顶层的变量声明

在文件顶层, 使用标准的 var 时, 可以不用指定类型, 除非他和表达式返回的类型不一样
错误示例

var _s string = F()  // 我知道他是string

func F() string { return "A" }

正确示例

var _s = F()
// 由于 F 已经明确了返回一个字符串类型,因此我们没有必要显式指定_s 的类型
// 还是那种类型

func F() string { return "A" }

私有的顶层变量和常量使用_作为前缀

顶级变量和常量作用域通常在包内, 并且名称都是通用的, 很容器在其他文件中被意外使用
错误示例

// foo.go

const (
  defaultPort = 8080
  defaultUser = "user"
)

// bar.go

func Bar() {
  defaultPort := 9090
  ...
  fmt.Println("Default port", defaultPort)  // 9090
}

正确示例

// foo.go

const (
  _defaultPort = 8080
  _defaultUser = "user"
)

结构体的嵌入

对于结构体嵌套, 应该将嵌套的结构体放置在最上方, 并且有空行分隔
错误示例

type Client struct {
  version int
  http.Client
}

正确示例

type Client struct {
  http.Client

  version int
}

需要注意的是, 尽量不要使用匿名的方式来嵌套结构体, 嵌入不应该:

  • 纯粹是为了美观或方便。
  • 使外部类型更难构造或使用。
  • 影响外部类型的零值。如果外部类型有一个有用的零值,则在嵌入内部类型之后应该仍然有一个有用的零值。
  • 作为嵌入内部类型的副作用,从外部类型公开不相关的函数或字段。
  • 公开未导出的类型。
  • 影响外部类型的复制形式。
  • 更改外部类型的 API 或类型语义。
  • 嵌入内部类型的非规范形式。
  • 公开外部类型的实现详细信息。
  • 允许用户观察或控制类型内部。
  • 通过包装的方式改变内部函数的一般行为,这种包装方式会给用户带来一些意料之外情况。

本地变量声明

将变量明确的设置为某个值, 应该使用:=
错误示例

var s = "foo"

正确示例

s := "foo"

而在有时候, 使用 var 会更加清晰, 例如 声明一个空切片
错误示例

func f(list []int) {
  filtered := []int{}
  for _, v := range list {
    if v > 10 {
      filtered = append(filtered, v)
    }
  }
}

正确示例

func f(list []int) {
  var filtered []int
  for _, v := range list {
    if v > 10 {
      filtered = append(filtered, v)
    }
  }
}

切片为nil也是有效的

nil其实是一个有效的切片, 只是他的长度为0, 因此

如果你需要返回一个长度为0的切片, 建议可以直接返回 nil
错误示例

if x == "" {
  return []int{}
}

正确示例

if x == "" {
  return nil
}

如果你需要检查切片是否为空, 请使用len()
错误示例

func isEmpty(s []string) bool {
  return s == nil
}

正确示例

func isEmpty(s []string) bool {
  return len(s) == 0
}

var 创建的切片可以直接使用, 不需要make
错误示例

nums := []int{}
// or, nums := make([]int)

if add1 {
  nums = append(nums, 1)
}

if add2 {
  nums = append(nums, 2)
}

正确示例

var nums []int

if add1 {
  nums = append(nums, 1)
}

if add2 {
  nums = append(nums, 2)
}

虽然nil是一个有效的切片, 但是并不完全等于一个长度为0的切片, 比如在序列化时会有不同的处理方式.

缩小变量的作用域

尽量缩小变量的作用域
错误示例

err := os.WriteFile(name, data, 0644)
if err != nil {
 return err
}

正确示例

if err := os.WriteFile(name, data, 0644); err != nil {
 return err
}

当然这个规则优先级要比 优先处理错误 的规则低
错误示例

if data, err := os.ReadFile(name); err == nil {
  err = cfg.Decode(data)
  if err != nil {
    return err
  }

  fmt.Println(cfg)
  return nil
} else {
  return err
}

正确示例

data, err := os.ReadFile(name)
if err != nil {
   return err
}

if err := cfg.Decode(data); err != nil {
  return err
}

fmt.Println(cfg)
return nil

避免参数语义不明确

参数的命名要保证可读性, 如果参数名无法很好的表达语义, 可以添加C样式的注释
错误示例

func printInfo(name string, isLocal, done bool){
  ....
}

printInfo("foo", true, true)

正确示例

type Region int  // 多种状态

const (
  UnknownRegion Region = iota
  Local
)

type Status int  // 多种状态

const (
  StatusReady Status= iota + 1
  StatusDone
)

func printInfo(name string, region Region, status Status)

使用原始字符串字面值

golang可以使用 " ` " 来表示原始字符串
错误示例

wantError := "unknown name:\"test\""

正确示例

wantError := `unknown error:"test"`

结构体初始化

使用字段名而不是顺序来初始化

保证可读性, 同时也保证了结构体字段变化时兼容
错误示例

k := User{"John", "Doe", true}

正确示例

k := User{
    FirstName: "John",
    LastName: "Doe",
    Admin: true,
}

而当结构体只有2个或以下字段也可以使用顺序的方式

省略结构中的零值

如果结构体中的某些字段现在是零值, 那么不需要显式的赋值, 可以直接忽略
错误示例

user := User{
  FirstName: "John",
  LastName: "Doe",
  MiddleName: "",
  Admin: false,
}

正确示例

user := User{
  FirstName: "John",
  LastName: "Doe",
}

对零值结构直接使用var

如果在声明中省略了结构的所有字段,请使用 var 声明结构
错误示例

user := User{}

正确示例

var user User

初始化结构体引用

请使用&T{}代替new(T),以使其与结构体初始化一致.
错误示例

sval := T{Name: "foo"}

// inconsistent
sptr := new(T)
sptr.Name = "bar"

正确示例

sval := T{Name: "foo"}

sptr := &T{Name: "bar"}

初始化maps

对于空 map 请使用 make() 初始化, 并且 map 是通过编程方式填充的。 这使得 map 初始化在表现上不同于声明,并且它还可以方便地在 make 后添加大小提示
错误示例

var (
  // m1 读写安全;
  // m2 在写入时会 panic
  m1 = map[T1]T2{}
  m2 map[T1]T2
)

正确示例

var (
  // m1 读写安全;
  // m2 在写入时会 panic
  m1 = make(map[T1]T2)
  m2 map[T1]T2
)

字符串format模板

如果你在函数外声明Printf-style 函数的格式字符串,请将其设置为const常量
这有助于go vet对格式字符串执行静态分析
正确示例

msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)

错误示例

const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章