判断 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 区别
new 的行为
作用:
new
用于为指定类型分配内存,并返回指向该类型零值的指针。切片内存分配:
当使用
new([]T)
时,会为切片类型[]T
分配内存,并返回一个指向切片零值的指针。切片的零值是一个零值切片,即:
len
和cap
均为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)
make 的行为
作用:
make
用于创建并初始化切片(Slice)、映射(map)或通道(channel)。对于切片,make
会分配内存并返回一个初始化后的切片(非指针)。切片内存分配:
当使用
make([]T, len, cap)
时,会为切片分配内存,并初始化切片的len
和cap
。底层数组会被分配,且所有元素初始化为零值。
如果未指定
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
分配内存,元素初始化为零值 使用场景 需要指针时(较少见) 直接创建切片时