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}
In a gRPC interceptor, I wanted to add a check to determine if the server’s response is nil to avoid subsequent panic
when accessing fields (I didn’t want to check every time). Initially, I thought of using reply == nil
to determine this, but it turned out to be false??? After a quick search, I found the following information:
Underlying Structure of Interface Values
- Type Information (Type): Stores the dynamic type of the interface.
- Value Information (Value): Stores the dynamic value of the interface.
When the dynamic value of an interface variable is nil
, if its dynamic type is not nil
, then the interface{} == nil
check will be false
. In the above code, reply
has a specific implementation type, so interface != nil
. In actual development, when the return value of an interface
type is clearly nil, you should directly return nil
, rather than an uninitialized empty pointer of a specific implementation structure.
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}
I modified the code to use the above method to determine if the interface
is nil
, and it usually works, but isNil(reply)
is still false??? Attempting to print it directly didn’t help, and after formatting, I found that the originally defined reply
with two fields actually contains five fields. The custom fields are indeed nil, but there are three additional fields that I didn’t know about.
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}
Looking at the .pb.go
file, you can see the complete structure. The first three are unexported fields, and when printed, they are not pointer types, so they cannot be directly checked using the isNil
method.
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 // Skip internal fields
11 if fieldName == "state" || fieldName == "sizeCache" || fieldName == "unknownFields" {
12 continue
13 }
14 // Check if pointer type fields are nil
15 if field.Kind() == reflect.Ptr && field.IsNil() {
16 return true
17 }
18 }
19
20 return false
21}
The current approach is to skip these three fields. I’m not sure if there’s a better way.