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:
// 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
3.129.39.55