阅读了Dave Cheney 关于go编码的博客:Practical Go: Real world advice for writing maintainable Go programs
实际应用下来,对我这个go入门者,提升效果显著。
我对作者的文章进行整理翻译,提取精炼,加上自己的理解,分享出来。希望也能给大家带来帮助。
希望大家支持原作者,原汁原味的内容可以点击 链接 阅读。文中部分例子为个人添加,如有不足敬请包容指出^ _ ^
(PS:如涉及侵权,请与我联系,我会及时删除文章,知识传播无界,望大家支持)
个人认为,编码的最佳实践本质是为了提高代码的迭代产能,减少bug的几率。(成本、效率、稳定)
作者Dave Cheney提到,go语言的最佳实践的指导原则,需要考虑3点:
简洁
可读性
开发效率
简洁是对于人而言的,如果代码很复杂,甚至违法人的惯性理解,那么修改和维护是牵一发而动全身的。
因为代码被阅读的次数远远多于被修改的次数。在作者看来,代码被人的阅读和修改的需求,比被机器执行的需求更强烈。go编码最佳实践第一步就应该确定代码的可读性。
在我个人看来,类似于一致性算法中, raft为什么比paxos传播和应用更广,一个很重要的原因就是raft更加易于理解,raft作者在论文中也提到,raft设计的最重要的初衷就是,paxos太难懂了。可读性的重要性应该排在首位的。
良好的编码习惯,可以提高代码的交流效率。使得同事们看到代码就知道实现了什么,而不必去逐行阅读,大大节约了时间,提高开发效率。
此外,对于go语言本身而言,无论在编译速度还是debug时间花费上,go相对C++也是开发效率大大提高的。
命名对编写可读性好的go程序至关重要!
曾经听到这样的一个言论:对变量的命名要像给自己孩子起名一样慎重。
其实,不光是变量命名,还包括function、method、type、package等,命名都很重要。
就像编码不是为了在尽量短的行数内,写完程序。而是为了写出可读性高的程序。
同样的,我们的命名标识也不是越短越好,而是容易被他人理解。
一个好名字应该具备的特点:
简短:一个好名字应该在具备高辨识度的情况下,尽量简短。
比如一个判断用户登录权限的方法:坏名字是judgeAuth
(容易歧义),judgeUserLoginAuthority
(冗长)
好的例子judgeLoginAuth
描述性的:一个好的名字应该是描述变量和常量的用途,而非他们的内容;描述function的结果,或者method的行为,而不是他们的操作;描述package的目的,而非包含的内容。描述的准确性衡量了名字的好坏。
比如设计一个用来主从选举的包。坏的package名字leader_operation
,好的名字election
坏的function或者method名字ReturnElection
,好的名字NewElection
坏的变量或者常量名字ElectionState
,好的名字Role
可预测的:一个好的名字,仅通过名字,大家就可以推断他们的用途。应该遵循大家的惯用理解。下面会详细阐述。比如
i,j,k
常用来在迭代中描述引用计数值
n
通常用来表示计数累加值
v
通常表示一个编码函数的值
k
通常用在map中的key
s
通常用来表示字符串
关于名字的长度,我们有这些建议:
如果变量的声明和它被最后一次使用的距离很短,可以使用短的变量名
如果一个变量很重要,那么可以避免歧义,允许变量名称长一些,消除歧义
变量的名字中请不要包含变量的类型名
常量的名字应该描述他们保存的值,而不是如何使用该值
单个字母的名字可以用作迭代、逻辑分支判断、参数和返回值。包和函数的名字请使用多个字母的组合。
method、interface、package 请使用单个单词
pakcage名字也是调用方引用时需要注明的,所以请利用package的名字
举一个作者文中的例子说明:
在这个例子中,people 距离最后一次使用间隔7行,而变量p是用来迭代perple的,p距离最后一次使用间隔1行。所以p可以使用1个字母命名,而people则使用单词来命名。
其实这里是防止人们阅读代码时,阅读过多行数后,突然发现一个上下文不理解的词,再去找定义,导致可读性差。
同时,注意例子中的空行的使用。一个是函数之间的空行,另一个是函数内的空行:在函数里干了3件事:异常判断;累加age;返回。在这3者之间添加空行,可以增加可读性。
以上强调的原则需要在上下文中去实际判断才行,万事无绝对。
相比,显然使用oid命名更具备可读性,而使用短变量o则不容易理解。
因为golang 是一个强类型的语言,在变量的命名中包含类型是信息冗余的,而且容易导致误解错误。举个作者的例子:
var usersMap map[string]*User
我们将一个从string 到 User 的map结构,命名为UsersMap,看起来合情合理,但是变量的类型中已经包含了map,没有必要再在变量中注明了。
作者的话来讲:如果Users 描述不清楚,nameUsersMap也不见得多清楚。
对于函数的名称同样适用,比如:
config 的名称有冗余了,类型中已经说明它是一个*Config了,如果变量在函数中最后一次引用的距离足够短,那么适用简称c或者conf 会更简洁。
提示:不要让包名抢占了好的变量名。比如context这个包,如果使用
func WriteLog(context context.Context, message string)
,那么编译的时候会报错,因为包名和变量名冲突了。所以一般使用的时候,会使用func WriteLog(ctx context.Context, message string)
尽量不要将常见的变量名,换成其他的意思,这样会造成读者的歧义。
而且对于代码中一个类型的变量,不要多次改换它的名字,尽量使用一个名字。比如对于数据库处理的变量,不要每次出现不同的名字,比如d *sql.DB,dbase *sql.DB,DB *sql.DB
,最好使用惯用的,一致的名字db *sql.DB
。这样你在其他的代码中,看到变量db时,也能推测到它是*sql.DB
还有一些惯用的短变量名字,这里提一下:
i, j, k
用作循环中的索引
n
用在计数和累加
v
表示值
k
表示一个map或者slice 的key
s
表示字符串
对于一个变量的声明有多重声明类型:
var x int = 1
var x = 1
var x int;x=1
var x = int(1)
x:=1
在作者看来,这是go的设计者犯的错误,但是来不及改正了,新的版本要保持向前兼容。有这么多种声明的方式,我们怎么选择自己的类型呢。
作者给出了这些建议:
当声明一个变量,但是不去初始化时,使用var
。
var
往往表示这是这个类型的空值。
当声明并且初始化值的时候,使用:=
对于go来说,= 右侧的类型,就是=左侧的类型,上面三个例子中,最后一个使用:=
的例子,既能充分标识类型,又足够简洁。
编程生涯大部分时间都是和作为团队的一员,参与其中。作者建议大家最好保持团队原来的编码风格,即使那不是你偏爱的风格。要不人会导致整个工程风格不一致,这会更糟糕。
注释很重要,注释应该做到以下3点之一:
解释做了什么
解释怎么做
解释为什么这么做
举个例子
这是适合对外方法的注释,解释了做了什么,怎么做的
这是适合方法内的注释,解释了做了什么
解释为什么的注释比较少见,但是也是必要的,比如以下:
将value 设置成0的作用并不好理解,增加注释大大增加可理解性。
在上文中提到,变量和常量的名字又应该描述他们的目的。然而他们的注释最好描述他们的内容。
const randomNumber = 6 // determined from an unbiased die
在这个例子中,注释描述了为什么randomNumber
被赋值为6,注释没有描述在哪里randomNumer
会被使用。再看一些例子:
这里区分一下,内容表示100代表什么,代表RFC 7231,但是100的目的是表示StatusContinue。
提示,对于没有初始值的变量,注释应该描述谁来初始化这些变量
// sizeCalculationDisabled indicates whether it is safe // to calculate Types
' widths and alignments. See dowidth. var sizeCalculationDisabled bool
因为dodoc 是你的项目package的文档,所以你应该在每个公共的名称上添加注释,包括变量,常量,函数,方法。
这里给出两个谷歌风格指南的准则:
任何不是简练清晰的公共的函数,都应该添加注释
库中的任何函数,不管名称多长或者多么负责,都必须增加注释
举个例子:
这个规则有一个例外,无需对实现接口的方法添加文档注释,比如不要这么做:
这里给出一个io
包的完整例子:
提示:在写函数的内容前,最好先把函数的注释写出
如果遇到了不完善的代码,应该记录一个issue,以便后续去修复。
传统的方法是在代码上记录一个todo,以便提醒。比如
好的代码本身就是注释。如果要在一段代码上添加注释,要问问自己,能否优化这段代码,而不用添加注释。
函数应该只做一件事,如果你发现要在这个函数的注释里,提到其他函数,那么该想想拆解这个冗余的函数。
此外,函数越精简,越便于测试。而且函数名本身就是最好的注释。
每个go 的package 实际上都是自己的小型go程序。就好比一个function或者method的实现对调用者无关一样,包内的对外暴露的function,method和类型的实现,和调用者无关。
一个好的go长须应该努力降低耦合度,这样随着项目的演化,一个package的变化不会影响到整个程序的其他package。
接下来会讨论如何设计一个package,包括名字,类型,和编写method和funciton的一些技巧。
package 的名字应该尽量简短,最好用一个单词表示。考虑package名字的时候,不要想着我要在package内写哪些类型,而是想着这个package要提供哪些服务。要以package提供哪些服务命名。
一个项目那的package名字应该都是不同的。如果你发现可能要取相同的pcakge名字,那么可能是以下原因:
package的名字太通用了
这个package提供的服务与另一个package重合了。如果是这种情况,要考虑你的package设计了
base
,common
,util
如果package内包含了一些列不相关的function,那么很难说明这个package提供了哪些服务。这常常会导致package名字取一些通用的名字,类似utilities
。
大的项目中,经常会出现像utils
或者helpers
这样的package名字。它们往往在依赖的最底层,以避免循环导入问题。但是这样也导致出现一些通用的包名称,并且体现不出包的用意。
作者的建议是将utils
和helpers
这样的package名字取取消掉:分析函数被调用的场景,如果可能的话,将函数转移到调用者的package内,即使这涉及一些代码的拷贝。
提示:代码重复,比错误的抽象,代价更低
提示:使用单词的复数命名通用的包。比如
strings
包含了string处理的通用函数。
我们应该尽可能的减少package的数量,比如现在有三个包common
、client
,server
,我们可以将其组合为一个包het/http
,用client.go和server.go来区分client和server,避免引入过多的冗余包。
提示,标识符的名字包含了包名,比如
net/http
的GET
function,调用的使用写作http.Get
,在标识符起名和package起名时要考虑这一点
活动: Gopher Meetup 巡回第五站 - 广州报名火热进行中~
详情点击阅读原文