判断 interface 是否为nil

 1func interceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
 2	start := time.Now()
 3	err := invoker(ctx, method, req, reply, cc, opts...)
 4	logger.Infof("[gRPC] method=%s req=%v rep=%+v duration=%s error=%v", method, req, reply, time.Since(start), err)
 5	if err != nil {
 6		return err
 7	}
 8
 9	return nil
10}

gRPC 拦截器中想添加对服务端响应结果是否为空的判断,避免后续取字段的 panic(不想每次判断)。原想直接使用 reply == nil 来判断,但是竟然是 false ???简单搜索后得知以下信息:

接口值的底层结构

  • 类型信息(Type):存储接口的动态类型。
  • 值信息(Value):存储接口的动态值。

当一个接口变量的动态值为 nil 时,如果它的动态类型不是 nil,那么 interface{} == nil 的判断会是 false。上文中的 reply 是有明确的实现类型的,因此 interface != nil。在实际开发中,当 interface 类型的返回值已经明确为nil时,应该直接返回 nil,而不是具体实现结构的未赋值空指针。

1import "reflect"
2
3func isNil(i interface{}) bool {
4    if i == nil {
5        return true
6    }
7    v := reflect.ValueOf(i)
8    return v.Kind() == reflect.Ptr && v.IsNil()
9}

修改为使用上述方法来判断 interface 是否为 nil,通常来说成功了,但是 isNil(reply) 仍然是 false???尝试直接打印无果,格式化后发现原本定义了两个字段的 reply,竟然包含五个字段,自定义字段确实是nil,但是还有三个我不知道的字段。

1type XxxInfo struct {
2	state         protoimpl.MessageState
3	sizeCache     protoimpl.SizeCache
4	unknownFields protoimpl.UnknownFields
5
6	Id              int32   `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
7	Type            int32   `protobuf:"varint,2,opt,name=type,proto3" json:"type,omitempty"`
8}

查看 .pb.go 文件可以看到完整结构,前三个为未导出的字段,打印出来均不是指针类型,因此不能直接通过isNil方法来判断。

 1func isCustomFieldNil(v interface{}) bool {
 2	if v == nil {
 3		return true
 4	}
 5	val := reflect.ValueOf(v).Elem()
 6	for i := range val.NumField() {
 7		field := val.Field(i)
 8		fieldName := val.Type().Field(i).Name
 9		logger.Infof("fieldName: %s, value: %v, type: %v", fieldName, field, field.Type())
10		// 跳过内部字段
11		if fieldName == "state" || fieldName == "sizeCache" || fieldName == "unknownFields" {
12			continue
13		}
14		// 检查指针类型字段是否为nil
15		if field.Kind() == reflect.Ptr && field.IsNil() {
16			return true
17		}
18	}
19
20	return false
21}

当前做法是跳过了这三个字段,不知道有没有更好的办法。

var、new、make 的区别

判断切片长度是否为0

最初 VSCode 对我的一行代码有个提示,代码内容大致如下

1info := new([]Info)
2if info == nil || len(*info) == 0 {
3    ...
4}

在第二个 if 语句提示内容为:

impossible condition: non-nil == nilnilnesscond

意思是说,不可能的条件,info 不可能是 nil,只需要判断 len(*info) == 0

在这段代码中,info 是通过 new([]Info) 创建的,是一个指向切片(slice)的指针,指针指向的是一个初始化的切片,切片的底层数据 nil(零值切片),即 *info 的初始值是 nil

从这之后,我误以为判断一个切片是否为空的时候只需要判断 len(*info) == 0,但是如下代码却发生报错

panic: runtime error: invalid memory address or nil pointer dereference

1var info *[]Info
2if len(*info) == 0 {
3	return nil
4}

var info *[]Info 仅仅声明了一个指向切片的指针,但未分配内存,此时 info 的值是 nil, len(*info) 会尝试对 info 进行解引用,*info 就会引发 panic。

声明方式内存分配指针是否 nil底层切片状态len(*info) 是否安全
new([]Info)分配内存,返回指向零值切片的指针零值切片(底层数组为 nil)安全(返回 0)
var info *[]Info仅声明指针,未分配内存指针本身为 nil触发 panic
new 和 make 区别
  1. new 的行为

    • 作用:new 用于为指定类型分配内存,并返回指向该类型零值的指针。

    • 切片内存分配:

      • 当使用 new([]T) 时,会为切片类型 []T 分配内存,并返回一个指向切片零值的指针。

      • 切片的零值是一个零值切片,即:

        • lencap 均为 0
        • 底层数组为 nil
      • 示例:

        1info := new([]int)  // info 是一个指针,指向零值的 []int
        2fmt.Println(info)   // 输出 &[]
        3fmt.Println(*info)  // 输出 []
        4fmt.Println(len(*info))  // 输出 0
        5fmt.Println(cap(*info))  // 输出 0
        6fmt.Println(*info == nil)  // 输出 false(切片本身非 nil,但底层数组为 nil)
        
  2. make 的行为

    • 作用:make 用于创建并初始化切片(Slice)、映射(map)或通道(channel)。对于切片,make 会分配内存并返回一个初始化后的切片(非指针)。

    • 切片内存分配:

      • 当使用 make([]T, len, cap) 时,会为切片分配内存,并初始化切片的 lencap

      • 底层数组会被分配,且所有元素初始化为零值。

      • 如果未指定 cap,则默认 cap 等于 len

    • 示例:

      1info := make([]int, 2, 5)  // 初始化一个长度为 2、容量为 5 的切片
      2fmt.Println(info)          // 输出 [0 0]
      3fmt.Println(len(info))     // 输出 2
      4fmt.Println(cap(info))     // 输出 5
      5fmt.Println(info == nil)   // 输出 false
      
    操作new([]T)make([]T, len, cap)
    返回值指向零值切片的指针(*[]T初始化后的切片([]T
    底层数组nil分配内存,元素初始化为零值
    使用场景需要指针时(较少见)直接创建切片时