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.