This page looks best with JavaScript enabled

Let's Learn Go -- (6) Interface

 ·  ☕ 5 min read

1. Interface-Oriented Programming

1.1 Characteristics

Interface-oriented programming emphasizes interaction between modules through interfaces. First, the caller specifies a set of method signatures; then, the callee implements that set of methods.

What sets interface programming apart from other programming styles is that it focuses on methods, not attributes. In many programming scenarios, methods are defined around attributes. As shown below:

But in interface programming it is the exact opposite: methods sit at the core, while attributes can be defined freely and extended. The same interface is implemented on different data structures. As shown below:

In addition, the wording itself shows that interface-oriented programming is an interface-centric programming paradigm.

1.2 Advantages

  • Module decoupling

Through interface abstraction, the dependencies between modules are made explicit. The boundaries between modules are clearer, and each module is more independent.

  • Maintainability

An interface hides the concrete implementation; as long as each module keeps the interface unchanged, its internal logic can be freely refactored.

  • Easy to extend, easy to upgrade

As long as the same set of methods is implemented, a module can be extended or upgraded very conveniently.

  • Easy to test

When writing unit tests, external dependencies need to be masked, and an interface is very easy to mock. Code written in an interface-oriented style is much easier to unit test.

2. Interface in Go

An interface in Go is a type that aggregates a set of methods.

2.1 Declaration and Implementation

You can declare an interface with the interface keyword plus a set of method signatures. A method signature means all the information that uniquely identifies a method: its name, parameters, return values, and so on. Here is the declaration of an Animal interface:

1
2
3
type Animal interface{
	Call()
}

Interfaces in Go are implemented implicitly. You do not need to specify which interface is being inherited; as long as some type in the same package implements all the methods of the interface, that type is said to implement the interface. In the example below, the Cat type implements the Animal interface:

1
2
3
4
5
type Cat struct {}

func (c Cat) Call() {
	// do something
}

When using it, you can declare a variable of type Animal, point it at a Cat, and then call the methods of the interface.

1
2
3
var i Animal
i = &Cat{}
i.Call()

2.2 Receiver Types

Go allows methods to be defined on a type, and the Receiver refers to the type that the method accepts — for example, Cat in the example above.

In the example above, both i = &Cat{} and i = Cat{} work; Go performs the conversion automatically. The exception is when the Receiver is a pointer type but the assignment is a value type. Because Go uses value passing, after the assignment the address obtained points to the address of the copy, not the address of the intended data, and the compiler reports an error:

1
	Cat does not implement Animal (Call method has pointer receiver)

Built-in types cannot be used as a Receiver. Receivers come in two kinds, value and pointer, and the two can also be mixed.

  • Value Receiver
1
2
3
4
5
6
7
type Dog struct {
	times int
}

func (d Dog) Call() {
	d.times = d.times + 1 // not work
}
  • Pointer Receiver
1
2
3
4
5
6
7
type Dog struct {
	times int
}

func (d *Dog) Call() {
	d.times = d.times + 1 // work
}

When implementing an interface you will face the choice between these two kinds. Passing a pointer seems more efficient, but it also means the operation is more dangerous. In scenarios where the data does not need to be modified, a value Receiver should be used whenever possible.

2.3 The Empty Interface

What is special about the empty interface is that interface{} can accept a value of any type. This feature looks like something only a dynamic language would have, but Go is a statically typed language. At compile time, Go validates interfaces strictly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import "fmt"

func main() {
	var i interface{}
	i = 123
	fmt.Println(i)
	i = "123"
	fmt.Println(i)
}

The empty interface is very useful when accepting or returning parameters of an uncertain type. But an uncertain type also brings maintenance cost. In a project, we should avoid using the empty interface as much as possible to keep the project more maintainable.

2.4 Type Determination

Since interface{} can accept a value of any type, during program execution it is quite likely that you need to know the dynamic value type of the interface. There are two ways to determine the type:

  • Assertion
1
2
3
4
5
6
7
var i interface{}
i = Cat{}
if _, ok := i.(Cat); ok {
	fmt.Println("i is Cat ")
} else {
	fmt.Println("i is not Cat ")
}
  • switch
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var i interface{}
i = Dog{}
switch i.(type) {
case Cat:
	fmt.Println("i is Cat")
case Dog:
	fmt.Println("i is Dog")
default:
	fmt.Println("i is unknow")
}

Note that here, when the Cat and Dog values are type-checked against Animal, the result is true.

3. Composition of Interfaces

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
type Active interface{
	Eat()
}

type Animal interface{
	Active
	Call()
}

type Cat struct {}

func (c Cat) Call() {}

func (c *Cat) Eat() {}

An interface can also nest other interfaces. A type that implements this interface must provide all the methods defined by every interface. This technique is very useful in projects.


微信公众号
WRITTEN BY
微信公众号