rss resume / curriculum vitae linkedin linkedin gitlab github twitter mastodon instagram
Learning Go: Interface Types - Part 2
Jul 16, 2021

Disclaimer: This post includes Amazon affiliate links. If you click on one of them and you make a purchase I’ll earn a commission. Please notice your final price is not affected at all by using those links.

In this post I will continue discussing Interface Types, specifically the empty interface, how to deal with nil interfaces, as well as type assertions and type switches.



What is in an interface value?

The code used for this post is available on Github.

An Interface Value is internally defined by 2 values, a tuple of:

  • A value and,
  • A concrete type.

For example, the following code:

type Interface interface {
	Method()
}

type String struct {
	Value string
}
func (s *String) Method() {}

type Integer int
func (i Integer) Method() {}

Defines 3 types:

  • Interface which is an interface type, and
  • 2 types, String and Integer, that implement the interface type Interface because both define a method called Method.

When we declare a variable of type Interface the internal representation of that value will change depending on what is assigned to it:

var iface Interface

iface = &String{"hello world"}
fmt.Printf("Value: %v, Type: %T\n", iface, iface)

iface = Integer(100)
fmt.Printf("Value: %v, Type: %T\n", iface, iface)

The code will print out the following, indicating the values stored internally in the variable iface:

Value: &{hello world}, Type: *main.String
Value: 100, Type: main.Integer

Understanding how Interface Values work is important in cases where we have the need to compare those variables to nil. When comparing to nil both, the value and concrete type, must be nil for it to be considered true.

This is covered in the official Frequently Asked Questions and it can be explained with the following snippet:

func main() {
	iface := AlwaysNonNil()

	fmt.Printf("Value: %v, Type: %T, Is nil? %t\n", iface, iface, iface == nil)
}

func AlwaysNonNil() Interface {
	var ret *String

	return ret
}

Which prints the following:

Value: <nil>, Type: *main.String, Is nil? false

This is because the returned value in AlwaysNonNil is assigned to a concrete type, in this case *String, however the returned type is using the Interface type therefore that field in the returned value is already assigned which in the leads to making it harder to compare to nil.

A much more realistic example of this issue would be the one mentioned by the FAQ when returning errors:

func returnsError() error {
	var err *MyError
	// some other code
	return ret
}

What is the empty interface (interface{})?

The empty interface, written as interface{}, is an interface type:

  • That specifies zero methods,
  • It may hold any values of any type and,
  • It’s used by code that handles unknown types.

Some examples in the standard library of using interface{} are:

func Println(a ...interface{}) (n int, err error)

That receives a variadic list of argument in a of interface{} types.

func Marshal(v interface{}) ([]byte, error)
type Scanner interface {
	Scan(src interface{}) error
}

Using Type Assertions and Type Switches

When working with Empty Interfaces there are situations where we need to consider the concrete type to execute some logic, in those cases there are two ways to convert the Interface Type into a Concrete Type, depending on our needs we may need to use one or the other.

Let’s consider the following:

type Interface interface {
	Method()
}

type Integer int
func (i Integer) Method() {}

In order Type Assert from one to another we use the syntax (variable name).(type to assert to):

var iface interface{} = Integer(100)

t, ok := iface.(Integer)
fmt.Printf("OK? %t, Value %v, Type %T\n", ok, t, t)

iface = "hello"

t, ok = iface.(Integer)
fmt.Printf("OK? %t, Value %v, Type %T\n", ok, t, t)

Which prints out:

OK? true, Value 100, Type main.Integer
OK? false, Value 0, Type main.Integer

First line indicates the expected assertion while the second one indicates the assertion that did not work. This is because in the first Type Assertion, the value assigned to the variable iface matches the type Integer. The assertion statement returns the asserted type as well as a variable indicated if it worked or not, named in the snippet above as ok, it’s important to always assert types this way otherwise failed assertions could lead to our program to panic:

var iface interface{} = Integer(100)

t := iface.(Integer)
fmt.Printf("Value %v, Type %T\n", t, t)

iface = "hello"

t = iface.(Integer) // XXX: Panic
fmt.Printf("Value %v, Type %T\n", t, t)

The output would be similar to:

Value 100, Type main.Integer
panic: interface conversion: interface {} is string, not main.Integer

goroutine 1 [running]:
main.main()
	/tmp/sandbox3926933851/prog.go:21 +0xa8

Another way to do what we did above is by using Type Switches, the idea is similar to the normal switch but this time the case is used to assert to the types defined:

func describe(i interface{}) {
	switch v := i.(type) {
	case Integer:
		fmt.Printf("int %d\n", v)
	case string:
		fmt.Printf("string %s\n", v)
	default:
		fmt.Printf("unknown %T - %v\n", v, v)
	}
}

If we call that function in main:

describe("hello")
describe(Integer(100))
describe(10)

We get:

string hello
int 100
unknown int - 10

This is useful in cases were multiple types implement an Interface Type and we need to define similar logic in a centralized way, the most common example would be when using the Error interface and we may return different messages to our customers depending on the error type.

Conclusion

With this post we complete covering everything related to the Interface Types in Go, please refer to the Recommended Reading links below for more interesting posts to learn more things about Go.


Back to posts