摘要

最近开发过程中遇到挺多空指针引发的Coredump,想在CI过程中集成一下Go的代码检查工具,但实际发现这些工具的效果都不行……

一、测试代码

简单写了一些空指针代码,如下所示:

package main

import (
    "fmt"
    "reflect"
    "testGolint/context" // 加载这个库时,就会调用其init(),所以其内部打印在main()之前
    "time"
)

type Address struct {
    Home  string
    Email string
}

type User struct {
    Name    string
    Address *Address
}

func testCase0() {
    fmt.Println("Test Case 0 Begin..")
    var a = User{}

    address := a.Address
    if address == nil {
        fmt.Println(*address) // nil dereference
    }

    fmt.Printf("Email Address = [%s] \n", address.Email) // 典型的空指针,查不出来

    fmt.Println("Test Case 0 Finish..")
}

func testCase1() {
    fmt.Println("Test Case 1 Begin..")
    var a = &context.Context{}
    var user = a.UserAllocate()
    fmt.Printf("User Name %s \n", user.Name)

    var company = user.WorkExps[0]
    fmt.Printf("User Company Name %s \n", company.CompanyName) // 典型的空指针,查不出来

    fmt.Println("Test Case 1 Finish..")
}

func testCase2() {
    fmt.Println("Test Case 2 Begin..")
    var a *context.Context

    go func() {
        fmt.Println("Before a.")
        a = &context.Context{}
        fmt.Println("After a")
    }()
    //time.Sleep(1 * time.Second) // 加上这句就不会dump

    go func() {
        fmt.Println("Before UserInfo.")
        a.UserInfo() // 这行很可能运行在a = &context.Context{}之前,会触发空指针dump
        fmt.Println("After UserInfo.")
    }()

    fmt.Println("Test Case 2 Finish..")
    time.Sleep(3 * time.Second)
}

func IsNil(i interface{}) bool {
    vi := reflect.ValueOf(i)
    if vi.Kind() == reflect.Ptr {
        return vi.IsNil()
    }
    return false
}

type Worker interface {
    String() string
}

type Person struct {
    Age  uint8
    Name string
    Sex  string
}

type Employee struct {
    Person
}

type Employer struct {
    Title string
    Person
}

func (p *Person) String() string {
    return fmt.Sprintf("Name: %s, Sex: %s, Age: %d", p.Name, p.Sex, p.Age)
}

func (er *Employer) String() string {
    return fmt.Sprintf("%s, Title: %s", er.Person.String(), er.Title)
}

func testCase3() {
    var (
        w          Worker
        emptyPoint *Employee
    )

    w = &Employer{
        Title: "Title",
        Person: Person{
            Name: "Tom",
            Age:  50,
            Sex:  "male",
        },
    }

    if IsNil(w) {
        fmt.Printf("interface w is nil")
        return
    }

    // **bad** 是类型断言问题
    e := w.(*Employee)
    fmt.Println(e.String())

    // ok
    if e, ok := w.(*Employee); ok {
        fmt.Printf("It's Employee, %s\n", e.String())
    } else if e, ok := w.(*Employer); ok {
        fmt.Printf("It's Employer, %s\n", e.String())
    }

    // **bad** 在嵌入式结构上定义方法时在nil指针上调用方法
    fmt.Println(emptyPoint.String())
}

func main() {
    fmt.Println("Test Begin..")

    testCase0()
    testCase1()
    testCase2()
    testCase3()

    fmt.Println("Test Finish..")

}

开测!

二、Golangci-lint

名气最大的一个工具Golangci-lint。修改.golangci.yml,放开所有的限制,运行后发现核心的问题没查出来,只有一些格式问题:

golangci-lint.png

三、Nilness

Nilness是官方的一个专门检查空指针的工具,但运行后发现只能查几种简单的空指针场景:

nilness.png

四、Staticcheck

Staticcheck,依然不行:

staticcheck.png

五、Gocritic

Gocritic,不行:

gocritic1.png

gocritic2.png