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设置了默认路径:https://proxy.golang.org,direct。但是由于某些因素,这个使用不了,你懂的。所以七牛云为中国的gopher提供了一个免费合法的代理goproxy.cn,其已经开源。只需一条简单命令就可以使用该代理:

go env -w GOPROXY=https://goproxy.cn,direct

原文链接:https://blog.csdn.net/qq_31930499/article/details/101108056

记住不要在go mod所在项目的目录中执行go get命令,否则会将其添加为该项目依赖放进go mod文件中,而不是编译二进制文件到go bin目录下。当需要使用go get命令安装响应的包时,应该在GOPATH路径下使用该命令。

GOPATH三个目录文件

$GOPATH 目录约定有三个子目录:

src 存放源代码(比如:.go .c .h .s等)
pkg 编译后生成的文件(比如:.a)
bin 编译后生成的可执行文件(为了方便,可以把此目录加入到 $PATH 变量中)

原文链接:https://blog.csdn.net/shangsongwww/article/details/89680101

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:https://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版本一致,否则可能会报错,可以参考文章https://blog.csdn.net/woailuo626/article/details/101377654

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选项

参考:https://blog.csdn.net/benben_2015/article/details/89643090
https://blog.csdn.net/linux_Allen/article/details/90262517

使用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.

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

love666666shen

谢谢您的鼓励!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值