In Golang, there are primarily two ways to handle encapsulation. These are structures and interfaces. Interfaces define method sets available for a struct. This helps with features like Polymorphism in object oriented programming languages.
- Interfaces specify method sets and is used to create modularity in Go programming language.
- It is like a blueprint for a method set. This is defined so that it can be implemented by other types like Structures.
- Interfaces only provide method signatures for each method of a method set. They do not implement these methods. These methods are then implemented by other types.
- It cannot have any variable declarations. They can only have method signatures.
Defining Interfaces
Interface is defined in Golang using below syntax.
1type <interface_name> interface {
2 // method signatures
3}
**Example:
1type Account interface {
2 withdrawMoney(int amount) bool
3 depositMoney(int amount) bool
4 getBalance() float64
5}
Interface Example using Shape
Interfaces are defined so that other types can implement them. In order to implement an interface, a type has to implement all of interface’s methods. There is no implements keyword like Java or C#. These are implemented implicitly and does not use specific keyword to implement an interface. This is how we can implement features like polymorphism from other object oriented programming languages.
Example:
1package main
2
3import "fmt"
4
5const PI float64 = 3.14
6
7type Shape interface {
8 area() float64
9 perimeter() float64
10}
11
12type Square struct {
13 side float64
14}
15
16func (s Square) area() float64 {
17 return s.side * s.side
18}
19
20func (s Square) perimeter() float64 {
21 return 4 * s.side
22}
23
24type Circle struct {
25 radius float64
26}
27
28func (c Circle) area() float64 {
29 return PI * c.radius * c.radius
30}
31
32func (c Circle) perimeter() float64 {
33 return 2 * PI * c.radius
34}
35
36func main() {
37 square := Square{side: 2}
38 circle := Circle{radius: 2}
39 printDetails(circle)
40 printDetails(square)
41 var shape Shape
42 shape = &square
43 fmt.Println(shape.area())
44
45 shape = &circle
46 fmt.Println(shape.area())
47}
48
49func printDetails(s Shape) {
50 fmt.Printf("Area = %.2f, Perimeter = %.2f\n", s.area(), s.perimeter())
51}
In this case, we define interface of type Shape. Further, we define two structs of type Circle and Square. Both of them have methods defined area and perimeter. These methods calculate correct values based on type of the shape. Here Square and Circle implicitly implements Shape interface. We do not use any explicit keyword for this implementing this. Something else to note here is that we can have method with same name as long as the receiver is different. This is similar to concept of method overloading in OOP languages.
Output:
Area = 12.56, Perimeter = 12.56
Area = 4.00, Perimeter = 8.00
4
12.56
Interface types: Static vs Dynamic
Another important point to note with interfaces is that, we can define a variable of type Shape. Once the variable is created, the variable can have any type that implements methods defined in this interface. For example, in above code, we used area() methods of Circle and Square both from shape variable we created.
In Go, there are two types of interfaces: static and dynamic. Static interface is the type of the interface itself. In above example, static type if interface Shape is Shape. The dynamic type, on the other hand, is the type that implements the interface which were Square and Circle. Internally, an interface is represented by a tuple which represents the dynamic type of the interface and the value of the dynamic type.
1package main
2
3import "fmt"
4
5const PI float64 = 3.14
6
7type Shape interface {
8 area() float64
9 perimeter() float64
10}
11
12type Square struct {
13 side float64
14}
15
16func (s Square) area() float64 {
17 return s.side * s.side
18}
19
20func (s Square) perimeter() float64 {
21 return 4 * s.side
22}
23
24type Circle struct {
25 radius float64
26}
27
28func (c Circle) area() float64 {
29 return PI * c.radius * c.radius
30}
31
32func (c Circle) perimeter() float64 {
33 return 2 * PI * c.radius
34}
35
36func main() {
37 square := Square{side: 2}
38 circle := Circle{radius: 2}
39 var shape Shape
40 shape = &square
41 fmt.Printf("shape type: %T and value: %+v\n", shape, shape)
42
43 shape = &circle
44 fmt.Printf("shape type: %T and value: %+v\n", shape, shape)
45}
Above code shows the internal type and value of shape variable. Initially it was pointer to Square and then it changed to pointer of Circle.
shape type: *main.Square and value: &{side:2}
shape type: *main.Circle and value: &{radius:2}
Empty Interface
In Go, empty interface is an interface without any method signature. By default, all types in Go implement the empty interface known as interface {}. This also includes types like nil and every other built-in types.
1import "fmt"
2
3func main() {
4
5 var i interface{}
6 fmt.Println(i)
7 fmt.Printf("i type: %T and value %+v\n", i, i)
8}
<nil>
i type: <nil> and value <nil>
Implementing Multiple Interfaces
In Go, a type can implement multiple interfaces. Again, we don’t have to explicitly say this type implements these interfaces. We simply define methods defined in the interfaces that we want to implement.
1package main
2
3import (
4 "fmt"
5)
6
7const PI float64 = 3.14
8
9type PrintDetails interface {
10 describe()
11}
12
13type Shape interface {
14 area() float64
15 perimeter() float64
16}
17
18type Square struct {
19 side float64
20}
21
22func (s *Square) area() float64 {
23 return s.side * s.side
24}
25
26func (s *Square) perimeter() float64 {
27 return 4 * s.side
28}
29
30func (s *Square) describe() {
31 fmt.Printf("The square with side: %.2f has area: %.2f and perimeter: %.2f\n", s.side, s.area(), s.perimeter())
32}
33
34func main() {
35 s := Square{side: 5}
36 var details PrintDetails
37 details = &s
38 details.describe()
39
40 var shape Shape
41 shape = &s
42 fmt.Println("Area: ", shape.area())
43}
In this code, we have two interfaces PrintDetails and Shape. The Shape interface is similar to earlier. However, in this code, we have additional method defined for describe() with receiver of Square type. That means Square implements PrintDetails interface. Now, we can create variable of PrintDetails type and call describe() method on it. We can also call area() method on Shape type variable.
Conditionals on Interface Types
In Go, switch statement supports checking against various data types. We can use it to check the underlying interface type. This is illustrated in below code example.
1package main
2
3import (
4 "fmt"
5)
6
7type Animal interface {
8 move()
9}
10
11type Human struct {
12}
13
14type Bird struct {
15}
16
17func (b *Bird) move() {
18 fmt.Println("Bird is flying...")
19}
20
21func (h *Human) move() {
22 fmt.Println("Human is walking...")
23}
24
25func TypeCheck(i interface{}) {
26 switch i.(type) {
27 case *Human:
28 fmt.Println("This is a human")
29 case *Bird:
30 fmt.Println("This is a bird")
31 default:
32 fmt.Println("Unknown type")
33 }
34}
35
36func main() {
37 h := Human{}
38 var animal Animal
39 TypeCheck(animal)
40 h.move()
41
42 animal = &h
43 TypeCheck(animal)
44 animal.move()
45
46 b := Bird{}
47 animal = &b
48 TypeCheck(animal)
49 animal.move()
50}
As you can see in above code, we can pass an interface {} which is implemented by all types to make compiler happy. In code, we can pass any type into this TypeCheck() function. The output looks like below.
Unknown type
Human is walking...
This is a human
Human is walking...
This is a bird
Bird is flying...
Embedded Interfaces: Inheritance
In OOP languages, we could have multi-layer inheritance where it has chain of inheritance hierarchy. Go supports a similar idea using embedded interfaces. You define an interface using the definition of other existing interfaces. This promotes code reuse.
1package main
2
3import (
4 "fmt"
5)
6
7type WalkingAnimal interface {
8 walk()
9}
10
11type FlyingAnimal interface {
12 fly()
13}
14
15type SwimmingAnimal interface {
16 swim()
17}
18
19type WalkingAndSwimmingAnimal interface {
20 WalkingAnimal
21 SwimmingAnimal
22}
23
24type Crocodile struct {
25 name string
26 tails int
27 eyes int
28}
29
30func (c *Crocodile) walk() {
31 fmt.Println("Crocodile is walking")
32}
33
34func (c *Crocodile) swim() {
35 fmt.Println("Crocodile is swimming")
36}
37
38func main() {
39 crocodile := Crocodile{name: "Hungry Crocodile", eyes: 2, tails: 1}
40 var walkAndSwim WalkingAndSwimmingAnimal
41 walkAndSwim = &crocodile
42 walkAndSwim.swim()
43 walkAndSwim.walk()
44}
In this case, we have two interfaces WalkingAnimal and SwimmingAnimal. Now, mostly animal can either walk or swim, but few animals can do both. In those cases, we can use existing interfaces to derive a new interface. In this case, we created WalkingAndSwimmingAnimal interface using above two interfaces. This way this interface has methods which were defined in the first two. This is similar to inheritance in object-oriented programming languages.


Comments