Methods and Interfaces¶
An interesting feature of Go is its use of interfaces. They are designed to provide many of the same benefits of object-oriented programming, but without the need for explicit classes or type inheritance as in C++ or Java.
Methods¶
Before we can discuss interfaces, we need to look at Go methods. Consider this code:
type Rectangle struct {
width, height float64
}
func (r Rectangle) area() float64 { // a method
return r.width * r.height
}
func (r Rectangle) perimeter() float64 { // a method
return 2 * (r.width + r.height)
}
// main is a function
func main() {
r := Rectangle{width:5, height:3}
fmt.Printf("dimensions of r: width=%d, height=%d\n", r.width, r.height)
fmt.Printf(" area of r: %.2f\n", r.area())
fmt.Printf(" perimeter of r: %.2f\n", r.perimeter())
}
Look at the signatures for area()
and perimeter()
. The extra parameter
in brackets before their names is called the method receiver, i.e. r
is the receiver in perimeter()
. The receiver is essentially an argument
with some special privileges.
In main
, dot-notation is used for calling methods, e.g. r.area()
calls
the area
method on r
.
Method receivers can, if the programmer desires, be passed as pointers to a
type. For example, here r
is passed a pointer because inflate
modifies
the object r
points to:
func (r *Rectangle) inflate(scale float64) {
r.width *= scale
r.height *= scale
}
Without the *
, r
would be passed by value, meaning that the code in
the body of inflate
would modify this copy instead of the original object
that calls the method.
Note that there is no change in the syntax for how the fields of r
are
accessed: the regular dot-notation is used. Similarly, inflate
is called
the same way, e.g. r.inflate(2.2)
. There is no special ->
operator as
in C/C++.
An important concept in Go is that of method sets. The Go language spec defines method sets like this:
A type may have a method set associated with it. The method set of an interface type is its interface. The method set of any other type
T
consists of all methods declared with receiver typeT
. The method set of the corresponding pointer type*T
is the set of all methods declared with receiver*T
orT
(that is, it also contains the method set ofT
). Further rules apply to structs containing embedded fields, as described in the section on struct types. Any other type has an empty method set. …The method set of a type determines the interfaces that the type implements and the methods that can be called using a receiver of that type.
So, in our example above, the method set of the type Rectangle
is:
{area
, perimeter
}, and the method set of *Rectangle
is: {area
,
perimeter
, inflate
}.
Calling a method is defined like this in the Go spec:
A method callx.m()
is valid if the method set of (the type of)x
containsm
and the argument list can be assigned to the parameter list ofm
. Ifx
is addressable and&x
’s method set containsm
,x.m()
is shorthand for(&x).m()
….
Interfaces¶
Here’s an example of a Go interface:
type Shaper interface {
area() float64
perimeter() float64
}
The name of this interface is Shaper
, and it lists the signatures of the
two methods that are necessary to satisfy it.
A type T
implements Shaper
if Shaper
’s method set, {“area”,
“perimeter”},is a subset of the method set for T
. We saw above that the
method set for Rectangle
is {area
, perimeter
}, and the methods
listed in Shaper
are clearly a subset of this. Similarly, Rectangle*
also implements Shape
because the methods in Shape
are a subset of the
method set of Rectangle*
, {area
, perimeter
, inflate
}.
Importantly, only the signatures of the methods are listed in the interface. The bodies of the functions are not mentioned at all. The implementation is not part of the interface.
As another example, suppose we add this code:
type Circle struct {
radius float64
}
func (c Circle) area() float64 { // a method
return 3.14 * c.radius * c.radius
}
func (c Circle) perimeter() float64 { // a method
return 2 * 3.14 * c.radius
}
func (c Circle) diameter() float64 { // a method
return 2 * c.radius
}
The method set of Circle
is {area
, perimeter
, diameter
}, and
so it implements Shaper
because it contains both area
and
perimeter
. The method set of *Circle
is also {area
, perimeter
,
diameter
}, and so *Circle
also implements Shaper
.
Now we can write code that works on any object of a type that implements
Shaper
. For example:
func printShape(s Shaper) {
fmt.Printf(" area: %.2f\n", s.area())
fmt.Printf(" perimeter: %.2f\n", s.perimeter())
}
All that printShape
knows about s
is that the methods in the method
set for Shaper
can be called on it. If s
has methods not in the member
set for Shaper
, then those methods cannot be called on s
.
Here’s how you could use printShape
:
func main() {
r := Rectangle{width:5, height:3}
fmt.Println("Rectangle ...")
printShape(r)
c := Circle{radius:5}
fmt.Println("\nCircle ...")
printShape(c)
}
An interesting detail about Go interfaces is that you don’t need to
explicitly tell Go that a struct
implements an interface: the compiler
figures it out for itself. This contrasts with, for example, Java, where you
must explicitly indicate when a class implements an interface.
In summary:
- The method set of an interface is all the methods named in the interface.
- The method set of a type
T
(whereT
is not a pointer type) is all methods with a receiver of typeT
. - The method set of a type
*T
(whereT
is not a pointer type) is all methods with a receiver of type*T
orT
. - A type
T
implements an interfaceI
if the method set ofI
is a subset of the method set ofT
.
Example: Using the Sort Interface¶
Lets see how we can use the standard Go sorting package.
Suppose we want to sort records of people. In practice, such records might contain lots of information, such as a person’s name, address, email address, relatives, etc. But for simplicity we will use this struct:
type Person struct {
name string
age int
}
For efficiency, lets sort a slice of pointers to Person
objects; this will
avoid moving and copying the name
strings. To help with this, we define
the type People
:
type People []*Person // slice of pointers to People objects
It turns out that it’s essential that we create the type People
. The code
we write below won’t compile if we use []*Person
directly. That’s because
the method set of []*Person
does not satisfy sort.Interface
. So we
will add methods on People
that make it implement sort.Interface
.
For convenience, here’s a String
method that prints a People
object:
func (people People) String() (result string) {
for _, p := range people {
result += fmt.Sprintf("%s, %d\n", p.name, p.age)
}
return result
}
The name of this is String()
, and it returns a string
object. A method
with this signature implements the fmt.Stringer
interface, which allows it
to be called by the print functions in fmt
.
Now we can write code like this:
users := People{
{"Mary", 34},
{"Lou", 3},
{"Greedo", 77},
{"Zippy", 50},
{"Morla", 62},
}
fmt.Printf("%v\n", users) // calls the People String method
To sort the items in the users
slice, we must create the methods listed in
sort.Interface
:
type Interface interface {
// number of elements in the collection
Len() int
// returns true iff the element with index i should come
// before the element with index j
Less(i, j int) bool
// swaps the elements with indexes i and j
Swap(i, j int)
}
This interface is pre-defined in the sort
package. Notice that this is a
very general interface. It does not even assume that you will be sorting
slices or arrays!
Three methods are needed:
func (p People) Len() int {
return len(p)
}
func (p People) Less(i, j int) bool {
return p[i].age > p[j].age
}
func (p People) Swap(i, j int) {
p[i], p[j] = p[j], p[i]
}
Less
is the function that controls the order in which the objects will be
sorted. By examining Less
you can see that we will be sorting people by
age, from oldest to youngest.
With these functions written, we can now sort users
like this:
users := People{
{"Mary", 34},
{"Lou", 3},
{"Greedo", 77},
{"Zippy", 50},
{"Morla", 62},
}
fmt.Printf("%v\n", users)
sort.Sort(users)
fmt.Printf("%v\n", users)
To change the sort order, modify Less
. For instance, this will sort
users
alphabetically by name:
func (p People) Less(i, j int) bool {
return p[i].name < p[j].name
}
Another way to sort by different orders is shown in the examples section of the Go sort package documentation. The trick there is to create a new type for every different sort order.
Questions¶
- How would you explain the difference between functions and methods to a programmer who is not familiar with Go?
- What is a method receiver? How many different receivers can a method have?
- When should a method receiver be a pointer type?
- What is a method set? How are method sets determined for regular types, pointer types, and interface types?
- How would you use the
sort
package to sort a slice of pointers to ints?