Skip to content

Instantly share code, notes, and snippets.

@wangyiyang
Last active June 26, 2023 11:17
Show Gist options
  • Save wangyiyang/15660e27b655ce5907b7854226ad32c8 to your computer and use it in GitHub Desktop.
Save wangyiyang/15660e27b655ce5907b7854226ad32c8 to your computer and use it in GitHub Desktop.
Golang 开发规范

Golang 开发规范

编程规约

命名风格

  1. 【强制】 代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。

    反例:_name / __name / $name / name_ / name$ / name__

  2. 【强制】 代码中的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。

    说明: 正确的英文拼写和语法可以让阅读者易于理解,避免歧义。注意,纯拼音命名方式更要避免采用。

    正例: renminbi / alibaba / taobao / youku / hangzhou 等国际通用的名称,可视同英文。

    反例: DaZhePromotion [打折] / getPingfenByName() [评分] / int 某变量 = 3

  3. 【强制】 公用的变量、类型、接口、结构、函数以及结构体的成员变量等命名使用 UpperCamelCase 风格,但以下情形例外:DO 等。

    正例: GolangStruct / UserDO / XmlService / TcpUdpDeal / TaPromotion

    反例: Golangstruct / UserDo / XMLService / TCPUDPDeal / TAPromotion

  4. 【强制】 私有的变量、类型、接口、结构、函数以及参数名、局部变量都统一使用 lowerCamelCase 风格,必须遵从驼峰形式。

    正例: localValue / getHttpMessage() / inputUserId

  5. 【强制】 常量命名命名使用 UpperCamelCase 风格,并使用 const 声明,力求语义表达完整清楚,不要嫌名长。

    正例: const StatusOK = 200

  6. 抽象结构命名使用 Abstract 或 Base 开头; 异常类命名使用 Err 结尾; 测试类命名以 Test 开头,以它要测试的函数的名称结尾。 **正例:**ParamsErr := errors.New(“params err”)

  7. 接口命名规范一般使用 er 结尾: 单个函数的接口名以“er”作为后缀,接口的实现则去掉 er; 两个函数的接口名综合两个函数名,以 er 作为后缀,接口的实现则去掉 er ; 三个以上函数的接口,抽象这个接口的功能,类似于结构体命名。 正例:Writer / WriteReader / Ioer

  8. 数据和切片类型命名以 Arr 结尾,map 类型以 Map 结尾。相同功用的结构体可以根据功能采用相同的结尾,【强制】Api 请求以 Req 结尾,相应以 Res 结尾,数据结构体以 xxxModel 结尾,xxx即为数据表名。 正例:var userArr [3]string / type LoginReq struct{} / type UserDO struct{}

  9. 返回结果主要为布尔类型的函数,函数名可以 is、has 等开头

  10. 【强制】 工程名统一使用小写,单词之间使用 - 分割。包目录名一律使用小写,尽量采用一个单词命名,单词间不用符号分割,统一使用单数形式,但是结构体名如果有复数含义,结构体名可以使用复数形式。包目录下的包名( package namepackage ),如非 main 函数,和包目录名保持一直且单词间用 _ 分隔,测试文件以 _test 结尾。

    正例: db-utils / package db_utils / package db_utils_test 反例: services

  11. 【强制】 杜绝完全不规范的缩写,避免望文不知义,五个字母及以下单词不可缩写。

  12. 为了达到代码自解释的目标,任何自定义编程元素在命名时,使用尽量完整的单词组合来表达其意。

    反例: var a int 的随意命名方式。

  13. 在常量与变量的命名时,表示类型的名词放在词尾,以提升辨识度。如规范【8】所示。

    正例: startTime / startDate **反例:**startedAt / startDt

  14. 如果模块、接口、类、方法使用了设计模式,在命名时需体现出具体模式。 **说明:**将设计模式体现在名字中,有利于阅读者快速理解架构设计理念。

  15. 【参考】 各层命名规约:

    1. Service/DAO 层方法命名规约
      1. 获取单个对象的方法用 get 做前缀。
      2. 获取多个对象的方法用 arr 做前缀,复数形式结尾如:arrObjects。
      3. 获取统计值的方法用 count 做前缀。
      4. 插入的方法用 save/insert 做前缀。
      5. 删除的方法用 remove/delete 做前缀。
      6. 修改的方法用 update 做前缀。
    2. 领域模型命名规约
      1. 数据对象:xxxModel,xxx 即为数据表名。
      2. 数据请求对象:xxxReq,xxx 为业务领域相关的名称。
      3. 数据相应对象:xxxRes,xxx 一般为网页名称。

常量定义

  1. 【强制】 不允许任何魔法值(即未经预先定义的常量)直接出现在代码中。 反例:

    key :=Id#taobao_+ tradeId
    cache.put(key, value)
    // 缓存 get 时,由于在代码复制时,漏掉下划线,导致缓存击穿而出现问题
  2. 【推荐】 不要使用一个常量集维护所有常量,要按常量功能进行归类,分开维护并做好归类的注释。

  3. 【推荐】 常量的复用层次有五层:跨应用共享常量、应用内共享常量、子工程内共享常量、包内共享常量、类内共享常量。

    1. 跨应用共享常量:放置在二方库 constant 目录下
    2. 应用内共享常量:放置在一方库中,通常是子模块中的 constant 目录下。
    3. 子工程内部共享常量:即在当前子工程的 constant 目录下。
    4. 包内共享常量:即在当前包下单独的 constant 文件中。
    5. 文件内共享常量:直接在文件内部 const 定义。
  4. 【推荐】 如果变量值仅在一个固定范围内变化用 const 类型来常量化。 正例: const Ok = 1 / const No = 0

代码格式

  1. 【强制】 如果是大括号内为空,则简洁地写成{}即可,大括号中间无需换行和空格;如果是非空代码块则:

    1. 左大括号前不换行。
    2. 左大括号后换行。
    3. 右大括号前换行。
    4. 右大括号后还有 else 等代码则不换行;表示终止的右大括号后必须换行。
  2. 【强制】 左小括号和字符之间不出现空格;同样,右小括号和字符之间也不出现空格;而左大括号前需要空格。 反例: if (空格 a == b 空格)

  3. 【强制】 if / for / switch 等保留字与括号之间都必须加空格。

  4. 【强制】 任何二目、三目运算符的左右两边都需要加一个空格。 说明: 运算符包括赋值运算符=、逻辑运算符&&、加减乘除符号等。

  5. 【强制】 采用 tab 字符缩进,宽度设置为 4 个空格。

  6. 注释应该是完整的句子,注释应以所描述事物的名称开头,并以句点结尾。注释的双斜线与注释内容之间有且仅有一个空格。 正例: 中文注释可以不加结尾的句号

    // Encode writes the JSON encoding of req to w.
    func Encode(w io.Writer, req *Request) { 
        …
    }
  7. 【强制】 在进行类型强制转换时,右括号与强制转换值之间不需要任何空格隔开。

  8. **【强制】**单行字符数限制不超过 120 个,超出需要换行,换行时遵循如下原则:

    1. 第二行相对第一行缩进一个 tab,从第三行开始,不再继续缩进。
    2. 运算符与上文一起作为结尾。
    3. 方法调用的点符号与上文一起作为结尾。
    4. 方法调用中的多个参数需要换行时,在逗号后进行。
    5. 在括号前不要换行。
  9. **【强制】**函数参数在定义和传入时,多个参数逗号后边必须加空格。

  10. 【推荐】 单个方法的总行数不超过 80 行。

    说明: 除注释之外的方法签名、左右大括号、方法内代码、空行、回车及任何不可见字符的总行数不 超过 80 行。 正例:代码逻辑分清红花和绿叶,个性和共性,绿叶逻辑单独出来成为额外方法,使主干代码更加清晰;共性逻辑抽取成为共性方法,便于复用和维护。

  11. 【推荐】不同逻辑、不同语义、不同业务的代码之间插入一个空行分隔开来以提升可读性。 说明:任何情形,没有必要插入多个空行进行隔开。

代码工具

有助于改善 Go 代码的工具概述 https://github.com/golang/go/wiki/CodeTools/_edit

数组处理

  1. 【强制】 数据是具有固定长度且拥有零个或者多个相同数据类型元素的序列。新数组的元素初始值为元素类型的零值,如非需要默认默认值,则应该做好默认值的判断和处理。

  2. 使用数组指针进行参数传递,可以在接受函数内部直接修改原始数组,但不能改变原始数组的长度和元素类型。 反例:

    func zero(ptr *[32]byte) {
    		*ptr = [16]byte{}
    }
  3. 【强制】 注意检测 slice 类型的容量和长度,避免超过被引用对象的边界而导致宕机。

  4. 检测 slice 类型是否为空,应使用 len(s) == 0,而不是 s == nil。

  5. 【推荐】如果只想单纯的创建一个 slice,可以使用 make 函数创建一个指定元素类型、长度和容量的 slice。 正例:make([]T, len, cap)

  6. 【推荐】对于以已经初始化的数组作为底层数组的 slice,应尽量重(chong)用底层数组。

字典处理

  1. **【推荐】**使用 map 实现集合的功能。
  2. **【强制】**通过下标的方式访问 map 中的元素总是会有值,如果健不在 map 中,会得到返回值的零值,有些情况下理应判断键是否在 map 中。 正例:age, ok := ages[“bob”]

结构体处理

  1. 【强制】 禁止使用结构体匿名变量

并发处理

lamdba 架构应避免使用线程以及协程

  1. 【强制】获取单例对象需要保证线程安全,其中的方法也要保证线程安全。 **说明:**资源驱动类、工具类、单例工厂类都需要注意。
  2. 【强制】创建线程或线程池以及协程或者协程池时请指定有意义的线协程名称,方便出错时回溯。
  3. 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程(协程操作待定)。 说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

控制语句

  1. 【强制】在一个 switch 块内,每个 case 无需声明 break 来终止,如果想顺序执行使用 fallthrough ;在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使它什么代码也没有。

  2. 【强制】在高并发场景中,避免使用”等于”判断作为中断或退出的条件。 说明:如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件来代替。 反例:判断剩余奖品数量等于 0 时,终止发放奖品,但因为并发处理错误导致奖品数量瞬间变成了负数, 这样的话,活动无法终止。

  3. **【推荐】**表达异常的分支时,少用 if-else 方式,这种方式可以改写成:

    if condition {
    	…
    	return obj;
    }
    // 接着写 else 的业务逻辑代码;

    **说明:**如果非使用 if()…else if()…else…方式表达逻辑,避免后续代码维护困难,【强制】请勿超过 3 层。 **正例:**超过 3 层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现,其中卫语句即代码逻辑先考虑失败、异常、中断、退出等直接返回的情况,以方法多个出口的方式,解决代码中判断分支嵌套的问题,这是逆向思维的体现。

  4. **【推荐】**除常用方法(如 getXxx/isXxx)等外,不要在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。 说明:很多 if 语句内的逻辑表达式相当复杂,与、或、取反混合运算,甚至各种方法纵深调用,理解成本非常高。如果赋值一个非常好理解的布尔变量名字,则是件令人爽心悦目的事情。

  5. **【推荐】**不要在其它表达式(尤其是条件表达式)中,插入赋值语句。 说明:赋值点类似于人体的穴位,对于代码的理解至关重要,所以赋值语句需要清晰地单独成为一行。 反例:return (sync = fair) ? new FairSync() : new NonfairSync();

  6. **【推荐】**循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、获取数据库连接,进行不必要的 try-catch 操作(这个 try-catch 是否可以移至循环体外,golang 没有原生的 try-catch)。

  7. **【推荐】**避免采用取反逻辑运算符。

    说明:取反逻辑不利于快速理解,并且取反逻辑写法必然存在对应的正向逻辑写法。

  8. **【推荐】**接口入参保护,这种场景常见的是用作批量操作的接口。

  9. **【参考】**下列情形,需要进行参数校验:

    1. 调用频次低的方法。
    2. 执行时间开销很大的方法。此情形中,参数校验时间几乎可以忽略不计,但如果因为参数错误导致中间执行回退,或者错误,那得不偿失。
    3. 需要极高稳定性和可用性的方法。
    4. 对外提供的开放接口,不管是 RPC/API/HTTP 接口。
    5. 敏感权限入口。
  10. 【参考】下列情形,不需要进行参数校验:

    1. 极有可能被循环调用的方法。但在方法说明里必须注明外部参数检查要求。
    2. 底层调用频度比较高的方法。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底层才会暴露问题。一般 DAO 层与 Service 层都在同一个应用中,部署在同一台服务器中,所以 DAO 的参数校验,可以省略。

注释规约

  1. 【强制】每个软件包都应有一个软件包注释。它应紧接在包文件中的 package 语句之前(它只需要出现在一个文件中)。它应该以一个以“ Package packagename ”开头的句子开头,并给出该软件包功能的简要概述。 正例:

    // Package superman implements methods for saving the world.
    //
    // Experience has shown that a small number of procedures can prove
    // helpful when attempting to save the world.
    package superman
  2. 每个常量( const )、变量( var )、和函数( func )都应该写明注释。注释的首个单词应该和注释的内容保持一直,包括大小写。 正例:

    // enterOrbit causes Superman to fly into low Earth orbit, a position
    // that presents several possibilities for planet salvation.
    func enterOrbit() os.Error {
    …
    }
  3. 在注释中缩进的所有文本,godoc都将呈现为预格式化的块。这有助于代码示例。 说明:

    // fight can be used on any enemy and returns whether Superman won.
    //
    // Examples:
    //
    //    fight(“a random potato”)
    //    fight(LexLuthor{})
    //
    func fight(enemy interface{}) bool {
    …
    }
  4. 【推荐】代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改。 说明:代码与注释更新不同步,就像路网与导航软件更新不同步一样,如果导航软件严重滞后,就失去了导航的意义。

  5. 【参考】谨慎注释掉代码。在上方详细说明,而不是简单地注释掉。如果无用,则删除。 说明:代码被注释掉有两种可能性:1)后续会恢复此段代码逻辑。2)永久不用。前者如果没有备注信息,难以知晓注释动机。后者建议直接删掉(代码仓库已然保存了历史代码)。

  6. 【参考】对于注释的要求:第一、能够准确反映设计思想和代码逻辑;第二、能够描述业务含义,使别的程序员能够迅速了解到代码背后的信息。完全没有注释的大段代码对于阅读者形同天书,注释是给自己看的,即使隔很长时间,也能清晰理解当时的思路;注释也是给继任者看的,使其能够快速接替自己的工作。

  7. 【参考】好的命名、代码结构是自解释的,注释力求精简准确、表达到位。避免出现注释的一个极端:过多过滥的注释,代码的逻辑一旦修改,修改注释是相当大的负担。 反例:

    // put elephant into fridge
    put(elephant, fridge)

    方法名 put,加上两个有意义的变量名 elephant 和 fridge,已经说明了这是在干什么,语义清晰的代码不需要额外的注释。

  8. 【参考】特殊注释标记,请注明标记人与标记时间。注意及时处理这些标记,通过标记扫描,经常清理此类标记。线上故障有时候就是来源于这些标记处的代码。 1) 待办事宜(TODO):(标记人,标记时间,[预计处理时间]) 表示需要实现,但目前还未实现的功能。 2) 错误,不能工作(FIXME):(标记人,标记时间,[预计处理时间]) 在注释中用 FIXME 标记某代码是错误的,而且不能工作,需要及时纠正的情况。

杂项

  1. 【强制】在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。 正例:

    // 预编译当前正则表达式
    re, _ := regexp.Compile("^hel+o")
    // 是否匹配指定字符串
    isMatch := re.MatchString(“hello world”)
  2. 【推荐】不要在视图模板中加入任何复杂的逻辑。 说明:根据 MVC 理论,视图的职责是展示,不要抢模型和控制器的活。

  3. 【推荐】任何数据结构的构造或初始化,都应指定大小,避免数据结构无限增长吃光内存。

  4. 【推荐】及时清理不再使用的代码段或配置信息。 说明:对于垃圾代码或过时配置,坚决清理干净,避免程序过度臃肿,代码冗余。 正例:对于暂时被注释掉,后续可能恢复使用的代码片断,在注释代码上方,统一规定使用三个斜杠(///)来说明注释掉代码的理由。

异常日志

  1. 【强制】使用控制流机制应对错误,通过从函数返回错误作为附加返回值来指示错误,如果函数有多个返回值,习惯上将错误值作为最后一个结果返回。如果错误只有一种情况,结果通常设置为布尔类型。nil 值表示没有错误。 正例: res, err := somepkgAction()

  2. 【强制】对于总是成功返回的函数,不必返回错误。

  3. 【强制】一般函数只要符合前置条件即可成功返回,对于明显的错误调用应该避免。

  4. 【强制】错误 type 以 Error 结尾,错误变量以 Errerr 开头。 正例:

    type ParseError struct {}
    var ErrBadAction = errors.New(“somepkg: a bad action was performed”)
  5. 【强制】错误信息的首字母小写且避免换行。

  6. 使用 fmt.Errorf 函数格式化一条错误信息并且返回一个新的错误信息。我们为原始的错误信息不断的添加额外的上下文信息来建立一个可读的错误描述。 说明:设计一个错误消息的时候应当慎重,确保每一条信息的描述都是有意义的,包含充足的相关信息,并且保持一致性。 正例:return fmt.Sprintf(“parse error on line %d, column %d”, p.Line, p.Col)

  7. 【推荐】使用结构、变量或者函数封装定义某种具有相同属性的错误信息。由此可以使用 switch 根据不同错误类型做不同处理。 正例:

    type errorConst string
    const ErrTooManyErrors errorConst =too many errors found.”
    func (e errorConst) Error() string {
    	return string(e)
    }
  8. 对于不固定或者比可预测的错误,在短暂的时间间隔后对操作进行重试。

  9. 【推荐】错误日志应由主程序在停止程序之前输出到标准错误流,库函数应当将错误传递给调用者。 正例:fmt.Fprintf(os.stderr, “failed to solve problem: %s\n”, err)

  10. 在一些错误情况下,只记录下错误信息然后程序继续运行,可以选择使用 log 包来增加日志的常用前缀。 正例:log.Printf(“failed to solve problem: %s\n”, err)

  11. 【强制】进行错误检查之后,检测到失败的情况往往在成功之前。如果检测到的失败导致函数返回,成功的逻辑一般不会放在 else 块中而是在外层的作用域中。 说明:函数会有一种通常的形式,就是在开头有一连串的检查用来返回错误,之后跟着实际的函数体一直到最后。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment