什么是高质量:编写的代码能够达到正确可靠,简洁清晰的目标可称之为高质量代码
- 各种边界条件是否考虑完备
- 异常情况处理,稳定性保证
- 易读易维护
编程原则:
-
简单性
- 消除”多余的复杂性“,以简单清晰的逻辑编写代码
- ’不理解的代码无法修复改进
-
可读性
- 代码是写给人看的,而不是机器
- 编写可维护的代码的第一步是确保代码可读
-
生产力
- 团队整体的工作效率非常重要
编码规范:
-
代码格式尽可能地统一
- 推荐使用gofmt自动格式化代码
-
注释(好的代码有很多注释,不好的代码需要很多注释,应该提供一些使用该代码的上下文信息)
-
注释应该做的:
- 注释应该注释的内容
- 注释应该解释的代码,它是如何做的
- 注释应该解释代码实现的原因,适当解释代码的外部因素,提供额外的上下文
- 注释应该解释代码什么情况下可能会出错,解释代码的限制条件
-
公共符号始终要进行注释
- 包中声明的每个公共的符号:变量,常量,函数,以及结构都需要进行注释
- 任何及不明显也不简短的公共功能必须予以注释
- 无论长度或者复杂程度如何,对库中的任何函数进行注释
- 有一个例外,不需要注释实现接口的方法
-
-
命名规范(核心目标是降低阅读代码的成本,重点考虑上下文的信息)
-
简洁胜于冗长
-
缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
- 利用ServeHTTP而不是ServeHttp
- 使用XMLHTTPResquest或者xmlHTTPRequest
-
变量距离其被使用的地方越远,则需要携带更多的上下文信息,参数名是可以代表着一些特点含义,知道它的意思
-
函数名:
- 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的
- 函数名尽量简短
- 当名为foo的包某个函数返回类型Foo时,可以省略类型信息而不导致歧义
- 当名为foo的包某个函数返回类型T时,可以在函数中加入类型信息
-
包名:
- 只由小写字母组成。不含有大写字母或者下划线等
- 简短并且包含一定的上下文信息
- 不要与标准库重名
-
-
控制流程
-
避免嵌套,保持正常流程
-
尽量保持正常代码路径为最小缩进。优先处理错误情况或者特殊情况,尽早返回
-
故障问题大多数出现在复杂的条件语句和循环嵌套当中
// Bad func OneFunc() error{ err:= doSomething() if err == nil{ err := doAnoterThing() if err == nil{ return nil } return err } return err } // 进行改进 func OneFunc() error{ if err := doSomething(); err != nil{ return err } if err := doAnotherthing(); err != nil{ return err } return }
-
-
错误和异常处理
-
简单的错误指仅出现一次的错误,其在其它的地方不需要捕获该错误
-
优先使用errors.New来创建匿名变量来直接表示简单错误
-
如果由格式化的需求,使用fmt.Errof
func defaultCheckRedirect(req *Request, via []*Request) error { if len(via) >= 10 { return errors.New("stopped after 10 redirects") } return nil } -
错误的Wrap和Unwrap
-
错误的wrap实际上是提供了一个error嵌套另一个error的能力,从而生成error的跟踪链
-
在fmt.Errorf中使用:%w关键字来将一个错误关联到另一个错误上去
list, _, err := c.GEtBytes(cache.Subkey(a.actionID,"srcfiles")) if err != nil{ return fmt.Errorf("reading srcfiles list: w%",err) }
-
-
判断一个错误是否为指定的错误,使用errors.Is,不同于==,该方法可以判断错误链上是否含有该错误
if errors.Is(err,fs.ErrNotExist){ return true,nil } -
在错误链上获取特定种类的错误,使用errors.As
if errors.As(err,fs.pathError) -
panic
- 业务中不建议使用你panic,如果出现说明业务基本崩溃了,用于真正的异常
- 若有问题可以被屏蔽或者解决,建议使用error来代替panic
- 调用时不使用recover会让程序奔溃
-
recover
- recover只能在defer的函数中使用
- 嵌套无法失效
- 只能在gotuntine中被defer的函数生效
- defer语句会在函数返回前被调用,多个defer语句是后进先出的
-
高质量编程与性能调优实战
简介:
- 性能优化的前提是满足正确可靠,简洁清晰等质量因素
- 性能优化是综合评估,有时候时间效率和空间效率可能对立
性能优化建议--Benchmark
go语言提供了支持基准性能测试,如何使用得去查一查
slice
-
slice预分配内存,尽可能在使用make()初始化切片时提供容量信息
data := make([]int,0) data := make([]int,0,10) -
切片的底层实现就是可扩容的链表
-
陷阱:大内存未释放
- 在已有的切片上创建切片,不会创建新的底层数组
- 原切片较大,代码在原切片的基础上新建小切片
- 原底层数组在内存中引用,得不到释放
-
可使用copy替代re-slice
func GetLastBySlice(origin []int) []int{ return origin[len(origin) - 2:] } // 下面这个节省的空间更多 fun getLastByCopy(origin []int) []int{ result := make([]int,2) copy(result,origin[len(origin) - 2:]) return result }
map
-
map预分配内存
- 不断地向map中添加新元素地操作会触发map地扩容
- 提前分配好空间可以减少内存拷贝和Rehash地消耗
- 建议提前根据实际需求预估好需要的空间大小
strings.Buider进行字符串处理
- 使用+拼接性能最差,strings.Buider,bytes.Buffer相近,strings。Buffer更快
- 字符串在go里面时不可变类型,占用大小固定
- 使用+每次都会重新分配内存
- strings.Buider和bytes.Buffer底层都是使用[]byte数组
使用空结构体节省内存
使用atomic包
-
锁的实现是通过操作系统来实现,属于系统调用, 调用成本很高
-
atomic操作是通过硬件实现,效率比锁高
-
sync.Mutex应该用来保护一段逻辑,。不仅仅用于保护一个变量
-
对于非值操作,可以使用atomic.Value,能承载一个interface{}
type atomicCounter struct{ i int32 } func AtomicAddOne(c *atomicCounter){ atomic.AddInt32(&c.i,1) } type mutexCounter struct{ i int32 m sync.Mutex } func MutexAddOne(c *mutexCounter){ c.m.Lock() c.i++ c.m.UnLock() }
小结:
- 避免常见的性能陷阱可以保证大部分程序的功能
- 普通应用代码,不要一味地追求程序的性能
- 越高级的性能优化手段越容易出现问题
- 在满足正确可靠,简洁清晰的质量要求的前提下提高程序性能