当前位置: 首页 > news >正文

Go开发者常犯的错误,及使用技巧 (1)

代码规范

命名不规范

  • 变量名要有意义,不能随便取a,b,c

    • 如果只是纯粹的算法题,这样问题不大。但工程上的代码可读性要求较高,不能随意命名变量名,例如:
for _, v := range userList {// ... 
}

如果for语句块简短还好,如果很长,顺着读下去在后面可能都不记得v代表什么,如果将v换成user,可读性会增加很多

不要说别人读自己的代码,就算自己隔几个月读自己的代码,如果可读性不高也会增加回忆时间

  • 变量名和实际含义要相同

    • 变量名需要和变量本身的意义相同,不能代码看起来是userIds,实际上装的是userInfos。这个问题比变量名没有意义更严重,因为对阅读代码的人产生误导
    • 常发生于随着代码迭代,修改了变量的内容而忽略了修改变量名
  • 方法名需要具体,不能用含糊不清的handleXXXdealXXX

    • 例如:处理试卷格式转换的方法,如果命名handleExam,使用者就无法通过方法名明确的知道该方法要干啥,还需要深入方法内部阅读详细逻辑,徒增理解成本
    • 推荐做法:将笼统的handleExam改为具体TransferExamFormat
  • 方法名含义要清晰

    • 例如:如果方法内部从缓存中获取,最好在方法名加上FromCache,提醒调用者可能使用的是过期的数据
  • 名称啰嗦:

    • 例如包名叫dingding,如果接口名再叫dingdngRobotService,调用者使用时的代码就是:dingding.dingdingRobotService.XXX,就会显得啰嗦
    • 解决方法:接口名不要和包名冗余,改为dingding.RobotService

必要的注释

如果代码质量好,可读性高,其实不需要什么注释

如果有特殊逻辑,需要用注释交代清楚这么做的原因

一旦加了注释,就需要随时保证注释和代码内容一致,不要因为产品迭代修改了代码,而忽略了修改注释

魔法数字

这个问题在比较常见,有时候为了图方便,会写出if XXX.status == 4 之类的代码,这必然会变成维护人员的噩梦

因为根本不知道4代表什么含义,只能尝试从各种文档,数据库改字段的注释中找,十分不方便

解决方法很简单,在const文件中集中维护这些特殊变量,例如:下面定义人教版的常量

const EditionRenjiao = 100

这样有以下好处:

  • 增加可读性:从常量名就知道其含义
  • 解耦:代码中其实不需要关心人教版的常量值是多少,只需要知道使用的是人教版,这里将人教版这个概念这个概念具体的值接偶,这样当以后如果值变动,代码侧也不需要修改

降低包的表面积

不要都放到一个常量文件中,导致其变得很臃肿,还容易造成命名冲突

  • 除了通用的常量外,应该各个模块分别维护一个常量文件(包),因为如果a模块不依赖b模块,那么a模块不需要也不应该使用b模块的常量
  • 甚至不同层之间的常量也可以区分,因为service层的逻辑不应该了解dao层自己使用的常量

不应该夸层依赖

如果a依赖b,b依赖c,则a不应该直接依赖c

例如:dao层依赖的gorm框架返回了gorm.RecordNotFound错误,dao层给service层要么返回nil,要么返回dao层自己的RecordNotFound错误,因为如果service层依赖了gorm的东西, 当dao层修改orm框架时,service层也需要修改,改动幅度较大。而如果service层只依赖dao层,那么变动只会影响dao层

函数太长

函数太长会导致可读性下降,难以维护

怎么解决呢?将方法根据功能进行拆分

func ABCDE() {ABCDE
}拆分为:func ABCDE() {A()B()C()D()E()
}

看起来就清晰多了,如果本身有上千行,进行划分之后每个函数也不会太长

而且每个小函数也方便写单测,容易提高代码质量

将函数在一屏幕展示完比较好,或者61行内

其次,同一个方法内不同部分也可以用空行分割,提高可读性

参数列表过长

当参数列表过长时会造成可读性下降,因此建议:

  • 参数大于6个就可以封装为req结构体这样以后要新增参数,不用修改方法签名,特别是函数调用层级很深的场景下,能降低不少改动量
  • 使用option模式

If else多层嵌套

func GetEnvName() string {if IsOnline() {return "online"} else {if IsLocalHost() {return "localhost"} else {return "test_" + GetShipName()}}
}

这样会导致else部分的层级很深,修改成下面:

func GetEnvName() string {if IsOnline() {return "online"}if IsLocalHost() {return "localhost"}return "test_" + GetShipName()
}

这两种实现的代码逻辑一样,但下面的实现方式通过提早返回,减少了很多else分支,使得每个逻辑的层级都不深,读起来十分清爽

并发问题

对map的并发使用

go的map不是线程安全的大家都知道,但有时候不是主动调用,而是依赖的库使用了map,我们并不知道,我们并发 使用了依赖的库,例如:

func ProcessWithHandle(ctx *gin.Context, req *dto.Req) (*http.Result, error) {// ...var redisKey = ctx.Request.Header.Get(defines.HeaderCacheKey)// ,,,

gohttp的header使用的普通map,因此不能并发使用

// A MIMEHeader represents a MIME-style header mapping
// keys to sets of values.
type MIMEHeader map[string][]string

怎么解决?使用gin.Context的set,get方法,有锁保护:

// Set is used to store a new key/value pair exclusively for this context.
// It also lazy initializes  c.Keys if it was not used previously.
func (c *Context) Set(key string, value interface{}) {c.mu.Lock()if c.Keys == nil {c.Keys = make(map[string]interface{})}c.Keys[key] = valuec.mu.Unlock()
}

context问题

  • context.Context 是 Go 语言中的一个标准类型,它内部往往包含跨越API,goroutine进程,进程边界的安全证书、trace信息、超时时间和取消信号等信息。Go 程序在整个过程的函数调用链中显式地传递 Context
  • 建议在所有方法的第一个参数位置都加上context,就算当前不用也建议加上,因为如果以后扩展,例如加上打印日志的逻辑时如果没有ctx,该条日志就会从请求链路中断掉
  • context不应该作为某个结构体的字段,因为context应该只和该次请求相关,请求结束,context的生命周期也应该结束

panic相关

panic造成的问题

  • 如果没有用recover处理panic,会直接结束go进程

    • 直接结束进程有什么问题?

      • 无法进行优雅关闭,正在处理中的用户请求会失败,损失用户体验
      • 后台的资源关闭无法进行,例如需要在优雅关闭回调中将本地缓存持久化的操作
      • 如果只有一台实例,会造成短时间内服务不可用,直到拉起新的实例为止
    • 一般发生在业务代码自己开的goroutine中,业务代码本身的panic会被框架(例如gin)的recover中间件兜底处理

func gopanic(e interface{}) {// ...preprintpanics(gp._panic)fatalpanic(gp._panic) // 结束进程:*(*int)(nil) = 0      // not reached}
  • 推荐做法:封装工具方法SafeGo
func SafeGo(ctx context.Context, fn func()) {go func() {defer func() {if err := recover(); err != nil {// 记录日志,发送报警          }}()fn()}()
}
  • 就算用recover处理了,例如中间件中,一般也是兜底的处理,只能给调用方返回系统错误之类的返回。因此如果提前识别出panic,可以根据不同情况返回不同的错误提示

可能出现panic的场景

  • 操作nil对象:

    • 常见于user.XXX.XXX.XXX之类的使用模式
    • 推荐做法:需要确保中间每个环节的对象都不为nil
  • 类型断言:

    • str := value.(string):这种断言调用,如果value不是string类型就会panic
    • 推荐做法:使用安全的断言方式:str, ok := value.(string)
  • slice操作不当:

    • 例如slice类型的a的len为0,却用a[0]取数据

推荐使用panic的场景

go推荐通过err来处理错误,panic则用于处理预期之外,或十分严重的错误

例如:

  • 项目启动时,如果各种组件,依赖的client初始化失败推荐panic直接终止进程,因为如果启动起来,就是不符合预期的代码在跑,可能造成问题,还不如提早把错误暴露出来

    • 三方依赖包内不要在内部panic,而是返回err,交给业务代码决定要不要panic
  • MustXXX系列方法

    • 例如MustConvertString2Int64(v string)MustGetUserId(ctx context.Context)
    • 这类方法为了方便使用不会返回err,如果内部有err只能panic处理,因此需要保证使用时一定能成功,例如在前面进行了参数检查,那么在后面的流程中调用MustConvert就一定能成功

栈溢出问题

只要没有递归调用,go其实不容易出现栈溢出的问题,因为栈深度最大能扩到1G

但一旦出现递归调用,且递归逻辑有bug,或者使用的数据配合递归逻辑有可能造成无限递归,就会出现栈溢出

栈溢出在go中十分严重,因为会直接结束进程

解决方法:

  • 检查递归逻辑,确保没有bug
  • 调用递归对使用的数据进行环检测,如果检测出环不能调用递归函数

错误处理

不能忽略错误

大部分情况下,错误一定不能忽略,因为如果忽略了,例如:

_ = redisClient.Set(ctx, key, value)

这样如果操作失败,不会在日志中体现,也不会返回给调用方,就默默吞掉错误

即:可能操作不成功,但给用户返回了成功,产生误导

且后面排查问题时由于没有日志记录,无法确认到底是不是因为这里没有操作成功

因此大部分情况下错误一定要处理,通常是要么返回给调用者,要么记录日志

如果出现错误了大部分情况下需要返回给调用者,否则后面的代码会使用错误/不完整的结果,除非明确知道这里返回的错误对后面的流程无影响

为什么说大部分?少部分情况下不用处理,例如json.Marshal,该方法一般不会返回err,不处理的话可以减少代码量

只应该处理一次错误

错误只应该被处理一次,例如不能在调用链上,每次出现错误都记录日志

if err != nil {log.Errorf(ctx, "doXXX fail, err = %v", err)return err
}

这样的话有多少次方法调用,就会打印多少次日志,产生大量冗余日志,干扰问题排查

推荐做法:没有特殊情况内部方法只管返回,在入口处记录err日志

错误中应该携带足够的上下文

如果记录的日志中没有足够的上下文信息,则该日志价值很低,一般来说需要携带的信息有:

  • 发生错误的调用栈快照:使用errors.wrap
  • 栈顶函数的部分参数信息,局部变量信息:使用errors.WithMessage

什么时候携带栈信息,什么时候携带其他信息呢?

  • 一般在最里层用wrap或stack包装

    • 什么是最里层,即没有再进行方法调用,或调用外部依赖的那一层
    • 调用当前项目的其他包不算最里层
  • 上层要么直接返回最里层的err,要么用withMessage添加一些当前层的信息

  • 相应的,如果自己开发第三方库,不应该返回堆栈信息

    • go标准库就是这个做法
    • 业务代码也不该关心三方库的调用堆栈

怎么判断是否携带了足够的上下文信息?

根据该条日志能否排查出问题

什么时候返回err

公开方法基于扩展性考虑最好加一个err作为返回值,即使当前认为不需要返回err

http://www.lryc.cn/news/12052.html

相关文章:

  • Servlet 作业
  • Hive高阶函数:explode函数、Lateral View侧视图、聚合函数、增强聚合
  • 信息系统服务管理
  • Windows10 安装ElasticStack8.6.1
  • gRPC 非官方教程
  • 6.2【人工智能与深度学习】RNN、GRU、远程服务管理、注意力、Seq2 搜索引擎和内存网络
  • 软件工程复习
  • 将Nginx 核心知识点扒了个底朝天(二)
  • 【PowerQuery】PowerBI 的PowerQuery支持的数据集成
  • scipy spatial transform Rotation库的源代码
  • JAVA文件操作
  • 字符串匹配 - 模式预处理:BM 算法 (Boyer-Moore)
  • RV1126笔记三十:freetype显示矢量字体
  • polkit pkexec 本地提权漏洞修复方案
  • es-06聚合查询
  • 面试知识点准备与总结——(并发篇)
  • Django框架之模型视图-URLconf
  • 操作系统闲谈06——进程管理
  • DaVinci 偏好设置:用户 - UI 设置
  • Nacos超简单-管理配置文件
  • 基于微信小程序的中国各地美食推荐平台小程序
  • 如何优雅的导出函数
  • c++多重继承
  • 15_FreeRtos计数信号量优先级翻转互斥信号量
  • 二叉树(一)
  • 【SCL】1200案例:天塔之光数码管显示液体混合水塔水位
  • 5.1配置IBGP和EBGP
  • c++中超级详细的一些知识,新手快来
  • [答疑]经营困难时期谈建模和伪创新-长点心和长点良心
  • 计算机基础知识