golang静态代码检查配置与常见格式异常
go环境配置
下载go1.13版本,通过命令解压到/usr/local目录下,并设置环境变量:sudo tar -zxvf go1.13.9.linux-amd64.tar.gz -C /usr/local/
Ubuntu_20200618虚拟机环境变量如下,通过gedit ~/.bashrc命令打开并编辑:
export GOROOT=/usr/local/go # install dictory export GOPATH=/home/sym/go/gopath # work envirment export GOBIN=$GOPATH/bin # executable file export PATH=$GOPATH:$GOBIN:$GOROOT/bin:$PATH # PATH path复制代码
代理设置
Go1.13为环境变量GOPROXY设置了默认路径:proxy.golang.org,direct。但是由于某些因素,这个使用不了,你懂的。所以七牛云为中国的gopher提供了一个免费合法的代理goproxy.cn,其已经开源。只需一条简单命令就可以使用该代理:
go env -w GOPROXY=https://goproxy.cn,direct复制代码
原文链接:blog.csdn.net/qq_31930499…
记住不要在go mod所在项目的目录中执行go get命令,否则会将其添加为该项目依赖放进go mod文件中,而不是编译二进制文件到go bin目录下。当需要使用go get命令安装响应的包时,应该在GOPATH路径下使用该命令。
GOPATH三个目录文件
$GOPATH 目录约定有三个子目录:
src 存放源代码(比如:.go .c .h .s等) pkg 编译后生成的文件(比如:.a) bin 编译后生成的可执行文件(为了方便,可以把此目录加入到 $PATH 变量中)复制代码
原文链接:blog.csdn.net/shangsongww…
golang静态代码检查
gofmt命令格式化代码
①gofmt保存的时候自动 格式化go代码 单文件的格式化:gofmt -l -w test.go 整个项目的格式化:gofmt -l -w goprojectName
goimports命令导入包排序
②goimports 保存的时候自动导入处理包及排序 科学上网安装:go get golang.org/x/tools/cmd/goimports 使用如下命令,对代码导入包顺序进行格式化:
sym@sym-virtual-machine:~/go/gopath/src/github.com/sean/sms$ goimports ./.. sym@sym-virtual-machine:~/go/gopath/src/github.com/sean/sms$ goimports tools/network/request_ip.go 复制代码
其中,goimports 具体的go代码文件包路径,是对单个go代码文件进行格式化; goimports ./.. 是对该项目下所有的go文件格式化。 注意:有时这个问题是由未格式化导致的,及时多次执行goimports ./xx.go代码文件的命令还是继续报这个错误,这时可以再执行gofmt -l -w ./xx.go命令格式化源代码文件。
golangci-lint语法检查
③golangci-lint或gometalinter 保存的时候自动检查go语法 golangci-lint代码格式检查:golangci-lint run ./... 这里...代表的就是检测目录下所有的文件 或者,在项目目录下直接运行:golangci-lint run ,该命令等价于golangci-lint run ./... 安装 安装最新版本的golint:github.com/golang/lint安装命令:go get -u github.com/golangci/golangci-lint/cmd/golangci-lint,该命令会出错,在GOPATH下改用命令:
wget -O - -q https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s v1.25.1 复制代码
这里使用1.25.1版本,因为gitlab-ci.yml中指定了该版本 image: golangci/golangci-lint:v1.25.1。同时,该包的版本,需要与golang版本一致,否则可能会报错,可以参考文章blog.csdn.net/woailuo626/…
sym@sym-virtual-machine:~/go/gopath/bin$ wget -O - -q https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s v1.25.1 golangci/golangci-lint info checking GitHub for tag 'v1.25.1' golangci/golangci-lint info found version: 1.25.1 for v1.25.1/linux/amd64 golangci/golangci-lint info installed ./bin/golangci-lint sym@sym-virtual-machine:~/go/gopath/bin$ sym@sym-virtual-machine:~/go/gopath/bin$ ls dlv fillstruct gocode gocode-gomod godef godoctor golangci-lint golint gomodifytags go-outline gopkgs goplay gorename goreturns go-symbols gotests guru impl sym@sym-virtual-machine:~/go/gopath/bin$ golangci-lint --version golangci-lint has version 1.25.1 built from 07374ce on 2020-04-27T18:08:03Z复制代码
运行:
golangci-lint run [目录]/[文件名]。复制代码
在vscode中配置如下:go.lintTool和go.lintFlags选项
参考:blog.csdn.net/benben_2015…blog.csdn.net/linux_Allen…
使用golangci-lint run后,不规范内容优化 ①a blank import should be only in a main or test package, or have a comment justifying it (golint)应该为使用_导入的包添加注释
_ "github.com/jinzhu/gorm/dialects/mysql"复制代码
应该修改为:
// mysql driver _ "github.com/jinzhu/gorm/dialects/mysql"复制代码
②可导出变量或常量需要添加注释
exported const XXX should have comment or be unexportedexported var XXX should have comment or be unexported复制代码
可导出变量或常量XXX需要添加注释,(比如// XXX .),XXX后面要带一个空格+至少一个字符,这里为了简便使用空格+. 可导出变量就是首字符为大写的变量。 ③可导出的函数XXX返回了一个未导出的类型(结构)
exported func XXX returns unexported type *cclua.tagLuaInfo, which can be annoying to use复制代码
可导出的函数XXX返回了一个未导出的类型(结构),将XXX首字符改为小写,或者将XXX的返回类型改为可导出(首字符大写)。
④error字符串首字母不应该大写,或以标点符号/换行符结尾
error strings should not be capitalized or end with punctuation or a newline复制代码
错误的字符串没有初始化或以标点或换行符结尾,比如errors.New("hey man,wtf!"),可以将最后的!号去掉即可消除该隐患。
⑤使用fmt.Errorf(...)替换errors.New(fmt.Sprintf(...))
should replace errors.New(fmt.Sprintf(...)) with fmt.Errorf(...)复制代码
应该替换errors.New(fmt.Sprintf(...))为fmt.Errorf(...)
⑥ if语句块以return语句作为结尾,应该删除其else语句块部分并且反缩进该语句块
if block ends with a return statement, so drop this else and outdent its block复制代码
if语句块以return语句作为结尾,应该删除其else语句块部分并且反缩进该语句块,e.g:
arr := strings.Split(funcName, ".") if len(arr) < 2 { arr = strings.Split(funcName, ":") if len(arr) < 2 { return nil, fmt.Errorf("func[%v].no-exists", funcName) } else { result := make([]lua.LValue, len(args)+1) result[0] = lua.LString("self") for i := 0; i < len(args); i++ { result[i+1] = args[i] } args = result } }复制代码
应该修改为:复制代码
arr := strings.Split(funcName, ".") if len(arr) < 2 { arr = strings.Split(funcName, ":") if len(arr) >= 2 { result := make([]lua.LValue, len(args)+1) result[0] = lua.LString("self") for i := 0; i < len(args); i++ { result[i+1] = args[i] } args = result } else { return nil, fmt.Errorf("func[%v].no-exists", funcName) } }复制代码
⑦包的导入顺序不合规范
file is not goimports-ed (goimports) "github.com/sean/sms/configs/consts" "github.com/sirupsen/logrus" "net" "net/http"复制代码
应该改为:
"net" "net/http" "github.com/sean/sms/configs/consts" "github.com/sirupsen/logrus"复制代码
包的导入顺序,一般为基础包,再者是开源的包或自己的(项目的其他)包,且两者之间有一个换行符,并且安装字典顺序排列。
⑧Error return value of ctx.JSON is not checked (errcheck) 按理说ctx.JSON不应该做err != nil的异常检测的,而golangci-lint也存在异常位置误报的情况,这里就需要看看上下文有没有换行符或者其他不符合规范的格式,如:
err := ctx.ReadJSON(&req) if err != nil { logrus.Errorf("parse request body error:%v", err) ctx.JSON(common.NormalRes{consts.ErrRequestParams, err.Error(), nil}) return } fmt.Printf("[GetSendByUserIDHistory] send history request data:%v\n", req)复制代码
改为:
if err := ctx.ReadJSON(&req); err != nil{ logrus.Errorf("parse request body error:%v", err) ctx.JSON(common.NormalRes{consts.ErrRequestParams, err.Error(), nil}) return } fmt.Printf("send history request data:%v", req)复制代码
上面主要的问题,是err := ctx.ReadJSON(&req)和err != nil 应该合并为一句 if err := ctx.ReadJSON(&req); err != nil
特别是,对于非空、判零、判等等字段内容的检查,应该放在services层,而不应该放在controllers的handle层;同时,在ctx.ReadJSON等errcheck语句里,不要有fmt.Error或者logrus.Errorf等语句,否则也会在该语句上线出现ctx.JSON is not checked(errcheck)类型的异常提示,如:
controllers/handler/contacts/contact.go:27:11: Error return value of `ctx.JSON` is not checked (errcheck) ctx.JSON(common.NormalRes{consts.ErrRequestParams, err.Error(), nil}) ^ controllers/handler/contacts/contact.go:32:11: Error return value of `ctx.JSON` is not checked (errcheck) ctx.JSON(common.NormalRes{consts.ErrRequestParams, errors.New("用户编号不能为空").Error(), nil}) ^复制代码
在controllers层源代码有如下语句:
if req.UserID == 0 { // 添加时不传联系人对应的用户user_id也返回错误 ctx.JSON(common.NormalRes{consts.ErrRequestParams, errors.New("用户编号不能为空").Error(), nil}) return }复制代码
进行如下修改即可:
if err := contactServ.CheckUserID(req.UserID); err != nil { // 添加时不传联系人对应的用户user_id也返回错误 ctx.JSON(common.NormalRes{consts.ErrRequestParams, errors.New("用户编号不能为空").Error(), nil}) return }复制代码
// 同时将CheckUserID,即判断req.UserID是否等于0的errcheck处理逻辑放在service层,如下:
func CheckUserID(userID int) (err error) { if userID == 0 { err = fmt.Errorf("用户编号不能为空") } return }复制代码
另外,对应返回结构的组装,参考就简原则,也不要在ctx.JSON前,使用如下先赋值给结构体变量,再把结构体变量传递给ctx.JSON的形式,否则也会出现上面的errcheck提示:
res = ContactRespose{ Code: consts.StatusOK, Msg: "OK", Data: ct, } ctx.JSON(res)复制代码
可以在ctx.JSON中直接使用结构体进行传参,结构体中的字段要使用带key的显示赋值,如:
ctx.JSON(ContactRespose{Code: consts.StatusOK, Msg: "OK", Data: ct})复制代码
注意:函数头部的注释是否与函数名一致,并且请求变量要使用结构体的指针形式,并使用短变量赋值符的形式:=进行赋值,否则也可能出现ctx.JSON errcheck的异常提示。
func GetSendHistoryByUser(ctx iris.Context) { var req RequstSendHistory if err := ctx.ReadJSON(&req); err != nil { ctx.JSON(common.NormalRes{consts.ErrRequestParams, err.Error(), nil}) return } ...... }复制代码
改为:
func GetSendHistoryByUser(ctx iris.Context) { req := &RequstSendHistory{} if err := ctx.ReadJSON(req); err != nil { ctx.JSON(common.NormalRes{consts.ErrRequestParams, err.Error(), nil}) return } ...... }复制代码
⑨composite literal uses unkeyed fields结构体返回时,字段不带key
func CommonRes(code int, msg string, data interface{}) (res NormalRes) { res = NormalRes{ Code: code, Msg: msg, Data: data, } return res } func FindHandler(ctx iris.Context) { addrs, code, err := addrServ.Find() if err != nil { ctx.JSON(common.NormalRes{code, err.Error(), nil}) return } ctx.JSON(common.NormalRes{Code: consts.StatusOK, Msg: "OK", Data: addrs}) }复制代码
使用上面无键字段初始化,就会警告:
controllers/address/address.go:24:12: composites: git.xxx.cn/ff/zhouyi/controllers/common.NormalRes
composite literal uses unkeyed fields (govet)
ctx.JSON(common.NormalRes{code, err.Error(), nil})
在自己看来NormalRes结构体中的字段就应该是与Code、Msg、Data对应的,但是使用golangci-lint进行静态代码检查时,编译器却不认账。 正确初始化方法是带上相应字段的key:
ctx.JSON(common.NormalRes{Code:code, Msg:err.Error(), Data:nil})复制代码
You can disable it with the -composites=false flag,可以使用下面的命令禁止composite检查,参考这篇文档:
go vet -composites=false复制代码
gosec安全分析
④Gosec:Go语言源码安全分析工具 安装:
$ go get github.com/securego/gosec/cmd/gosec/...复制代码
使用: 我们可以将Gosec配置为仅运行某个规则子集,如排除某些文件路径,生成不同格式的报告等。在默认情况下,Gosec将对提供的输入文件运行所有规则。要从当前目录递归扫描,你可以提供’./…’ 作为输入参数。
选择规则: 默认情况下,gosec将针对提供的文件路径运行所有规则。但如果你要指定运行某个规则,则可以使用 ‘-include=’ 参数,或者你也可以使用 ‘-exclude=’来排除那些你不想运行的规则。
可用规则:
G101:查找硬编码凭证 G102:绑定到所有接口 G103:审计不安全区块的使用 G104:审计错误未检查 G105:审计math/big.Int.Exp的使用 G106:审计ssh.InsecureIgnoreHostKey的使用 G201:SQL查询构造使用格式字符串 G202:SQL查询构造使用字符串连接 G203:在HTML模板中使用未转义的数据 G204:审计命令执行情况 G301:创建目录时文件权限分配不合理 G302:chmod文件权限分配不合理 G303:使用可预测的路径创建临时文件 G304:作为污点输入提供的文件路径 G305:提取zip存档时遍历文件 G401:检测DES,RC4或MD5的使用情况 G402:查找错误的TLS连接设置 G403:确保最小RSA密钥长度为2048位 G404:不安全的随机数源(rand) G501:导入黑名单列表:crypto/md5 G502:导入黑名单列表:crypto/des G503:导入黑名单列表:crypto/rc4 G504:导入黑名单列表:net/http/cgi复制代码
Run a specific set of rules:
$ gosec -include=G101,G203,G401 ./...复制代码
Run everything except for rule G303:
$ gosec -exclude=G303 ./...复制代码
注释代码:
与所有自动检测工具一样,gosec也会出现误报的情况。如果gosec报告已手动验证为安全的,则可以使用“#nosec”来注释代码。
注释将导致gosec停止处理AST中的任何其他节点,因此可以应用于整个块或应用于单个表达式中。
import "md5" // #nosec 不要忘记//注释符号 func main(){ /* #nosec */ if x > y { h := md5.New() // this will also be ignored } }复制代码
在某些情况下,你可能还需要重新访问已使用#nosec注释的位置。那么你可以执行以下命令来运行扫描程序以及忽略#nosec注释:
$ gosec -nosec=true ./...复制代码
govet
⑤govet
Go vet composite literal uses unkeyed fields复制代码
在使用go vet进行语法检查时, 报了这么个错composite literal uses unkeyed fields 对于刚开始看Golang的我一脸懵逼, 明明是可以编译通过且跑通的… struct 是这样定义的
type CallRequest struct { AccessToken string APIName string APIVersion string APIParams map[string]string }复制代码
代码里是这样用的 // ... 省略 ...
request := CallRequest{accessToken, apiName, apiVersion, params}复制代码
然后 go vet ./... 就报错了... composite literal uses unkeyed fields 看了些资料后知道了, 这样写更严谨一些:
request := CallRequest{AccessToken: accessToken, APIName: apiName, APIVersion: apiVersion, APIPara复制代码
或者,直接将golangci.yml配置文件中的govet注释掉。
这里将errcheck、gosec、govet注释掉。
gocritic检查
⑥gocritic golint代码检查异常信息:
tools/utils/reflect.go:45:2: typeSwitchVar: case 0 can benefit from type switch with assignment (gocritic) switch value.(type) {复制代码
对应的代码如下:
func InterfaceType2String(value interface{}) string { var key string if value == nil { return "" } switch value.(type) { case float64: ft := value.(float64) key = strconv.FormatFloat(ft, 'f', -1, 64) case float32: ft := value.(float32) key = strconv.FormatFloat(float64(ft), 'f', -1, 64) case int: it := value.(int) key = strconv.Itoa(it) case uint: it := value.(uint) key = strconv.Itoa(int(it)) case int8: it := value.(int8) key = strconv.Itoa(int(it)) case uint8: it := value.(uint8) key = strconv.Itoa(int(it)) case int16: it := value.(int16) key = strconv.Itoa(int(it)) case uint16: it := value.(uint16) key = strconv.Itoa(int(it)) case int32: it := value.(int32) key = strconv.Itoa(int(it)) case uint32: it := value.(uint32) key = strconv.Itoa(int(it)) case int64: it := value.(int64) key = strconv.FormatInt(it, 10) case uint64: it := value.(uint64) key = strconv.FormatUint(it, 10) case string: key = value.(string) case []byte: key = string(value.([]byte)) default: newValue, err := json.Marshal(value) if err != nil { logrus.Errorf("value:%v, Marshal failed, error:%v", value, err) } key = string(newValue) } return key }复制代码
对于type switch,应该使用一个变量接收该value的类型推断,如t := value.(type),并且第一个case应该为nil,对类型推断为nil的进行逻辑处理。 应该修改为:
func InterfaceType2String(value interface{}) (key string) { switch t := value.(type) { case nil: fmt.Printf("type:%T, value:%v\n", t, t) key = "" case float64: ft := value.(float64) key = strconv.FormatFloat(ft, 'f', -1, 64) case float32: ft := value.(float32) key = strconv.FormatFloat(float64(ft), 'f', -1, 64) case int: it := value.(int) key = strconv.Itoa(it) case uint: it := value.(uint) key = strconv.Itoa(int(it)) case int8: it := value.(int8) key = strconv.Itoa(int(it)) case uint8: it := value.(uint8) key = strconv.Itoa(int(it)) case int16: it := value.(int16) key = strconv.Itoa(int(it)) case uint16: it := value.(uint16) key = strconv.Itoa(int(it)) case int32: it := value.(int32) key = strconv.Itoa(int(it)) case uint32: it := value.(uint32) key = strconv.Itoa(int(it)) case int64: it := value.(int64) key = strconv.FormatInt(it, 10) case uint64: it := value.(uint64) key = strconv.FormatUint(it, 10) case string: key = value.(string) case []byte: key = string(value.([]byte)) default: newValue, err := json.Marshal(value) if err != nil { logrus.Errorf("value:%v, Marshal failed, error:%v", value, err) } key = string(newValue) } return key }复制代码
结构体内存对其
⑦struct of size maligned内存对齐 参考:Golang 是否有必要内存对齐?有些同学可能不知道,struct 中的字段顺序不同,内存占用也有可能会相差很大。比如:
type T1 struct { a int8 b int64 c int16 } type T2 struct { a int8 c int16 b int64 }复制代码
在 64 bit 平台上,T1 占用 24 bytes,T2 占用 16 bytes 大小;而在 32 bit 平台上,T1 占用 16 bytes,T2 占用 12 bytes 大小。可见不同的字段顺序,最终决定 struct 的内存大小,所以有时候合理的字段顺序可以减少内存的开销。 这是为什么呢?因为有内存对齐的存在,编译器使用了内存对齐,那么最后的大小结果就会不一样。至于为什么要做对齐,主要考虑下面两个原因:
平台(移植性)
不是所有的硬件平台都能够访问任意地址上的任意数据。例如:特定的硬件平台只允许在特定地址获取特定类型的数据,否则会导致异常情况
性能
若访问未对齐的内存,将会导致 CPU 进行两次内存访问,并且要花费额外的时钟周期来处理对齐及运算。而本身就对齐的内存仅需要一次访问就可以完成读取动作,这显然高效很多,是标准的空间换时间做法。 有的小伙伴可能会认为内存读取,就是一个简单的字节数组摆放。但实际上 CPU 并不会以一个一个字节去读取和写入内存,相反 CPU 读取内存是一块一块读取的,块的大小可以为 2、4、6、8、16 字节等大小,块大小我们称其为内存访问粒度。假设访问粒度为 4,那么 CPU 就会以每 4 个字节大小的访问粒度去读取和写入内存。 在不同平台上的编译器都有自己默认的 “对齐系数”。一般来讲,我们常用的 x86 平台的系数为 4;x86_64 平台系数为 8。需要注意的是,除了这个默认的对齐系数外,还有不同数据类型的对齐系数。数据类型的对齐系数在不同平台上可能会不一致。例如,在 x86_64 平台上,int64 的对齐系数为 8,而在 x86 平台上其对齐系数就是 4。
models/orders/orders.go:15:12: struct of size 544 bytes could be of size 536 bytes (maligned) type Order struct { ^ models/stars/stars.go:15:11: struct of size 256 bytes could be of size 248 bytes (maligned) type Star struct { ^复制代码
可以将结构体中的int32、float64等类型统一,如都统一为64位。
仔细看,T1 存在许多 padding,显然它占据了不少空间。那么也就不难理解,为什么调整结构体内成员变量的字段顺序就能达到缩小结构体占用大小的疑问了,是因为巧妙地减少了 padding 的存在。让它们更 “紧凑” 了。 其实内存对齐除了可以降低内存占用之外,还有一种情况是必须要手动对齐的:在 x86 平台上原子操作 64bit 指针。之所以要强制对齐,是因为在 32bit 平台下进行 64bit 原子操作要求必须 8 字节对齐,否则程序会 panic。详情可以参考 atomic 官方文档(:
Bugs
On x86-32, the 64-bit functions use instructions unavailable before the Pentium MMX. On non-Linux ARM, the 64-bit functions use instructions unavailable before the ARMv6k core. On ARM, x86-32, and 32-bit MIPS, it is the caller’s responsibility to arrange for 64-bit alignment of 64-bit words accessed atomically. The first word in a variable or in an allocated struct, array, or slice can be relied upon to be 64-bit aligned.
作者:goonwalk
链接:https://juejin.cn/post/7026367302819905567