Chapter 8. Methods, Interfaces, and Objects

Using your skills at this point, you can write an effective Go program using the fundamental concepts covered so far. As you will see in this chapter, the Go type system can support idioms that go beyond simple functions. While the designers of Go did not intend to create an object-oriented language with deep class hierarchies, the language is perfectly capable of supporting type compositions with advanced features to express the creation of complex object-like structures, as covered in the following topics:

  • Go methods
  • Objects in Go
  • The interface type
  • Type assertion

Go methods

A Go function can be defined with a scope narrowed to that of a specific type. When a function is scoped to a type, or attached to the type, it is known as a method. A method is defined just like any other Go function. However, its definition includes a method receiver, which is an extra parameter placed before the method's name, used to specify the host type to which the method is attached.

To better illustrate this concept, the following figure highlights the different parts involved in defining a method. It shows the quart method attached to the type gallon based receiver via the g gallon receiver parameter:

Go methods

As mentioned, a method has the scope of a type. Therefore, it can only be accessed via a declared value (concrete or pointer) of the attached type using dot notation. The following program shows how the declared method quart is accessed using this notation:

package main 
import "fmt" 
 
type gallon float64 
 
func (g gallon) quart() float64 { 
   return float64(g * 4) 
} 
func main(){ 
    gal := gallon(5) 
    fmt.Println(gal.quart()) 
} 

golang.fyi/ch08/method_basic.go

In the previous example, the gal variable is initialized as the gallon type. Therefore, the quart method can be accessed using gal.quart().

At runtime, the receiver parameter provides access to the value assigned to the base type of the method. In the example, the quart method receives the g parameter, which passes in a copy of the value for the declared type. So when the gal variable is initialized with a value of 5, a call to gal.quart() sets the receiver parameter g to 5. So the following would then print a value of 20:

func main(){ 
    gal := gallon(5) 
    fmt.Println(gal.quart()) 
} 

It is important to note that the base type for method receivers cannot be a pointer (nor an interface). For instance, the following will not compile:

type gallon *float64    
func (g gallon) quart() float64 {
  return float64(g * 4)
}

The following shows a lengthier version of the source that implements a more general liquid volume conversion program. Each volumetric type receives its respective methods to expose behaviors attributed to that type:

package main 
import "fmt" 
 
type ounce float64 
func (o ounce) cup() cup { 
   return cup(o * 0.1250) 
} 
 
type cup float64 
func (c cup) quart() quart { 
   return quart(c * 0.25) 
} 
func (c cup) ounce() ounce { 
   return ounce(c * 8.0) 
} 
 
type quart float64 
func (q quart) gallon() gallon { 
   return gallon(q * 0.25) 
} 
func (q quart) cup() cup { 
   return cup(q * 4.0) 
} 
 
type gallon float64 
func (g gallon) quart() quart { 
   return quart(g * 4) 
} 
 
func main() { 
    gal := gallon(5) 
    fmt.Printf("%.2f gallons = %.2f quarts
", gal, gal.quart()) 
    ozs := gal.quart().cup().ounce() 
    fmt.Printf("%.2f gallons = %.2f ounces
", gal, ozs) 
} 

github.com/vladimirvivien/learning-go/ch08/methods.go

For instance, converting 5 gallons to ounces can be done by invoking the proper conversion methods on a given value, as follows:

gal := gallon(5) 
ozs := gal.quart().cup().ounce() 

The entire implementation uses a simple, but effective, typical structure to represent both data type and behavior. Reading the code, it cleanly expresses its intended meaning without any reliance on heavy class structures.

Note

Method set

The number of methods attached to a type, via the receiver parameter, is known as the type's method set. This includes both concrete and pointer value receivers. The concept of a method set is important in determining type equality, interface implementation, and support of the notion of the empty method set for the empty interface (all discussed in this chapter).

Value and pointer receivers

One aspect of methods that has escaped discussion so far is that receivers are normal function parameters. Therefore, they follow the pass-by-value mechanism of Go functions. This implies that the invoked method gets a copy of the original value from the declared type.

Receiver parameters can be passed as either values of or pointers of the base type. For instance, the following program shows two methods, half and double; both directly update the value of their respective method receiver parameters, g:

package main
import "fmt" 
type gallon float64 
func (g gallon) quart() float64 { 
  return float64(g * 4) 
} 
func (g gallon) half() { 
  g = gallon(g * 0.5) 
} 
func (g *gallon) double() { 
  *g = gallon(*g * 2) 
} 
func main() { 
  var gal gallon = 5 
  gal.half() 
  fmt.Println(gal) 
  gal.double() 
  fmt.Println(gal) 
} 

golang.fyi/ch08/receiver_ptr.go

In the half method, the code updates the receiver parameter with g = gallon(g * 0.5). As you would expect, this will not update the original declared value, but rather the copy stored in the g parameter. So, when gal.half() is invoked in main, the original value remains unchanged and the following would print 5:

func main() { 
   var gal gallon = 5 
   gal.half() 
   fmt.Println(gal) 
} 

Similar to regular function parameters, a receiver parameter that uses a pointer to refer to its base value allows the code to dereference the original value to update it. This is highlighted in the double method following snippet. It uses a method receiver of the *gallon type, which is updated using *g = gallon(*g * 2). So when the following is invoked in main, it would print a value of 10:

func main() { 
   var gal gallon = 5 
   gal.double() 
   fmt.Println(gal) 
} 

Pointer receiver parameters are widely used in Go. This is because they make it possible to express object-like primitives that can carry both state and behaviors. As the next section shows, pointer receivers, along with other type features, are the basis for creating objects in Go.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.154.64