技术规范(2): 后端技术开发规范

Python 开发规范

优美胜于丑陋
明了胜于晦涩
简洁胜于复杂
复杂胜于凌乱
扁平胜于嵌套
间隔胜于紧凑
可读性很重要
即便假借特例的实用性之名,也不可违背这些规则

上面是 Python 之禅,很好地体现了 Python 语言所传达的编程理念。

PEP8 标准

PEP8 规范:
https://www.python.org/dev/peps/pep-0008/

标准:

  1. 缩进:每一级缩进使用4个空格。

  2. 续行应该与其包裹元素对齐,要么使用圆括号、方括号和花括号内的隐式行连接来垂直对齐,要么使用挂行缩进对齐3。当使用挂行缩进时,应该考虑到第一行不应该有参数,以及使用缩进以区分自己是续行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 与左括号对齐
    foo = long_function_name(var_one, var_two,
    var_three, var_four)

    # 用更多的缩进来与其他行区分
    # 挂行缩进应该再换一行
    def long_function_name(
    var_one, var_two, var_three,
    var_four):
    print(var_one)
  3. 在多行结构中的大括号/中括号/小括号的右括号可以与内容对齐单独起一行作为最后一行的第一个字符。

    1
    2
    3
    4
    5
    6
    7
    8
    my_list = [
    1, 2, 3,
    4, 5, 6,
    ]
    result = some_function_that_takes_arguments(
    'a', 'b', 'c',
    'd', 'e', 'f',
    )
  4. 所有行限制的最大字符数为79。

  5. 顶层函数和类的定义,前后用两个空行隔开。类里的方法定义用一个空行隔开。

  6. import导入通常在分开的行。

    1
    2
    3
    4
    推荐: import os
    import sys

    不推荐: import sys, os
  7. import顺序:
    标准库导入
    相关第三方库导入
    本地应用/库特定导入
    (在每类导入之间加入空行分割)

  8. __all__ , __author__ , __version__ 等这样的模块级“呆名“(也就是名字里有两个前缀下划线和两个后缀下划线),应该放在文档字符串的后面,以及除 from __future__ 之外的import表达式前面。Python要求将来在模块中的导入,必须出现在除文档字符串之外的其他代码之前。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    """This is the example module.

    This module does stuff.
    """

    from __future__ import barry_as_FLUFL

    __all__ = ['a', 'b', 'c']
    __version__ = '0.1'
    __author__ = 'Cardinal Biggles'

    import os
    import sys
  9. 避免使用无关的空格,避免在尾部添加空格。因为尾部的空格通常都看不见,会产生混乱。

  10. 总是在二元运算符两边加一个空格。

  11. 如果使用具有不同优先级的运算符,请考虑在具有最低优先级的运算符周围添加空格。

    1
    2
    3
    4
    5
    i = i + 1
    submitted += 1
    x = x*2 - 1
    hypot2 = x*x + y*y
    c = (a+b) * (a-b)
  12. 与代码相矛盾的注释比没有注释还糟,当代码更改时,优先更新对应的注释。

  13. 块注释通常适用于跟随它们的某些(或全部)代码,并缩进到与代码相同的级别。块注释的每一行开头使用一个#和一个空格(除非块注释内部缩进文本)。块注释内部的段落通过只有一个#的空行分隔。
  1. 要为所有的公共模块,函数,类以及方法编写文档说明。

    1
    2
    3
    4
    5
    6
    7
    8
    def complex(real=0.0, imag=0.0):
    """Form a complex number.

    Keyword arguments:
    real -- the real part (default 0.0)
    imag -- the imaginary part (default 0.0)
    """
    ...
  2. 命名规范

(1)模块应该用简短全小写的名字,如果为了提升可读性,下划线也是可以用的。Python包名也应该使用简短全小写的名字,但不建议用下划线。
(2)类名一般使用首字母大写的约定。
在接口被文档化并且主要被用于调用的情况下,可以使用函数的命名风格代替。
注意,对于内置的变量命名有一个单独的约定:大部分内置变量是单个单词(或者两个单词连接在一起),首字母大写的命名法只用于异常名或者内部的常量。
(3)因为异常一般都是类,所有类的命名方法在这里也适用。然而,你需要在异常名后面加上“Error”后缀。
(4)函数名应该小写,如果想提高可读性可以用下划线分隔。
(5)全局变量名和函数命名规则一样,值得注意的是通过 from M import * 导入的模块应该使用all机制去防止内部的接口对外暴露,或者使用在全局变量前加下划线的方式。
(6)如果函数的参数名和已有的关键词冲突,在最后加单一下划线比缩写或随意拼写更好。
(7)常量通常定义在模块级,通过下划线分隔的全大写字母命名。例如: MAX_OVERFLOWTOTAL

  1. 从Exception继承异常,而不是BaseException。直接继承BaseException的异常适用于几乎不用来捕捉的异常。
  2. 当捕获到异常时,如果可以的话写上具体的异常名,而不是只用一个except,如果只有一个except: 块将会捕获到SystemExitKeyboardInterrupt异常,这样会很难通过ctrl+C中断程序,而且会掩盖掉其他问题。如果你想捕获所有指示程序出错的异常,使用 except Exception
    正确的是:

    1
    2
    3
    4
    try:
    import platform_specific_module
    except ImportError:
    platform_specific_module = None
  3. 无论何时获取和释放资源,都应该通过单独的函数或方法调用上下文管理器,比如使用with 表达式来确保这个资源使用完后被清理干。

  4. 返回的语句保持一致。函数中的返回语句都应该返回一个表达式,或者都不返回。如果一个返回语句需要返回一个表达式,那么在没有值可以返回的情况下,需要用 return None 显式指明。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    def foo(x):
    if x >= 0:
    return math.sqrt(x)
    else:
    return None

    def bar(x):
    if x < 0:
    return None
    return math.sqrt(x)

工具

具体的可以使用Autopep8工具协助排版。

安装autopep8:

1
pip install autopep8

用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
autopep8 [参数] [Python文件]
核心参数:
-d, --diff print the diff for the fixed source
-i, --in-place make changes to files in place
--global-config filename
path to a global pep8 config file; if this file does
not exist then this is ignored (default:
~/.config/pep8)
--ignore-local-config
don't look for and apply local config files; if not
passed, defaults are updated with any config files in
the project's root directory
-r, --recursive run recursively over directories; must be used with
--in-place or --diff
-j n, --jobs n number of parallel jobs; match CPU count if value is
less than 1
-p n, --pep8-passes n
maximum number of additional pep8 passes (default:
infinite)
-a, --aggressive enable non-whitespace changes; multiple -a result in
more aggressive changes
--experimental enable experimental fixes
--exclude globs exclude file/directory names that match these comma-
separated globs
--list-fixes list codes for fixes; used by --ignore and --select
--ignore errors do not fix these errors/warnings (default:
E226,E24,W50,W690)
--select errors fix only these errors/warnings (e.g. E4,W)
--max-line-length n set maximum allowed line length (default: 79)
--line-range line line, --range line line
only fix errors found within this inclusive range of
line numbers (e.g. 1 99); line numbers are indexed at
1
--hang-closing hang-closing option passed to pycodestyle
--exit-code change to behavior of exit code. default behavior of
return value, 0 is no differences, 1 is error exit.
return 2 when add this option. 2 is exists
differences.

--in-place 会直接将结果保存到源文件中,如果不包含--in-place选项,则会将autopep8格式化以后的代码直接输出到控制台。

Go 开发规范

Go 语言规范

Go 代码审核规范官方地址:
https://github.com/golang/go/wiki/CodeReviewComments

  1. 所有代码在发布前均使用gofmt进行修正。
  2. 所有的注释都应该是一个完整的句子。句子应该以主语开头,句号结尾。
  3. 声明空的数组分片,避免分配内存空间。因为有些时候,可能你从没向这个数组分片里面append元素

    1
    2
    3
    4
    5
    // 推荐这样
    var t []string
    // 而不是这样
    t := []string{}
    `
  4. 大多数使用 Context 的函数都应该接受 Context 作为函数的第一个参数。
    比如这样:

    1
    func F(ctx context.Context, /* other arguments */) {}
  5. 不要将 Context 成员添加到某个 struct 类型中;而是将 ctx 参数添加到该类型的方法上。一个例外情况是当前方法签名必须与标准库或第三方库中的接口方法匹配。不要在函数签名中创建自定义 Context 类型或使用除了 Context 以外的接口。

  6. 为避免意外的别名,从另一个包复制 struct 时要小心。例如,bytes.Buffer 类型包含一个 []byte 的 slice,并且作为短字符串的优化,slice 可以引用一个短字节数组。如果复制一个 Buffer,副本中的 slice 可能会对原始数组进行别名操作,从而导致后续方法调用产生令人惊讶的效果。通常,如果 T 类型的方法与其指针类型 *T 相关联,请不要复制 T 类型的值。

  7. 不要使用包math/rand来生成密钥,即使是一次性密钥。在没有种子(seed)的情况下,生成器是完全可以被预测的。使用time.Nanoseconds()作为种子值,熵只有几位。请使用crypto/rand的 Reader 作为替代,如果你倾向于使用文本,请输出成十六进制或 base64 编码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import (
    "crypto/rand"
    // "encoding/base64"
    // "encoding/hex"
    "fmt"
    )

    func Key() string {
    buf := make([]byte, 16)
    _, err := rand.Read(buf)
    if err != nil {
    panic(err) // out of randomness, should never happen
    }
    return fmt.Sprintf("%x", buf)
    // or hex.EncodeToString(buf)
    // or base64.StdEncoding.EncodeToString(buf)
    }
  8. Go提供两种注释风格,C的块注释风格/**/,C++的行注释风格//。包注释必须出现在 package 声明的临近位置,无空行。所有的顶级导出的名称都应该有 doc 注释,重要的未导出类型或函数声明也应如此。每个public函数都应该有注释,注释句子应该以该函数名开头,如:

    1
    2
    3
    // Compile parses a regular expression and returns, if successful,
    // a Regexp that can be used to match against text.
    func Compile(str string) (*Regexp, error) {}
  9. 尽量不要使用panic处理错误。函数应该设计成多返回值,其中包括返回相应的error类型。

  10. 错误信息字符串不应大写(除非以专有名词或首字母缩略词开头)或以标点符号结尾,因为它们通常是在其他上下文后打印的。即使用fmt.Errorf("something bad")而不要使用fmt.Errorf("Something bad"),因此log.Printf("Reading %s: %v", filename, err)的格式中将不会出现额外的大写字母。否则这将不适用于日志记录,因为它是隐式的面向行,而不是在其他消息中组合。

  11. 添加新包时,请包含预期用法的示例:可运行的示例,或是演示完整调用链的简单测试。Example函数一般放在包下的_test.go文件中,比如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package stringutil_test

    import (
    "fmt"

    "github.com/golang/example/stringutil"
    )

    func ExampleReverse() {
    fmt.Println(stringutil.Reverse("hello"))
    // Output: olleh
    }

这种写法的好处是使用 go test -v,如果和注释种的结果一致只会返回PASS,如果不一定打印。

  1. 当你生成 goroutines 时,要清楚它们何时或是否会退出。请尽量让并发代码足够简单,从而更容易地确认 goroutine 的生命周期。

  2. 不要使用 _ 变量丢弃 error 如果函数返回 error,请检查它以确保函数成功。处理 error,返回 error,或者在真正特殊的情况下使用 panic。

  3. 包导入按组进行组织,组与组之间有空行。标准库包始终位于第一组中。

  4. 部分包由于循环依赖,不能作为测试包的一部分进行测试时,可以以.形式导入它们:

    1
    2
    3
    4
    5
    6
    package foo_test

    import (
    "bar/testutil" // also imports "foo"
    . "foo"
    )
  5. 将正常的代码路径保持在最小的缩进处,优先处理错误并缩进,如下:

    1
    2
    3
    4
    5
    if err != nil {
    // error handling
    return // or continue, etc.
    }
    // normal code
  6. 名称中的单词是首字母或首字母缩略词需要具有相同的大小写规则,例如,“URL” 应显示为 “URL” 或 “url” (如 “urlPony” 或 “URLPony” ),而不是 “Url”。

  7. Go 接口通常属于使用 interface 类型值的包,而不是实现这些值的包。实现包应返回具体(通常是指针或结构)类型。
    案例:这里有一个生产者的包和消费者的包,他们之间有一个Thinger做接口交互。消费者包代码:

    1
    2
    3
    4
    5
    package consumer  // consumer.go

    type Thinger interface { Thing() bool }

    func Foo(t Thinger) string { … }

正确的生产者应该这样实现:

1
2
3
4
5
6
7
8
9
10
11
12
package producer

type Thinger struct{ … }
func (t Thinger) Thing() bool { … }

func NewThinger() Thinger { return Thinger{ … } }

// 而不是在这里再声明一个接口,这样是没必要的
// type Thinger interface { Thing() bool }
// type defaultThinger struct{ … }
// func (t defaultThinger) Thing() bool { … }
// func NewThinger() Thinger { return defaultThinger{ … } }

  1. 返回参数直接写类型就可以了,但如果函数返回两个或三个相同类型的参数,或者如果从上下文中不清楚返回结果的含义,那么在某些上下文中添加命名可能很有用。
    比如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 应该这样
    func (n *Node) Parent1() *Node
    func (n *Node) Parent2() (*Node, error)
    // 不要这样
    func (n *Node) Parent1() (node *Node)
    func (n *Node) Parent2() (node *Node, err error)
    // 多值的时候,应该这样
    func (f *Foo) Location() (lat, long float64, err error)
    // 不要这样
    func (f *Foo) Location() (float64, float64, error)
  2. 包中名称的所有引用都将使用包名完成,因此您可以从标识符中省略该名称。例如,如果有一个 chubby 包,你不应该定义类型名称为 ChubbyFile ,否则使用者将写为 chubby.ChubbyFile。而是应该命名类型名称为 File,使用时将写为 chubby.File。

  3. 方法接收者的名称应该反映其身份;通常,其类型的一个或两个字母缩写就足够了(例如“client”的“c”或“cl”)。不要使用通用名称,例如“me”,“this”或“self”,这是面向对象语言的典型标识符,它们更强调方法而不是函数。名称不必像方法论证那样具有描述性,因为它的作用是显而易见的,不起任何记录目的。名称可以非常短,因为它几乎出现在每种类型的每个方法的每一行上。

  4. 接收器什么时候使用值或者指针:
    (1)小的不变结构或基本类型可以用值接收器。这样可以提高效率。
    (2)在你不确定是使用值还是指针作为接收器时,请用指针接收器。
    (3)如果该方法需要改变接收器的值,则接收器必须是指针。
    (4)如果接收器是 map,func或 chan,则不要使用指向它们的指针。如果接收器是 slice 并且该方法不重新切片或不重新分配切片,则不要使用指向它的指针。

Go 语言规范工具

go 的官方工具链做得很好,可以直接使用gofmtgolint检查代码规范。

安装:

1
2
go get github.com/golang/lint
go install github.com/golang/lint

用法:

1
golint <文件/文件目录/包名>

shikanon wechat
欢迎您扫一扫,订阅我滴↑↑↑的微信公众号!