6. Objects¶
Program to an interface, not an implementation. – Design Patterns by Gang of Four
In the chapter on functions, we have also learned about methods. A method is a function associated with a type, more specifically a concrete type. As you know, an object is an instance of a type. In general, methods define the behavior of an object.
Interfaces in Go provide a formal way to specify the behavior of an object. In layman’s terms, Interface is like a blueprint which describes an object. So, Interface is considered as an abstract type, commonly referred to as interface type.
Small interfaces with one or two methods are common in Go.
Here is a Geometry
interface which defines two methods:
type Geometry interface {
Area() float64
Perimeter() float64
}
If any type satisfy this interface - that is define these two methods which returns float64 - then, we can say that type is implementing this interface. One difference with many other languages with interface support and Go is that, in Go implementing an interface happens implicitly. So, no need to explicitly declare a type is implementing a particular interface.
To understand this idea, consider this Rectangle
struct type:
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2*(r.width + r.height)
}
As you can see above, the above Rectangle type has two methods named
Area and Perimeter which returns float64. So, we can say Rectangle is
implementing the Geometry
interface. To elaborate the example
further, we will create one more implementation:
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14 * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * 3.14 * c.Radius
}
Now we have two separate implementations for the Geometry
interface.
So, if anywhere the Geometry
interface type is expected, you can use
any of these implementations.
Let’s define a function which accepts a Geometry
type and prints
area and perimeter.
func Measure(g Geometry) {
fmt.Println("Area:", g.Area())
fmt.Println("Perimeter:", g.Perimeter())
}
When you call the above function, you can pass the argument as an object
of Geometry
interface type. Since both Rectangle
and Circle
satisfy that interface, you can use either one of them.
Here is a code which call Measure
function with Rectangle
and
Circle
objects:
r := Rectangle{Width: 2.5, Height: 4.0}
c := Circle{Radius: 6.5}
Measure(r)
Measure(c)
6.1. Type with Multiple Interfaces¶
In Go, a type can implement more than one interface. If a type has methods that satisfy different interfaces, we can say that that type is implementing those interfaces.
Consider this interface:
type Stringer interface {
String() string
}
In previous section, there was a Rectangle type declared with two methods. In the same package, if you declare one more method like below, it makes that type implementing Stringer interface in addition to the Geometry interface.
func (r Rectangle) String() string {
return fmt.Sprintf("Rectangle %vx%v", r.Width * r.Height)
}
Now the Rectangle
type conforms to both Geometry
interface and
Stringer
interface.
6.2. Empty Interface¶
The empty interface is the interface type that has no methods. Normally
the empty interface will be used in the literal form: interface
. All
types satisfy empty interface. A function which accepts empty interface,
can receive any type as the argument. Here is an example:
func blackHole(v interface{}) {
}
blackHole(1)
blackHole("Hello")
blackHole(struct{})
In the above code, the blackHole
functions accepts an empty
interface. So, when you are calling the function, any type of argument
can be passed.
The Println
function in the fmt
package is variadic function
which accepts empty interfaces. This is how the function signature looks
like:
func Println(a ...interface{}) (n int, err error) {
Since the Println
accepts empty interfaces, you could pass any type
arguments like this:
fmt.Println(1, "Hello", struct{})
6.3. Pointer Receiver¶
In the chapter on Functions, you have seen that the methods can use a pointer receiver. Also we understood that the pointer receivers are required when the object attributes need be to modified or when passing large size data.
Consider the implementation of Stringer
interface here:
type Temperature struct {
Value float64
Location string
}
func (t *Temperature) String() string {
o := fmt.Sprintf("Temp: %.2f Loc: %s", t.Value, t.Location)
return o
}
In the above example, the String
method is implemented using a
pointer receiver. Now if you define a function which accepts the
fmt.Stringer
interface, and want the Temperature
object, it
should be a pointer to Temperature
.
func cityTemperature(v fmt.Stringer) {
fmt.Println(v.String())
}
func main() {
v := Temperature{35.6, "Bangalore"}
cityTemperature(&v)
}
As you can see, the cityTemperature
function is called with a
pointer. If you modify the above code and pass normal value, you will
get an error. The below code will produce an error as pointer is not
passed.
func main() {
v := Temperature{35.6, "Bangalore"}
cityTemperature(v)
}
The error message will be something like this:
cannot use v (type Temperature) as type fmt.Stringer in argument to
cityTemperature: Temperature does not implement fmt.Stringer (String
method has pointer receiver)
6.4. Type Assertions¶
In some cases, you may want to access the underlying concrete value from the interface value. Let’s say you define a function which accepts an interface value and want access attribute of the concrete value. Consider this example:
type Geometry interface {
Area() float64
Perimeter() float64
}
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2*(r.width + r.height)
}
func Measure(g Geometry) {
fmt.Println("Area:", g.Area())
fmt.Println("Perimeter:", g.Perimeter())
}
In the above example, if you want to print the width and and height from
the Measure
function, you can use type assertions.
Type assertion gives the underlying concrete value of an interface type. In the above example, you can access the rectangle object like this:
r := g.(Rectangle)
fmt.Println("Width:", r.Width)
fmt.Println("Height:", r.Height)
If the assertion fail, it will trigger a panic.
Type assertion has an alternate syntax where it will not panic if
assertion fail, but gives one more return value of boolean type. The
second return value will be true
if assertion succeeds otherwise it
will give false
.
r, ok := g.(Rectangle)
if ok {
fmt.Println("Width:", r.Width)
fmt.Println("Height:", r.Height)
}
If there are many types that need to be asserted like this, Go provides a type switches which is explained in the next section.
6.5. Type Switches¶
As you have seen in the previous section, type assertions gives access
to the underlying value. But if there any many assertions need to be
made, there will be lots if
blocks. To avoid this, Go provides type
switches.
switch v := g.(type) {
case Rectangle:
fmt.Println("Width:", v.Width)
fmt.Println("Height:", v.Height)
case Circle:
fmt.Println("Width:", v.Radius)
case default:
fmt.Println("Unknown:")
}
In the above example, type assertion is used with switch cases. Based on
the type of g
, the case is executed.
Note that the fallthrough statement does not work in type switch.
6.6. Exercises¶
Exercise 1: Using the Marshaller
interface, make the marshalled
output of the Person
object given here all in upper case.
type Person struct {
Name string
Place string
}
Solution:
1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "strings"
7)
8
9// Person represents a person
10type Person struct {
11 Name string
12 Place string
13}
14
15// MarshalJSON implements the Marshaller interface
16func (p Person) MarshalJSON() ([]byte, error) {
17 name := strings.ToUpper(p.Name)
18 place := strings.ToUpper(p.Place)
19 s := []byte(`{"NAME":"` + name + `","PLACE":"` + place + `"}`)
20 return s, nil
21}
22
23func main() {
24 p := Person{Name: "Baiju", Place: "Bangalore"}
25 o, err := json.Marshal(p)
26 if err != nil {
27 panic(err)
28 }
29 fmt.Println(string(o))
30}
6.6.1. Additional Exercises¶
Answers to these additional exercises are given in the Appendix A.
Problem 1: Implement the built-in error
interface for a custom
data type. This is how the error
interface is defined:
type error interface {
Error() string
}
6.7. Summary¶
This chapter explained the concept of interfaces and their uses. Interfaces are an important concept in Go. Understanding interfaces and using them properly makes the design robust. The chapter covered the empty interface, pointer receivers, and type assertions and type switches.
Brief summary of key concepts introduced in this chapter:
An interface is a set of methods that a type must implement. A type that implements an interface can be used anywhere an interface is expected. This allows for greater flexibility and reusability in Go code.
A pointer receiver is a method that takes a pointer to a struct as its receiver. Pointer receivers are often used to modify the state of a struct.
A type assertion is a way of checking the type of a value at runtime. Type assertions can be used to ensure that a value is of a certain type before using it.
A type switch is a control flow statement that allows for different code to be executed based on the type of a value. Type switches can be used to make code more robust and easier to read.