CHAPTER 8
As programmers, we often must represent real-world objects in code, and being able to store all the information we need about a given “thing” in a discrete entity, along with the operations we need to perform on that information, is very helpful. This is the essence of object-oriented programming, a paradigm based on the concept of “objects” that store data (state) in the form of fields, and code (behavior) that can access and manipulates that data in the form of methods.
Go supports types, methods, and interfaces that enable us to work in an object-oriented fashion. You might argue that Go isn’t strictly an object-oriented language in that it does not directly support a type hierarchy, but it goes a long way towards implementing many of the features you will find in a true object-oriented language.
Before we dive into how Go implements all of this, we need to discuss pointers. The reason for doing so will become apparent as we move forward.
If you’ve never used a language like C or C++, the concept of a pointer might be unfamiliar to you. In programming parlance, a pointer contains the memory address of a variable.
Here is how we define a pointer to a variable of type int:
var p *int
And this is how we assign the memory address of the variable q to pointer p:
q = &i
To display the pointer’s underlying value, use the * (indirection or de-referencing) operator:
*p = 123 // De-reference pointer p to get i and assign a value to it
fmt.Println(*p) // Print the value of i by dereferencing p
The usage of pointers is demonstrated in the following example:
Code Listing 28
package main import "fmt" func main() { a, b := 20, 71 // pointer to a p := &a // read a via the pointer fmt.Printf("Value of a is: %d\n", *p) // set a via the pointer *p = 21 // display the new value of a fmt.Printf("New value of a is: %d\n", a) // pointer to b p = &b // read b via the pointer fmt.Printf("Value of b is: %d\n", *p) // add 10 to b via its pointer *p = *p + 10 // display the new value of b fmt.Printf("New value of b is: %d\n", b) } |
Value of a is: 20
New value of a is: 21
Value of b is: 71
New value of b is: 81
Go doesn’t use the word “object.” What you might think of as an object in another language is a struct in Go.
To define a struct, use the type keyword followed by an identifier and then the struct keyword. Add any fields you want to describe the struct’s state.
The following example defines a structure for a rectangle called rect, sets initial values for its height and width properties, and then displays them to the user.
Code Listing 29
package main import ( "fmt" ) type rect struct { height int width int } func main() { r := rect{height: 12, width: 20} fmt.Printf("Height: %d\nWidth: %d\n", r.height, r.width) } |
Height: 12
Width: 20
Note how we access the fields on the struct using dot notation in the form:
struct_name.field
What this is really doing behind the scenes is de-referencing the pointer to the struct for us and then accessing its field, so if we really wanted to, we could write:
(*struct_name).field
If you don’t explicitly assign values to a field, Go automatically assigns the default value for the data type.
Note: As we saw in Chapter 3, if you capitalize the first letter of a variable name, that variable becomes global. The same concept applies to fields within structures. Other programming languages use keywords like public, private, package, protected, and so on to allow the developer to define the visibility and accessibility of variables within different contexts. Go's approach is nothing short of genius, in my humble opinion!
We used the following syntax in Code Listing 29 to define the struct and initialize its fields:
r := rect{height: 12, width: 20}
To get a pointer to the actual structure data in memory, use the & operator:
myRectangle := &rect{
height: 12,
width: 20,
}
If we know the order the fields are defined in, we can assign values to those fields in the following way:
r := rect{12, 20}
We can also assign field values using dot notation:
r.height = 12
r.width = 20
To create an instance of a structure and return a pointer to it, use the following syntax:
var myRectangle *rect
myRectangle = new(rect)
This is quite verbose, so Go provides a shorthand:
myRectangle := new(rect)
We create methods in Go by defining a function and then binding it to a struct. We need to decide whether our method needs to act on the actual structure itself or just a copy of it. For example, if our method needs to change the values of fields within the structure, we would need access to the original structure because if we worked on a copy, the original field values would be unaffected by any change. Another good reason for using a pointer is in case the structure is large and would therefore be expensive to copy.
Let’s create a method that calculates the area of our rectangle and binds it to the rect structure:
Code Listing 30
package main import ( "fmt" ) type rect struct { height int width int } func main() { r := rect{height: 12, width: 20} fmt.Printf("Height: %d\n", r.height) fmt.Printf("Width: %d\n", r.width) fmt.Printf("Area: %d\n", r.area()) } func (r rect) area() int { h := r.height w := r.width return h * w } |
Height: 12
Width: 20
Area: 240
The binding occurs by specifying the receiver of the method in the parentheses immediately following the func keyword:
func (r rect) area() int {
// Code to calculate the area
}
In this instance, we don’t need to modify the field values, so we are working on a copy of the structure rather than the original.
Let’s create another function called double() that doubles the dimensions of our rectangle.
Code Listing 31
package main import ( "fmt" ) type rect struct { height int width int } func main() { r := rect{height: 12, width: 20} fmt.Printf("Height: %d\n", r.height) fmt.Printf("Width: %d\n", r.width) fmt.Printf("Area: %d\n", r.area())
r.double() fmt.Printf("Height: %d\n", r.height) fmt.Printf("Width: %d\n", r.width) fmt.Printf("Area: %d\n", r.area())
} func (r rect) area() int { h := r.height w := r.width return h * w } func (r rect) double() { r.height *= 2 r.width *=2 } |
Height: 12
Width: 20
Area: 240
Double it!
Height: 12
Width: 20
Area: 240
That’s not what we intended! In this case, the fact that we are working on a copy of the structure and not the original renders our function virtually useless. Let’s amend the double() method so that the receiver is a pointer to the original rect structure.
Code Listing 32
package main import ( "fmt" ) type rect struct { height int width int } func main() { r := rect{height: 12, width: 20} fmt.Printf("Height: %d\n", r.height) fmt.Printf("Width: %d\n", r.width) fmt.Printf("Area: %d\n", r.area())
fmt.Printf("\nDouble it!\n\n") r.double() fmt.Printf("Height: %d\n", r.height) fmt.Printf("Width: %d\n", r.width) fmt.Printf("Area: %d\n", r.area())
} func (r rect) area() int { h := r.height w := r.width return h * w } func (r *rect) double() { r.height *= 2 r.width *=2 } |
Height: 12
Width: 20
Area: 240
Double it!
Height: 24
Width: 40
Area: 960
The double() method is now working as intended.
Types can contain other types. When you embed one type in another, the embedded type is anonymous: you don’t give it a name. Instead, you refer to it via the name of its type.
For example, here is a type that describes a sale discount:
type Discount struct {
percent float32
promotionId string
}
Here is another type that reflects a special discount with extra off the discounted sale price:
type ManagersSpecial struct {
Discount // The embedded type
extraoff float32
}
We can instantiate these types as follows:
januarySale := Discount{15.00, "January"}
managerSpecial := ManagersSpecial{januarySale, 10.00}
We can then refer to the fields on the embedded type using the type name as follows:
managerSpecial.Discount.percent // "15.00
managerSpecial.Discount.promotionId // "January"
If we bind a method to Discount, we can invoke it using the same syntax:
managerSpecial.Discount.someMethod(someParameter)
The following sample shows this concept in action:
Code Listing 33
package main import ( "fmt" ) type Discount struct { percent float32 promotionId string } type ManagersSpecial struct { Discount extraoff float32 } func main() { normalPrice := float32(99.99) januarySale := Discount{15.00, "January"} managerSpecial := ManagersSpecial{januarySale, 10.00} discountedPrice := januarySale.Calculate(normalPrice) managerDiscount := managerSpecial.Calculate(normalPrice) fmt.Printf("Original price: $%4.2f\n", normalPrice) fmt.Printf("Discount percentage: %2.2f\n", januarySale.percent) fmt.Printf("Discounted price: $%4.2f\n", discountedPrice) fmt.Printf("Manager's special: $%4.2f\n", managerDiscount) } func (d Discount) Calculate(originalPrice float32) float32 { return originalPrice - (originalPrice / 100 * d.percent) } func (ms ManagersSpecial) Calculate(originalPrice float32) float32 { return ms.Discount.Calculate(originalPrice) - ms.extraoff } |
Original price: $99.99
Discount percentage: 15.00
Discounted price: $84.99
Manager's special: $74.99
A Go interface is a contract that defines the methods that any type using the interface must implement and expose.
For example, say we have defined types for different vehicles: a car, a bicycle, and a motorcycle. Each of these vehicles can accelerate, decelerate, and brake. The actual details of how each of these actions is achieved is the responsibility of the type, but the fact that they must all accommodate these behaviors can be enforced by an interface.
The benefit of this approach is that once a number of types implement the same interface, they can all be regarded as being equivalent in some way. To continue our example, even though we have cars, cycles, and motorcycles, if our program simply wants to drive them, we can use the same drive() function, pass in any vehicle, and drive() will call the appropriate type-specific accelerate(), decelerate(), and brake() methods.
This is really useful, but is hard to explain without a concrete example, so let’s create a very simple (and rather contrived) example to demonstrate the concept.
I’m going to define two types: one for animals and one for people. The Animal class has a sound field, and the Person class has a name field. Both types need to be able to say hello. The person will introduce him or herself, and the animal will make an appropriate noise depending on what type of animal it is.
In order to enforce this behavior, I’m going to require that both types have a method called sayHi(). I can do this by defining an interface called Greeter that specifies the method signature of sayHi(). Any type that implements the interface must have a sayHi() method with the exact same signature.
type Greeter interface {
sayHi() string
}
type Person struct {
Name string
}
type Animal struct {
Sound string
}
func (p Person) SayHi() string {
return "Hello! My name is " + p.Name
}
func (a Animal) SayHi() string {
return a.Sound
}
I can then create a function called greet() that accepts any type that implements the Greeter interface (a person or an animal) and calls the appropriate sayHi() method for the type:
func greet(i Greeter) {
fmt.Println(i.SayHi())
}
Even though we’re supplying different types, the greet() function can deal with all of them. This is extremely powerful.
Here is the full code and output:
Code Listing 34
package main import ( "fmt" ) type Person struct { Name string } type Animal struct { Sound string } type Greeter interface { SayHi() string } // this method is specific to the Person class func (p Person) SayHi() string { return "Hello! My name is " + p.Name } // this method is specific to the Animal class func (a Animal) SayHi() string { return a.Sound } /* this method can be called on any type that implements the Greeter interface */ func greet(i Greeter) { fmt.Println(i.SayHi()) } func main() { man := Person{Name: "Bob Smith"} dog := Animal{Sound: "Woof! Woof!"} fmt.Println("\nPerson : ") greet(man) fmt.Println("\nAnimal : ") greet(dog) } |
Person :
Hello! My name is Bob Smith
Animal :
Woof! Woof!
The empty interface looks like this:
interface {}
It’s an interface that all types implement—basically because there is nothing to implement! Other languages like .NET and Java refer to this type of interface as a “marker interface.”
This might seem like nonsense, but it can actually be very useful, and in fact it is used widely in the Go standard libraries. The reason it is useful is that it gives you a way to express an unknown type that is determined at runtime.
Because Go is a statically typed language, the compiler complains very loudly if any variable has not been assigned a type, so the empty interface helps us get around this restriction.
Think of interface{} as being a bit like Object in other languages.
Here is an example of how we might use the empty interface.
Code Listing 35
package main import ( "fmt" ) func main() { displayType(42) displayType(3.14) displayType("ここでは文字列です") } func displayType(i interface{}) { switch theType := i.(type) { case int: fmt.Printf("%d is an integer\n", theType) case float64: fmt.Printf("%f is a 64-bit float\n", theType) case string: fmt.Printf("%s is a string\n", theType) default: fmt.Printf("I don't know what %v is\n", theType) } } |
42 is an integer
3.140000 is a 64-bit float
ここでは文字列です is a string
The function displayType() accepts any data type (because of the empty interface parameter). It then uses a type switch to return the actual type of the parameter that was passed into it.
If you have used the empty interface to access a value of an unknown data type and want to convert it to a known type so that you can work with it, you cannot simply use a type conversion because the empty interface is not a type. Instead, you must use a type assertion.
A type assertion takes the following form:
var anything interface{} = "something"
aString := anything.(string)
Code Listing 36
package main import ( "fmt" ) func main() { var anything interface{} = "something" aString := anything.(string) fmt.Println(aString) } |
something
If Go is unable to perform the conversion, it will “panic.”
Code Listing 37
package main import ( "fmt" ) func main() { var anything interface{} = "something" aInt := anything.(int) fmt.Println(aInt) } |
panic: interface conversion: interface is string, not int
goroutine 1 [running]:
panic(0xda640, 0xc820010180)
/usr/local/go/src/runtime/panic.go:464 +0x3e6
main.main()
.../src/hello/main.go:9 +0xa8
exit status 2
To protect against this eventuality, take advantage of the fact that the assertion can pass back two parameters: one containing the converted type and another indicating success or failure. You can then test the second parameter and gracefully deal with any errors.
Code Listing 38
package main import ( "fmt" ) func main() { var anything interface{} = "something" aInt, ok := anything.(int) if !ok { fmt.Println("Cannot turn input into an integer") } else { fmt.Println(aInt) } } |
Cannot turn input into an integer