2.5 Type Declarations

The type of a variable or expression defines the characteristics of the values it may take on, such as their size (number of bits or number of elements, perhaps), how they are represented internally, the intrinsic operations that can be performed on them, and the methods associated with them.

In any program there are variables that share the same representation but signify very different concepts. For instance, an int could be used to represent a loop index, a timestamp, a file descriptor, or a month; a float64 could represent a velocity in meters per second or a temperature in one of several scales; and a string could represent a password or the name of a color.

A type declaration defines a new named type that has the same underlying type as an existing type. The named type provides a way to separate different and perhaps incompatible uses of the underlying type so that they can’t be mixed unintentionally.

type name underlying-type

Type declarations most often appear at package level, where the named type is visible throughout the package, and if the name is exported (it starts with an upper-case letter), it’s accessible from other packages as well.

To illustrate type declarations, let’s turn the different temperature scales into different types:

gopl.io/ch2/tempconv0
// Package tempconv performs Celsius and Fahrenheit temperature computations.
package tempconv

import "fmt"

type Celsius float64
type Fahrenheit float64

const (
    AbsoluteZeroC Celsius = -273.15
    FreezingC     Celsius = 0
    BoilingC      Celsius = 100
)

func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }

func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }

This package defines two types, Celsius and Fahrenheit, for the two units of temperature. Even though both have the same underlying type, float64, they are not the same type, so they cannot be compared or combined in arithmetic expressions. Distinguishing the types makes it possible to avoid errors like inadvertently combining temperatures in the two different scales; an explicit type conversion like Celsius(t) or Fahrenheit(t) is required to convert from a float64. Celsius(t) and Fahrenheit(t) are conversions, not function calls. They don’t change the value or representation in any way, but they make the change of meaning explicit. On the other hand, the functions CToF and FToC convert between the two scales; they do return different values.

For every type T, there is a corresponding conversion operation T(x) that converts the value x to type T. A conversion from one type to another is allowed if both have the same underlying type, or if both are unnamed pointer types that point to variables of the same underlying type; these conversions change the type but not the representation of the value. If x is assignable to T, a conversion is permitted but is usually redundant,

Conversions are also allowed between numeric types, and between string and some slice types, as we will see in the next chapter. These conversions may change the representation of the value. For instance, converting a floating-point number to an integer discards any fractional part, and converting a string to a []byte slice allocates a copy of the string data. In any case, a conversion never fails at run time.

The underlying type of a named type determines its structure and representation, and also the set of intrinsic operations it supports, which are the same as if the underlying type had been used directly. That means that arithmetic operators work the same for Celsius and Fahrenheit as they do for float64, as you might expect.

fmt.Printf("%g
", BoilingC-FreezingC) // "100" °C
boilingF := CToF(BoilingC)
fmt.Printf("%g
", boilingF-CToF(FreezingC)) // "180" °F
fmt.Printf("%g
", boilingF-FreezingC)       // compile error: type mismatch

Comparison operators like == and < can also be used to compare a value of a named type to another of the same type, or to a value of an unnamed type with the same underlying type. But two values of different named types cannot be compared directly:

var c Celsius
var f Fahrenheit
fmt.Println(c == 0)          // "true"
fmt.Println(f >= 0)          // "true"
fmt.Println(c == f)          // compile error: type mismatch
fmt.Println(c == Celsius(f)) // "true"!

Note the last case carefully. In spite of its name, the type conversion Celsius(f) does not change the value of its argument, just its type. The test is true because c and f are both zero.

A named type may provide notational convenience if it helps avoid writing out complex types over and over again. The advantage is small when the underlying type is simple like float64, but big for complicated types, as we will see when we discuss structs.

Named types also make it possible to define new behaviors for values of the type. These behaviors are expressed as a set of functions associated with the type, called the type’s methods. We’ll look at methods in detail in Chapter 6 but will give a taste of the mechanism here.

The declaration below, in which the Celsius parameter c appears before the function name, associates with the Celsius type a method named String that returns c’s numeric value followed by °C:

func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }

Many types declare a String method of this form because it controls how values of the type appear when printed as a string by the fmt package, as we will see in Section 7.1.

c := FToC(212.0)
fmt.Println(c.String()) // "100°C"
fmt.Printf("%v
", c)   // "100°C"; no need to call String explicitly
fmt.Printf("%s
", c)   // "100°C"
fmt.Println(c)          // "100°C"
fmt.Printf("%g
", c)   // "100"; does not call String
fmt.Println(float64(c)) // "100"; does not call String
..................Content has been hidden....................

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