Skip to content

Latest commit

 

History

History
395 lines (287 loc) · 12.3 KB

File metadata and controls

395 lines (287 loc) · 12.3 KB

2.6 Interface

Interface

One of the subtlest design features in Go are interfaces. After reading this section, you will likely be impressed by their implementation.

What is an interface

In short, an interface is a set of methods that we use to define a set of actions.

Like the examples in previous sections, both Student and Employee can SayHi(), but they don't do the same thing.

Let's do some more work. We'll add one more method Sing() to them, along with the BorrowMoney() method to Student and the SpendSalary() method to Employee.

Now, Student has three methods called SayHi(), Sing() and BorrowMoney(), and Employee has SayHi(), Sing() and SpendSalary().

This combination of methods is called an interface and is implemented by both Student and Employee. So, Student and Employee implement the interface: SayHi() and Sing(). At the same time, Employee doesn't implement the interface: SayHi(), Sing(), BorrowMoney(), and Student doesn't implement the interface: SayHi(), Sing(), SpendSalary(). This is because Employee doesn't have the method BorrowMoney() and Student doesn't have the method SpendSalary().

Type of Interface

An interface defines a set of methods, so if a type implements all the methods we say that it implements the interface.

type Human struct {
	name  string
	age   int
	phone string
}

type Student struct {
	Human
	school string
	loan   float32
}

type Employee struct {
	Human
	company string
	money   float32
}

func (h *Human) SayHi() {
	fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

func (h *Human) Sing(lyrics string) {
	fmt.Println("La la, la la la, la la la la la...", lyrics)
}

func (h *Human) Guzzle(beerStein string) {
	fmt.Println("Guzzle Guzzle Guzzle...", beerStein)
}

// Employee overloads Sayhi
func (e *Employee) SayHi() {
	fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
		e.company, e.phone) //Yes you can split into 2 lines here.
}

func (s *Student) BorrowMoney(amount float32) {
	s.loan += amount // (again and again and...)
}

func (e *Employee) SpendSalary(amount float32) {
	e.money -= amount // More vodka please!!! Get me through the day!
}

// define interface
type Men interface {
	SayHi()
	Sing(lyrics string)
	Guzzle(beerStein string)
}

type YoungChap interface {
	SayHi()
	Sing(song string)
	BorrowMoney(amount float32)
}

type ElderlyGent interface {
	SayHi()
	Sing(song string)
	SpendSalary(amount float32)
}

We know that an interface can be implemented by any type, and one type can implement many interfaces simultaneously.

Note that any type implements the empty interface interface{} because it doesn't have any methods and all types have zero methods by default.

Value of interface

So what kind of values can be put in the interface? If we define a variable as a type interface, any type that implements the interface can assigned to this variable.

Like the above example, if we define a variable "m" as interface Men, then any one of Student, Human or Employee can be assigned to "m". So we could have a slice of Men, and any type that implements interface Men can assign to this slice. Be aware however that the slice of interface doesn't have the same behavior as a slice of other types.

package main

import "fmt"

type Human struct {
	name  string
	age   int
	phone string
}

type Student struct {
	Human
	school string
	loan   float32
}

type Employee struct {
	Human
	company string
	money   float32
}

func (h Human) SayHi() {
	fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

func (h Human) Sing(lyrics string) {
	fmt.Println("La la la la...", lyrics)
}

func (e Employee) SayHi() {
	fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
		e.company, e.phone) //Yes you can split into 2 lines here.
}

// Interface Men implemented by Human, Student and Employee
type Men interface {
	SayHi()
	Sing(lyrics string)
}

func main() {
	mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
	paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
	sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
	tom := Employee{Human{"Sam", 36, "444-222-XXX"}, "Things Ltd.", 5000}

	// define interface i
	var i Men

	//i can store Student
	i = mike
	fmt.Println("This is Mike, a Student:")
	i.SayHi()
	i.Sing("November rain")

	//i can store Employee
	i = tom
	fmt.Println("This is Tom, an Employee:")
	i.SayHi()
	i.Sing("Born to be wild")

	// slice of Men
	fmt.Println("Let's use a slice of Men and see what happens")
	x := make([]Men, 3)
	// these three elements are different types but they all implemented interface Men
	x[0], x[1], x[2] = paul, sam, mike

	for _, value := range x {
		value.SayHi()
	}
}

An interface is a set of abstract methods, and can be implemented by non-interface types. It cannot therefore implement itself.

Empty interface

An empty interface is an interface that doesn't contain any methods, so all types implement an empty interface. This fact is very useful when we want to store all types at some point, and is similar to void* in C.

// define a as empty interface
var a interface{}
var i int = 5
s := "Hello world"
// a can store value of any type
a = i
a = s

If a function uses an empty interface as its argument type, it can accept any type; if a function uses empty interface as its return value type, it can return any type.

Method arguments of an interface

Any variable can be used in an interface. So how can we use this feature to pass any type of variable to a function?

For example we use fmt.Println a lot, but have you ever noticed that it can accept any type of argument? Looking at the open source code of fmt, we see the following definition.

type Stringer interface {
		String() string
}

This means any type that implements interface Stringer can be passed to fmt.Println as an argument. Let's prove it.

package main

import (
	"fmt"
	"strconv"
)

type Human struct {
	name  string
	age   int
	phone string
}

// Human implemented fmt.Stringer
func (h Human) String() string {
	return "Name:" + h.name + ", Age:" + strconv.Itoa(h.age) + " years, Contact:" + h.phone
}

func main() {
	Bob := Human{"Bob", 39, "000-7777-XXX"}
	fmt.Println("This Human is : ", Bob)
}

Looking back to the example of Box, you will find that Color implements interface Stringer as well, so we are able to customize the print format. If we don't implement this interface, fmt.Println prints the type with its default format.

fmt.Println("The biggest one is", boxes.BiggestsColor().String())
fmt.Println("The biggest one is", boxes.BiggestsColor())

Attention: If the type implemented the interface error, fmt will call error(), so you don't have to implement Stringer at this point.

Type of variable in an interface

If a variable is the type that implements an interface, we know that any other type that implements the same interface can be assigned to this variable. The question is how can we know the specific type stored in the interface. There are two ways which I will show you.

  • Assertion of Comma-ok pattern

Go has the syntax value, ok := element.(T). This checks to see if the variable is the type that we expect, where "value" is the value of the variable, "ok" is a variable of boolean type, "element" is the interface variable and the T is the type of assertion.

If the element is the type that we expect, ok will be true, false otherwise.

Let's use an example to see more clearly.

package main

import (
	"fmt"
	"strconv"
)

type Element interface{}
type List []Element

type Person struct {
	name string
	age  int
}

func (p Person) String() string {
	return "(name: " + p.name + " - age: 	" + strconv.Itoa(p.age) + " years)"
}

func main() {
	list := make(List, 3)
	list[0] = 1       // an int
	list[1] = "Hello" // a string
	list[2] = Person{"Dennis", 70}

	for index, element := range list {
		if value, ok := element.(int); ok {
			fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
		} else if value, ok := element.(string); ok {
			fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
		} else if value, ok := element.(Person); ok {
			fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
		} else {
			fmt.Printf("list[%d] is of a different type\n", index)
		}
	}
}

It's quite easy to use this pattern, but if we have many types to test, we'd better use switch.

  • switch test

Let's use switch to rewrite the above example.

package main

import (
	"fmt"
	"strconv"
)

type Element interface{}
type List []Element

type Person struct {
	name string
	age  int
}

func (p Person) String() string {
	return "(name: " + p.name + " - age: " + strconv.Itoa(p.age) + " years)"
}

func main() {
	list := make(List, 3)
	list[0] = 1       //an int
	list[1] = "Hello" //a string
	list[2] = Person{"Dennis", 70}

	for index, element := range list {
		switch value := element.(type) {
		case int:
			fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
		case string:
			fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
		case Person:
			fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
		default:
			fmt.Println("list[%d] is of a different type", index)
		}
	}
}

One thing you should remember is that element.(type) cannot be used outside of the switch body, which means in that case you have to use the comma-ok pattern .

Embedded interfaces

The most beautiful thing is that Go has a lot of built-in logic syntax, such as anonymous fields in struct. Not suprisingly, we can use interfaces as anonymous fields as well, but we call them Embedded interfaces. Here, we follow the same rules as anonymous fields. More specifically, if an interface has another interface embedded within it, it will behave as if it has all the methods that the embedded interface has.

We can see that the source file in container/heap has the following definition:

type Interface interface {
    	sort.Interface // embedded sort.Interface
    	Push(x interface{}) //a Push method to push elements into the heap
    	Pop() interface{} //a Pop method that pops elements from the heap
}

We see that sort.Interface is an embedded interface, so the above Interface has the three methods contained within the sort.Interface implicitly.

type Interface interface {
    	// Len is the number of elements in the collection.
    	Len() int
    	// Less returns whether the element with index i should sort
    	// before the element with index j.
    	Less(i, j int) bool
    	// Swap swaps the elements with indexes i and j.
    	Swap(i, j int)
}

Another example is the io.ReadWriter in package io.

// io.ReadWriter
type ReadWriter interface {
		Reader
		Writer
}

Reflection

Reflection in Go is used for determining information at runtime. We use the reflect package, and this official article explains how reflect works in Go.

There are three steps involved when using reflect. First, we need to convert an interface to reflect types (reflect.Type or reflect.Value, this depends on the situation).

t := reflect.TypeOf(i)    // get meta-data in type i, and use t to get all elements
v := reflect.ValueOf(i)   // get actual value in type i, and use v to change its value

After that, we can convert the reflected types to get the values that we need.

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())

Finally, if we want to change the values of the reflected types, we need to make it modifiable. As discussed earlier, there is a difference between pass by value and pass by reference. The following code will not compile.

var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)

Instead, we must use the following code to change the values from reflect types.

var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(7.1)

We have just discussed the basics of reflection, however you must practice more in order to understand more.

Links