Lesson 22. Go’s got no class

After reading lesson 22, you’ll be able to

  • Write methods that provide behavior to structured data
  • Apply principles of object-oriented design

Go isn’t like classical languages. It has no classes and no objects, and it omits features like inheritance. Yet Go still provides what you need to apply ideas from object-oriented design. This lesson explores the combination of structures with methods.

Consider this

Synergy is a buzzword commonly heard in entrepreneurial circles. It means “greater than the sum of its parts.” The Go language has types, methods on types, and structures. Together, these provide much of the functionality that classes do for other languages, without needing to introduce a new concept into the language.

What other aspects of Go exhibit this property of combining to create something greater?

22.1. Attaching methods to structures

In lesson 13, you attached celsius and fahrenheit methods to the kelvin type to convert temperatures. In the same way, methods can be attached to other types you declare. It works the same whether the underlying type is a float64 or a struct.

To start, you need to declare a type, such as the coordinate structure in the following listing.

Listing 22.1. The coordinate type: coordinate.go
// coordinate in degrees, minutes, seconds in a N/S/E/W hemisphere.
type coordinate struct {
    d, m, s float64
    h       rune
}

Bradbury Landing is located at 4°35’22.2” S, 137°26’30.1” E in DMS format (degrees, minutes, seconds). There are 60 seconds (") in one minute, and 60 minutes (') in one degree, but these minutes and seconds represent a location, not a time.

The decimal method in the following listing will convert a DMS coordinate to decimal degrees.

Listing 22.2. The decimal method: coordinate.go
// decimal converts a d/m/s coordinate to decimal degrees.
func (c coordinate) decimal() float64 {
    sign := 1.0
    switch c.h {
    case 'S', 'W', 's', 'w':
        sign = -1
    }
    return sign * (c.d + c.m/60 + c.s/3600)
}

Now you can provide coordinates in the friendly DMS format and convert them to decimal degrees to perform calculations:

// Bradbury Landing: 4°35'22.2" S, 137°26'30.1" E
lat := coordinate{4, 35, 22.2, 'S'}
long := coordinate{137, 26, 30.12, 'E'}

fmt.Println(lat.decimal(), long.decimal())        1

  • 1 Prints -4.5895 137.4417
Quick check 22.1

Q1:

What is the receiver for the decimal method in listing 22.2?

QC 22.1 answer

1:

The receiver is c of type coordinate.

 

22.2. Constructor functions

To construct a decimal degrees location from degrees, minutes, and seconds, you can use the decimal method from listing 22.2 with a composite literal:

type location struct {
    lat, long float64
}

curiosity := location{lat.decimal(), long.decimal()}

If you need a composite literal that’s anything more than a list of values, consider writing a constructor function. The following listing declares a constructor function named newLocation.

Listing 22.3. Construct a new location: construct.go
// newLocation from latitude, longitude d/m/s coordinates.
func newLocation(lat, long coordinate) location {
    return location{lat.decimal(), long.decimal()}
}

Classical languages provide constructors as a special language feature to construct objects. Python has _init_, Ruby has initialize, and PHP has __construct(). Go doesn’t have a language feature for constructors. Instead newLocation is an ordinary function with a name that follows a convention.

Functions in the form newType or NewType are used to construct a value of said type. Whether you name it newLocation or NewLocation depends on whether the function is exported for other packages to use, as covered in lesson 12. You use newLocation like any other function:

curiosity := newLocation(coordinate{4, 35, 22.2, 'S'},
coordinate{137, 26, 30.12, 'E'})
fmt.Println(curiosity)                   1

  • 1 Prints {-4.5895 137.4417}

If you want to construct locations from a variety of inputs, just declare multiple functions with suitable names—perhaps newLocationDMS and newLocationDD for degrees, minutes, and seconds and decimal degrees, respectively.

Note

Sometimes constructor functions are named New, as is the case with the New function in the errors package. Because function calls are prefixed with the package they belong to, naming the function NewError would be read as errors.NewError rather than the more concise and preferable errors.New.

Quick check 22.2

Q1:

What would you name a function that constructs a variable of type Universe?

QC 22.2 answer

1:

By convention the function would be named NewUniverse, or newUniverse if not exported.

 

22.3. The class alternative

Go doesn’t have the class of classical languages like Python, Ruby, and Java. Yet a structure with a few methods fulfills much of the same purpose. If you squint, they aren’t that different.

To drive the point home, build a whole new world type from the ground up. It will have a field for the radius of the planet, which you’ll use to calculate the distance between two locations, as shown in the following listing.

Listing 22.4. A whole new world: world.go
type world struct {
    radius float64
}

Mars has a volumetric mean radius of 3,389.5 kilometers. Rather than declare 3389.5 as a constant, use the world type to declare Mars as one of many possible worlds:

var mars = world{radius: 3389.5}

Then a distance method is attached to the world type, giving it access to the radius field. It accepts two parameters, both of type location, and will return a distance in kilometers:

func (w world) distance(p1, p2 location) float64 {
                                                     1
}

  • 1 To-do: some math using w.radius

This is going to involve some math, so be sure to import the math package, as follows:

import "math"

The location type uses degrees for latitude and longitude, but the math functions in the standard library use radians. Given that a circle has 360° or 2π radians, the following function performs the necessary conversion:

// rad converts degrees to radians.
func rad(deg float64) float64 {
    return deg * math.Pi / 180
}

Now for the distance calculation. It uses a number of trigonometric functions including sine, cosine, and arccosine. If you’re a math geek, you can look up the formulas (www.movable-type.co.uk/scripts/latlong.html) and research the Spherical Law of Cosines to understand how this works. Mars isn’t a perfect sphere, but this formula achieves a “good enough” approximation for our purposes:

// distance calculation using the Spherical Law of Cosines.
func (w world) distance(p1, p2 location) float64 {
    s1, c1 := math.Sincos(rad(p1.lat))
    s2, c2 := math.Sincos(rad(p2.lat))
    clong := math.Cos(rad(p1.long - p2.long))
    return w.radius * math.Acos(s1*s2+c1*c2*clong)       1
}

  • 1 Uses the world’s radius field

If your eyes just glazed over, don’t worry. The math is needed in a program that calculates distance, but as long as distance returns the correct results, fully understanding how all the math works is optional (though a good idea).

Speaking of results, to see distance in action, declare some locations and use the mars variable declared earlier:

spirit := location{-14.5684, 175.472636}
opportunity := location{-1.9462, 354.4734}

dist := mars.distance(spirit, opportunity)        1
fmt.Printf("%.2f km
", dist)                     2

  • 1 Uses the distance method on mars
  • 2 Prints 9669.71 km

If you get a different result, go back to ensure the code is typed exactly as shown. One missing rad will result in incorrect calculations. If all else fails, download the code from github.com/nathany/get-programming-with-go and resign yourself to copy and paste.

The distance method was adopted from formulas for Earth, but using the radius of Mars. By declaring distance as a method on the world type, you can calculate distance for other worlds, such as Earth. The radius for each planet is found in table 22.2, as provided by the Planetary Fact Sheet (nssdc.gsfc.nasa.gov/planetary/factsheet/)

Quick check 22.3

Q1:

How is it beneficial to declare a distance method on the world type compared to a less object-oriented approach?

QC 22.3 answer

1:

It provides a clean way to calculate distance for different worlds, and there’s no need to pass the volumetric mean radius into the distance method, because it already has access to w.radius.

 

Summary

  • Combining methods and structures provides much of what classical languages provide without introducing a new language feature.
  • Constructor functions are ordinary functions.

Let’s see if you got this...

Experiment: landing.go

Use the code from listings 22.1, 22.2, and 22.3 to write a program that declares a location for each location in table 22.1. Print out each of the locations in decimal degrees.

Experiment: distance.go

Use the distance method from listing 22.4 to write a program that determines the distance between each pair of landing sites in table 22.1.

Which two landing sites are the closest?

Which two are farthest apart?

To determine the distance between the following locations, you’ll need to declare other worlds based on table 22.2:

  • Find the distance from London, England (51°30’N 0°08’W) to Paris, France (48°51’N 2°21’E).
  • Find the distance from your city to the capital of your country.
  • Find the distance between Mount Sharp (5°4’ 48”S, 137°51’E) and Olympus Mons (18°39’N, 226°12’E) on Mars.
Table 22.1. Landing sites on Mars

Rover or lander

Landing site

Latitude

Longitude

Spirit Columbia Memorial Station 14°34’6.2” S 175°28’21.5” E
Opportunity Challenger Memorial Station 1°56’46.3” S 354°28’24.2” E
Curiosity Bradbury Landing 4°35’22.2” S 137°26’30.1” E
InSight Elysium Planitia 4°30’0.0” N 135°54’0” E

Table 22.2. The volumetric mean radius of various planets

Planet

Radius (km)

Mercury 2439.7
Venus 6051.8
Earth 6371.0
Mars 3389.5
Jupiter 69911
Saturn 58232
Uranus 25362
Neptune 24622
..................Content has been hidden....................

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