Go Language Comparison of interface and nil

  • 2020-06-07 04:40:26
  • OfStack

interface profile

Go is known for its simplicity, its syntax is very simple, familiar with C++, Java developers need only a short time to master the basic usage of Go.

interface is a very important feature provided in the Go language. One or more functions can be defined in one interface. For example, the definition of io. ReadWriter provided by the system is as follows:


type ReadWriter interface {
  Read(b []byte) (n int, err error)
  Write(b []byte) (n int, err error)
}

Any type that provides binding function implementations for Read and Write is considered to implement interface (ES23en-ES24en), unlike Java, which requires developers to use implements.

However, interface of Go has a special pit in its usage, which is something to be avoided when comparing the value of an interface type to that of nil.

1 real pit

This is an actual bug we encountered in the development of the GoWorld distributed game server. Since GoWorld supports a variety of different databases (including MongoDB, Redis, etc.) to store server-side objects, GoWorld provides a unified object storage interface definition on the upper layer, while different object database implementations only need to implement functions provided by THE EntityStorage interface.


// EntityStorage defines the interface of entity storage backends
type EntityStorage interface {
 List(typeName string) ([]common.EntityID, error)
 Write(typeName string, entityID common.EntityID, data interface{}) error
 Read(typeName string, entityID common.EntityID) (interface{}, error)
 Exists(typeName string, entityID common.EntityID) (bool, error)
 Close()
 IsEOF(err error) bool
}

Take an example of an implementation that USES Redis as the object database. The function OpenRedis connects to the Redis database and eventually returns a pointer to an redisEntityStorage object.


// OpenRedis opens redis as entity storage
func OpenRedis(url string, dbindex int) *redisEntityStorage {
 c, err := redis.DialURL(url)
 if err != nil {
 return nil
 }

 if dbindex >= 0 {
 if _, err := c.Do("SELECT", dbindex); err != nil {
  return nil
 }
 }

 es := &redisEntityStorage{
 c: c,
 }

 return es
}

In the upper logic, we use the OpenRedis function to connect to the Redis database and assign the returned redisEntityStorage pointer to one of the EntityStorage interface variables, because the redisEntityStorage object implements all the functions defined by the EntityStorage interface.


var storageEngine StorageEngine //  This is a 1 Global variables 
storageEngine = OpenRedis(cfg.Url, dbindex)
if storageEngine != nil {
  //  The connection is successful 
  ...
} else {
  //  The connection fails 
  ...
}

The above code looks normal. OpenRedis returns nil if the connection to THE Redis database fails, and the caller compares the return value to nil to determine whether the connection was successful. This is one of the few pits in the Go language, because whether or not the OpenRedis function connects to Redis successfully, it runs the logic of connecting successfully.

Find the problem

To understand this, you first need to understand the nature of the interface{} variable. In the Go language, a variable of type interface{} contains two Pointers, one to the type of value and one to the actual value. We can verify this with the following test code.


// InterfaceStructure  Defines the 1 a interface{} Internal structure 
type InterfaceStructure struct {
 pt uintptr //  Pointer to the value type 
 pv uintptr //  A pointer to the content of the value 
}

// asInterfaceStructure  will 1 a interface{} convert InterfaceStructure
func asInterfaceStructure (i interface{}) InterfaceStructure {
 return *(*InterfaceStructure)(unsafe.Pointer(&i))
}

func TestInterfaceStructure(t *testing.T) {
 var i1, i2 interface{}
 var v1 int = 0x0AAAAAAAAAAAAAAA
 var v2 int = 0x0BBBBBBBBBBBBBBB
 i1 = v1
 i2 = v2
 fmt.Printf("sizeof interface{} = %d\n", unsafe.Sizeof(i1))
 fmt.Printf("i1 %x %+v\n", i1, asInterfaceStructure(i1))
 fmt.Printf("i2 %x %+v\n", i2, asInterfaceStructure(i2))
 var nilInterface interface{}
 fmt.Printf("nil interface = %+v\n", asInterfaceStructure(nilInterface))
}

The output of this code is as follows:


sizeof interface{} = 16
i1 aaaaaaaaaaaaaaa {pt:5328736 pv:825741282816}
i2 bbbbbbbbbbbbbbb {pt:5328736 pv:825741282824}
nil interface = {pt:0 pv:0}

So for an nil variable of type interface{}, both its Pointers are 0. This conforms to the standard definition of nil in the Go language. In Go, nil is zero (Zero Value), while in languages like Java, null is actually a null pointer. I'm not going to expand it any more about the difference between null and null Pointers.

When we assign a value of a specific type to a variable of type interface, we assign both the type and the value to both Pointers in interface. If the value of the specific type is nil, the interface variable still stores the corresponding type and value Pointers.


func TestAssignInterfaceNil(t *testing.T) {
 var p *int = nil
 var i interface{} = p
 fmt.Printf("%v %+v is nil %v\n", i, asInterfaceStructure(i), i == nil)
}

Enter as follows:


<nil> {pt:5300576 pv:0} is nil false

As you can see, in this case, although we assign an nil value to interface{}, there is still a pointer to the type in interface, so comparing this interface variable with the nil constant returns false.

How to solve this problem

To avoid this Go pit, avoid assigning a value that might be a specific type of nil to the interface variable. In the case of OpenRedis above, one approach is to first check the results returned by OpenRedis for non-ES127en, and then assign the interface variable, as shown below.


var storageEngine StorageEngine //  This is a 1 Global variables 
redis := OpenRedis(cfg.Url, dbindex)
if redis != nil {
  //  The connection is successful 
  storageEngine = redis //  determine redis not nil And then assign it to interface variable 
} else {
  //  The connection fails 
  ...
}

Another option is to have the OpenRedis function return the value of the EntityStorage interface type directly, so that the return value of OpenRedis can be assigned to the EntityStorage interface variable directly and correctly.


// OpenRedis opens redis as entity storage
func OpenRedis(url string, dbindex int) EntityStorage {
 c, err := redis.DialURL(url)
 if err != nil {
 return nil
 }

 if dbindex >= 0 {
 if _, err := c.Do("SELECT", dbindex); err != nil {
  return nil
 }
 }

 es := &redisEntityStorage{
 c: c,
 }

 return es
}

As to which method is better, that is a matter of opinion. I hope you don't step on the pit in the actual project, even if you do, you can jump out quickly!


Related articles: