build, code, linux

Mesos, Modules, and Travis-CI

When my manager asked me to badge the Docker Volume Driver Isolator Module for Mesos (DVDI) project with a Travis-CI build status I thought “Sure, no problem.”

Hoo boy, little did I know…

Continue reading

Advertisements
code

Golang Equality & Type Aliases

The go language specification states:

In any comparison, the first operand must be assignable to the type of the second operand, or vice versa.

In other words only identical types are comparable. Take the following code:

package main

import "fmt"

func main() {
	var x int32 = 3
	var y int64 = 3
	fmt.Printf("x == y is %v\n", x == y)
}

Attempting to compile the above program (goplay) will result in an error:

prog.go:8: invalid operation: x == y (mismatched types int32 and int64)

The compilation fails because x and y are of types int32 and int64 respectively. However, what if the addresses of x and y are compared instead?

package main

import "fmt"

func main() {
	var x int32 = 3
	var y int64 = 3
	
	var px *int32 = &x
	var py *int64 = &y
	
	fmt.Printf("px == py is %v\n", px == py)
}

Once again, using goplay, compilation fails:

prog.go:12: invalid operation: px == py (mismatched types *int32 and *int64)

The code fails to compile because golang’s type system is composed of two basic types – named and unnamed. A named type is something like string or int32. An unnamed type is any named type with a type literal applied to it. For example, the type *int32 is the named type int32 with the * (pointer) type literal applied.

So the reason the above example fails when comparing pointers is because golang doesn’t consider the pointer, a type literal, when deciding whether types are identical and therefore comparable. Only the named, or underlying, types are considered. In the case of *int32 and *int64 that would be int32 and int64.

For what it’s worth, the above code, had it compiled, would have printed the following:

px == py is false

The reason the comparison is false is because the addresses of the integer values are being compared, not the values themselves. The example was simply to show that the compilation itself fails due to the way golang looks at the underlying type when deciding whether two types are comparable.

So what does all this have to with type aliases? Well, let’s look at another example:

package main

import "fmt"

type Int32 int32

func main() {
	var x int32 = 3
	var y Int32 = 3
	
	fmt.Printf("x == y is %v\n", x == y)
}

In golang, an aliased type is more than an alias, it’s an entirely new type. That is why the above example fails to compile (goplay) with the following error:

prog.go:11: invalid operation: x == y (mismatched types int32 and Int32)

Just like the previous comparison of an int32 and int64, the types int32 and Int32 are different, and thus not comparable.

However, what happens if type literals, such as pointers, are thrown back into the mix?

package main

import "fmt"

type pInt32 *int32

func main() {
	var x int32 = 3
	var y int32 = 3
	
	var px *int32 = &x
	var py pInt32 = &y
	
	fmt.Printf("x == y is %v\n", *px == *py)
	fmt.Printf("px == py is %v\n", px == py)
}

Executing the above program with goplay produces the following output:

x == y is true
px == py is false

Success! The compilation succeeded and the program produced the expected results (please note that px != py, and that’s expected as x and y are not pointing to the same address).

Although to be fair, the very fact that py was assignable to the address of y dictated that the program would compile before it reached the heretofore problematic fmt.Printf... line. And that’s exactly why the program compiled — because of the fact that py was assignable to the address of y. As the go language specification states:

A value x is assignable to a variable of type T if:

  • x‘s type is identical to T.
  • x‘s type V and T have identical underlying types…

Which aligns with the tl;dr at the top of this post – only identical types are comparable.

It’s just that in golang, what makes one type identical to the next may not be so readily obvious.

Once the idea of how type aliases fit into equality with regards to go, they become quite useful when redefining/aliasing types from external libraries or versioned packages. Let’s use this source file from EMC{code}’s XtremIO adapter as an example:

package goxtremio

import xms "github.com/emccode/goxtremio/api/v3"

type Initiator *xms.Initiator
type InitiatorOptions xms.PostInitiatorsReq
type NewInitiatorResult *xms.PostInitiatorsResp

//GetInitiator returns a specific initiator by name or ID
func (c *Client) GetInitiator(id string, name string) (Initiator, error) {
	i, err := c.api.GetInitiator(id, name)
	if err != nil {
		return nil, err
	}
	
	return i.Content, nil
}

//GetInitiators returns a list of initiators
func (c *Client) GetInitiators() ([]*Ref, error) {
	i, err := c.api.GetInitiators()
	if err != nil {
		return nil, err
	}
	return ToRefArray(i.Initiators), nil
}

//NewInitiator creates a volume
func (c *Client) NewInitiator(opts *InitiatorOptions) (NewInitiatorResult, error) {
	pireq := xms.PostInitiatorsReq(*opts)
	pires, err := c.api.PostInitiators(&pireq)
	if err != nil {
		return nil, err
	}
	
	nir := NewInitiatorResult(pires)
	return nir, nil
}

//DeleteInitiator deletes a volume
func (c *Client) DeleteInitiator(id string, name string) error {
	return c.api.DeleteInitiators(id, name)
}

The above source imports the github.com/emccode/goxtremio/api/v3 as xms and then aliases the following types:

type Initiator *xms.Initiator
type InitiatorOptions xms.PostInitiatorsReq
type NewInitiatorResult *xms.PostInitiatorsResp

This enables the ability to mask internal, versioned types with ones specifically designed to be exported.

Note that the type InitiatorOptions is not an alias to a pointer, but to a named type. The reason for this is that the function c.api.PostInitiators requires a pointer to an xms.PostInitiatorsReq structure, and it’s not possible to instantiate a structure from an alias to its pointer. Instead an alias to the actual named type is created in order for developers to be able to create new instances of it the usual way:

opts := &InitiatorOptions{}

Yet, as was discussed earlier, it’s not possible to point to the same address with two different named types. It *is* possible, however, to seamlessly create a copy of that data into an instance of the desired type:

pireq := xms.PostInitiatorsReq(*opts)
pires, err := c.api.PostInitiators(&pireq)

The first line above takes the structure to which *opts points, a NewInitiatorOptions, and casts it as a xms.PostInitiatorsReq. Because the two types are different a new xms.PostInitiatorsReq is created. This does represent new memory allocation, but since the majority of this package’s abstracted functions take no parameters or only primitive ones, it’s not a major issue.

Still, how do we know that the above method of casting a value to a new type really does just that? That the data survives intact? Well, here’s a short and sweet go program to prove it:

package main

import "fmt"

type ExternalSubType struct {
  NickName string
}

type ExternalType struct {
  Name string
  Data *ExternalSubType
}

type MyTypeAlias ExternalType
type MyTypePtrAlias *ExternalType

func main() {
  v0 := ExternalType{"Andrew", &ExternalSubType{"Idjit"}}
  v1 := &v0
  v2 := MyTypeAlias(v0)
  v3 := MyTypePtrAlias(&v0)
  v4 := MyTypePtrAlias(v1)

  fmt.Printf("v0 %-11[1]p %-20[1]T %[1]v\n", &v0)
  fmt.Printf("v1 %-11[1]p %-20[1]T %[1]v\n", v1)
  fmt.Printf("v2 %-11[1]p %-20[1]T %[1]v\n", &v2)
  fmt.Printf("v3 %-11[1]p %-20[1]T %[1]v\n", v3)
  fmt.Printf("v4 %-11[1]p %-20[1]T %[1]v\n", v4)
  
  // Change the value of the Name field for the
  // structure. All variables that refer to this
  // instance will reflect this change.
  v0.Name = "werdnA"
  fmt.Printf("\nv0.Name = \"%s\"\n\n", v0.Name)
  
  fmt.Printf("v0 %-11[1]p %-20[1]T %[1]v\n", &v0)
  fmt.Printf("v1 %-11[1]p %-20[1]T %[1]v\n", v1)
  fmt.Printf("v2 %-11[1]p %-20[1]T %[1]v\n", &v2)
  fmt.Printf("v3 %-11[1]p %-20[1]T %[1]v\n", v3)
  fmt.Printf("v4 %-11[1]p %-20[1]T %[1]v\n", v4)
}

Running the above program will produce output similar to the following:

v0 0x104382e0  *main.ExternalType   &{Andrew 0x1040a130}
v1 0x104382e0  *main.ExternalType   &{Andrew 0x1040a130}
v2 0x104382f0  *main.MyTypeAlias    &{Andrew 0x1040a130}
v3 0x104382e0  main.MyTypePtrAlias  &{Andrew 0x1040a130}
v4 0x104382e0  main.MyTypePtrAlias  &{Andrew 0x1040a130}

v0.Name = "werdnA"

v0 0x104382e0  *main.ExternalType   &{werdnA 0x1040a130}
v1 0x104382e0  *main.ExternalType   &{werdnA 0x1040a130}
v2 0x104382f0  *main.MyTypeAlias    &{Andrew 0x1040a130}
v3 0x104382e0  main.MyTypePtrAlias  &{werdnA 0x1040a130}
v4 0x104382e0  main.MyTypePtrAlias  &{werdnA 0x1040a130}

v0, v1, v2, v3, and v4 are all equal with regards to their fields’ data, but v2 is not the same as the others. Let’s look at the variables’ types:

  • v0 –  ExternalType
  • v1*ExternalType
  • v2 –  MyTypeAlias
  • v3 –  MyTypePtrAlias
  • v4 –  MyTypePtrAlias

v2 is the only variable with an underlying type that is not ExternalType. v1 is just a pointer to a ExternalType value, thus the underlying type is ExternalType.

v3 and v4 may have a type of MyTypePtrAlias, but that type is actually an alias for *ExternalType, which has an underlying type of ExternalType.

That is why the above output shows that while all of the variables, including v2, have a value of &{Andrew <0x&ExternalSubType>}, only the variables v0, v1, v3, and v4 share the same memory address.

The point of this demonstration is to show that it’s possible in golang to alias types and use those aliases to refer to the same memory addresses of other types in a type-safe manner. However, the aliases should be defined as to the pointer of another type, not to the concrete type itself.