Go规范:Google 风格决策 · 命名

目录

命名规则

包名称

接收者

常量

缩写词

Get方法Getters

变量名

单字母变量名

重复

包名 vs 可导出符号名

变量名 vs 类型

外部上下文 vs 本地名称


因为发现自己的变量命名可读性很差,所以整理了一下规范。

主要参考 Google Style Guides 和 Go Style ,并且结合了其他的资料和个人理解。

命名规则

  • 命名必须以字母(a-z, A-Z)或下划线(_)开头,后面可以跟字母、数字(0-9)或下划线。
  • 不能使用特殊字符,如@$%等。
  • Go是区分大小写的,因此Manman是两个不同的标识符。
  • 大写字母开头的命名是导出的,意味着它们可以被外部包访问。小写字母开头的命名是未导出的,只能在定义它们的包内部访问。

包名称

  1. Go 包名称应该简短并且只包含小写字母。由多个单词组成的包名称应全部小写。例如,包 tabwriter 不应该命名为 tabWriterTabWriter 或 tab_writer
  2. 避免选择可能被常用局部变量遮蔽覆盖的包名称。例如,usercount 是比 count 更好的包名,因为 count 是常用变量名。
  3. Go 包名称不应该有下划线。如果你需要导入名称中确实有一个包(通常来自生成的或第三方代码),则必须在导入时将其重命名为适合在 Go 代码中使用的名称。一个例外是仅由生成的代码导入的包名称可能包含下划线。具体例子包括:
    • 对外部测试包使用 _test 后缀,例如 mypackage_test
    • 使用 _test 后缀作为 包级文档示例,例如 example_mypackage_test
  4. 避免使用无意义的包名称,例如 utilutilitycommonhelper 等。查看更多关于所谓的“实用程序包”。
  5. 当导入的包被重命名时(例如 import foob "path/to/foo_go_proto"),包的本地名称必须符合上述规则,因为本地名称决定了包中的符号在文件中的引用方式。如果给定的导入在多个文件中重命名,特别是在相同或附近的包中,则应尽可能使用相同的本地名称以保持一致性

接收者

  • 短(通常是一两个字母的长度)
  • 类型本身的缩写
  • 始终如一地应用于该类型的每个接收者
长名称 更好命名
func (tray Tray) func (t Tray)
func (info *ResearchInfo) func (ri *ResearchInfo)
func (this *ReportWriter) func (w *ReportWriter)
func (self *Scanner) func (s *Scanner)

常量

常量名称必须像 Go 中的所有其他名称一样使用 混合大写字母MixedCaps。(导出常量以大写字母开头,而未导出的常量以小写字母开头)即使打破了其他语言的约定,这也是适用的。常量名称不应是其值的派生词,而应该解释值所表示的含义。

// Good:
const MaxPacketSize = 512

const (
    ExecuteBit = 1 << iota
    WriteBit
    ReadBit
)

不要使用非混合大写常量名称或带有 K 前缀的常量。

// Bad:
const MAX_PACKET_SIZE = 512
const kMaxBufferSize = 1024
const KMaxUsersPergroup = 500

根据它们的角色而不是它们的值来命名常量。如果一个常量除了它的值之外没有其他作用,那么就没有必要将它定义为一个常量。

// Bad:
const Twelve = 12

const (
    UserNameColumn = "username"
    GroupColumn    = "group"
)

缩写词

名称中的首字母缩略词或单独的首字母缩略词(例如,“URL”和“NATO”)应该具有相同的大小写

URL 应显示为 URL 或 url(如 urlPony 或 URLPony),绝不能显示为 Url

ID 为 identifier 缩写:应显示为 appID 而不是 appId

  • 在具有多个首字母缩写的名称中(例如 XMLAPI 因为它包含 XML 和 API),给定首字母缩写中的每个字母都应该具有相同的大小写,但名称中的每个首字母缩写不需要具有相同的大小写。
  • 在带有包含小写字母的首字母缩写的名称中(例如DDoSiOSgRPC),首字母缩写应该像在标准中一样出现,除非你需要为了满足 导出 而更改第一个字母。在这些情况下,整个缩写词应该是相同的情况(例如 ddosIOSGRPC)。
缩写词 范围 正确 错误
XML API Exported XMLAPI XmlApiXMLApiXmlAPIXMLapi
XML API Unexported xmlAPI xmlapixmlApi
iOS Exported IOS IosIoS
iOS Unexported iOS ios
gRPC Exported GRPC Grpc
gRPC Unexported gRPC grpc
DDoS Exported DDoS DDOSDdos
DDoS Unexported ddos dDoSdDOS

Get方法Getters

函数和方法名称不应使用 Get 或 get 前缀,除非底层概念使用单词 get(例如 HTTP GET)。此时,更应该直接以名词开头的名称,例如使用 Counts 而不是 GetCounts

type User struct {
    name string
}

// 直接访问导出字段(无需 Getter)
func (u *User) Name() string { return u.name }

// 冗余的 Getter 模式
func (u *User) GetName() string { return u.name }

如果该函数涉及执行复杂的计算或执行远程调用,则可以使用Compute 或 Fetch等不同的词代替Get,以使读者清楚函数调用可能需要时间,并有可能会阻塞或失败。

// 明确表示远程调用
func (c *Client) FetchUserData(id string) (*Data, error) { ... }

// 明确表示计算密集型操作
func CalculateHash(data []byte) (string, error) { ... }

// 模糊的命名,无法体现操作性质
func GetUserData(id string) (*Data, error) { ... } 

变量名

一般的经验法则是,名称的长度应与其范围的大小成正比,并与其在该范围内使用的次数成反比。在文件范围内创建的变量可能需要多个单词,而单个内部块作用域内的变量可能是单个单词甚至只是一两个字符,以保持代码清晰并避免无关信息。

这是一条粗略的基础原则。这些数字准则不是严格的规则。要根据上下文、清晰 和[简洁](02 Guide | Google Style Guides简洁)来进行判断。

  • 小范围是执行一两个小操作的范围,比如 1-7 行。
  • 中等范围是一些小的或一个大的操作,比如 8-15 行。
  • 大范围是一个或几个大操作,比如 15-25 行。
  • 非常大的范围是指超过一页(例如,超过 25 行)的任何内容。

在小范围内可能非常清楚的名称(例如,c 表示计数器)在较大范围内可能不够用,并且需要澄清以提示进一步了解其在代码中的用途。一个作用域中有很多变量,或者表示相似值或概念的变量,那就可能需要比作用域建议的采用更长的变量名称。

概念的特殊性也有助于保持变量名称的简洁。例如,假设只有一个数据库在使用,像db这样的短变量名通常可能保留给非常小的范围,即使范围非常大,也可能保持完全清晰。在这种情况下,根据范围的大小,单个词database可能是可接受的,但不是必需的,因为db是该词的一种非常常见的缩写,几乎没有其他解释。

局部变量的名称应该反映它包含的内容以及它在当前上下文中的使用方式,而不是值的来源。例如,通常情况下最佳局部变量名称与结构或协议缓冲区字段名称不同。

一般来说:

  • 像 count 或 options 这样的单字名称是一个很好的起点。

  • 可以添加其他词来消除相似名称的歧义,例如 userCount 和 projectCount

  • 不要简单地省略字母来节省打字时间。例如,Sandbox 优于 Sbx,特别是对于导出的名称。

  • 大多数变量名可省略 类型和类似类型的词

    • 对于数字,userCount 是比 numUsers 或 usersInt 更好的名称。
    • 对于切片,users 是一个比 userSlice 更好的名字。
    • 如果范围内有两个版本的值,则包含类似类型的限定符是可以接受的,例如,你可能将输入存储在 ageString 中,并使用 age 作为解析值。
  • 省略上下文中清楚的单词。例如,在 UserCount 方法的实现中,名为 userCount 的局部变量可能是多余的, countusers 甚至 c 都具有可读性。

单字母变量名

单字母变量名是可以减少 重复 的有用工具,但也可能使代码变得不透明。将它们的使用限制在完整单词很明显以及它会重复出现以代替单字母变量的情况。

一般来说:

  • 对于方法接收者变量,最好使用一个字母或两个字母的名称。
  • 对常见类型使用熟悉的变量名通常很有帮助:
    • r 用于 io.Reader 或 *http.Request
    • w 用于 io.Writer 或 http.ResponseWriter
  • 单字母标识符作为整数循环变量是可接受的,特别是对于索引(例如,i)和坐标(例如,x 和 y)。
  • 当范围很短时,循环标识符使用缩写是可接受的,例如for _, n := range nodes { ... }

重复

一段 Go 源代码应该避免不必要的重复。一个常见的情形是重复名称,其中通常包含不必要的单词或重复其上下文或类型。如果相同或相似的代码段在很近的地方多次出现,代码本身也可能是不必要的重复。

重复命名可以有多种形式,包括:

包名 vs 可导出符号名

当命名导出的符号时,包的名称始终在包外可见,因此应减少或消除两者之间的冗余信息。如果一个包如果需要仅导出一种类型并且以包本身命名,则构造函数的规范名称是New(如果需要的话)。

实例: 重复的名称 -> 更好的名称

  • widget.NewWidget -> widget.New
  • widget.NewWidgetWithName -> widget.NewWithName
  • db.LoadFromDatabase -> db.Load
  • goatteleportutil.CountGoatsTeleported -> gtutil.CountGoatsTeleported or goatteleport.Count
  • myteampb.MyTeamMethodRequest -> mtpb.MyTeamMethodRequest or myteampb.MethodRequest

包名在外部使用时始终可见,因此导出符号名中无需重复包名隐含的信息

// 冗余:包名 `widget` 已隐含类型信息
widget.NewWidget → widget.New

// 冗余:包名 `db` 已关联数据库
db.LoadFromDatabase → db.Load

若导出类型名与包名无关,需保留类型名以避免歧义。

// 包 `graph` 导出类型 `Node` 和 `Edge`
func NewNode() *Node { ... } 
func NewEdge() *Edge { ... }  

若包仅导出一个核心类型且类型名与包名相同,构造函数应命名为 New

// 包 `widget` 导出类型 `Widget`
func New() *Widget { ... }

如果包导出多个类型,构造函数需明确类型名以区分。

// 包 `stream` 导出 `Reader` 和 `Writer`
func NewReader() *Reader { ... }  
func NewWriter() *Writer { ... }  

变量名 vs 类型

编译器总是知道变量的类型,并且在大多数情况下,阅读者也可以通过变量的使用方式清楚地知道变量是什么类型。只有当一个变量的值在同一范围内出现两次时,才有需要明确变量的类型。

重复的名称 更好的名称
var numUsers int var users int
var nameString string var name string
var primaryProject *Project var primary *Project

如果该值以多种形式出现,这可以通过额外的词(如rawparsed)或底层表示来澄清:

// Good:
limitStr := r.FormValue("limit")
limit, err := strconv.Atoi(limitStr)
// Good:
limitRaw := r.FormValue("limit")
limit, err := strconv.Atoi(limitRaw)
状态标识 含义 特点 典型场景
raw 原始数据,未经任何处理 不可信、未验证、可能存在安全风险 HTTP 参数、文件/网络原始字节、用户输入
parsed 从原始数据中提取的结构化信息 已初步处理,但可能未完全验证 类型转换、正则匹配、字符串分解
decoded 从编码格式转换回业务对象的数据 已结构化,可直接使用但需校验 JSON/XML 反序列化、Base64 解码

外部上下文 vs 本地名称

包含来自周围上下文信息的名称通常会产生额外的噪音,而没有任何好处。包名、方法名、类型名、函数名、导入路径,包含来自其上下文信息的名称。

// In package "ads/targeting/revenue/reporting"

// Bad:
type AdsTargetingRevenueReport struct{}

func (p *Project) ProjectName() string

// Good:
type Report struct{}

func (p *Project) Name() string
// In package "sqldb"

// Bad:
type DBConnection struct{}

// Good:
type Connection struct{}
// In package "ads/targeting"

// Bad:
func Process(in *pb.FooProto) *Report {
    adsTargetingID := in.GetAdsTargetingID()
}

// Good:
func Process(in *pb.FooProto) *Report {
    id := in.GetAdsTargetingID()
}

重复通常应该在符号使用者的上下文中进行评估,而不是孤立地进行评估。例如,下面的代码有很多名称,在某些情况下可能没问题,但在上下文中是多余的:

// Bad:
func (db *DB) UserCount() (userCount int, err error) {
    var userCountInt64 int64
    if dbLoadError := db.LoadFromDatabase("count(distinct users)", &userCountInt64); dbLoadError != nil {
        return 0, fmt.Errorf("failed to load user count: %s", dbLoadError)
    }
    userCount = int(userCountInt64)
    return userCount, nil
}

相反,在上下文和使用上信息是清楚的情况下,常常可以忽略:

// Good:
func (db *DB) UserCount() (int, error) {
    var count int64
    if err := db.Load("count(distinct users)", &count); err != nil {
        return 0, fmt.Errorf("failed to load user count: %s", err)
    }
    return int(count), nil
}

其他

  • 文件名应一律使用小写,不同单词之间用下划线分割,不用驼峰式,命名应尽可能地见名知意
  • bool 类型变量,名称应以 Has, Is, Can 或 Allow 开头

你可能感兴趣的:(go,golang,开发语言,后端,go,代码规范)