2. Types, Operators, and Expressions
Every value in Go has a type, fixed when the program is compiled and never
changing afterwards. A scripting language decides what x + y means while the
program runs; Go decides while the program is built, refuses the ones that make
no sense, and generates code that does not have to check. This chapter is the
type system’s ground floor.
2.1 The Basic Types
The numeric types come in explicit widths. Signed integers are int8, int16,
int32, int64; unsigned are uint8 through uint64; floats are float32
and float64. There are complex types and you can ignore them.
Two of those have second names. byte is another spelling of uint8 and rune
is another spelling of int32, used when the value is a raw byte or a Unicode
code point respectively. These are true aliases and not distinct types, so a
byte and a uint8 mix freely.
Then there are int and uint, which are the sizes the machine prefers: 64
bits on any machine you are likely to use, though the language permits 32.
Use int unless you have a reason not to. It is what array indexes and
lengths are, it is what an integer literal becomes, and reaching for int64
because the number might be large is a mistake on a 64-bit machine and a
deliberate choice about file formats and wire protocols everywhere else.
// What the numeric types can hold.
package main
import (
"fmt"
"math"
"unsafe"
)
func main() {
var i int
fmt.Printf("int %d bytes %d .. %d\n",
unsafe.Sizeof(i), math.MinInt, math.MaxInt)
fmt.Printf("int8 1 byte %d .. %d\n", math.MinInt8, math.MaxInt8)
fmt.Printf("uint8 1 byte 0 .. %d\n", math.MaxUint8)
fmt.Printf("int32 4 bytes %d .. %d\n", math.MinInt32, math.MaxInt32)
fmt.Printf("float64 8 bytes largest %g, smallest step above 1 is %g\n",
math.MaxFloat64, math.Nextafter(1, 2)-1)
}
int 8 bytes -9223372036854775808 .. 9223372036854775807
int8 1 byte -128 .. 127
uint8 1 byte 0 .. 255
int32 4 bytes -2147483648 .. 2147483647
float64 8 bytes largest 1.7976931348623157e+308, smallest step above 1 is 2.220446049250313e-16
bool is true or false, and nothing converts to it. There is no truthiness:
an empty string is not false, zero is not false, nil is not false. if x where
x is an integer does not compile, which removes a whole family of bugs and
costs you the occasional != 0.
string is an immutable sequence of bytes, conventionally UTF-8. Chapter 7
takes strings apart properly; until then, treat them as values you can compare
with ==, concatenate with +, and index for bytes.
2.2 Declarations and Zero Values
Four ways to bring a name into being, in rough order of how often you will write them.
x := 42 // short declaration, type inferred: int
var y int // zero value: 0
var z int = 42 // explicit type and value
var a, b = 1, "two" // several at once, types inferred
The short form only works inside a function. At package level, use var or
const.
A variable declared without a value is not undefined; it holds the zero
value for its type. Numbers are 0, strings are "", booleans are false,
and pointers, slices, maps, channels, functions, and interfaces are nil. Go’s
designers made the zero value useful on purpose, so a var buf bytes.Buffer is
an empty buffer ready to write to and a var mu sync.Mutex is an unlocked
mutex, neither needing a constructor. Watch for the type whose zero value is
not useful, the map, covered in section 7.3.
2.3 Constants
A const is fixed at compile time and can be a number, string, or boolean.
Constants that do not name a type are untyped, and this is more useful than
it sounds: an untyped constant carries arbitrary precision and takes on whatever
type the context needs.
const big = 1 << 100 // fine, no type yet
fmt.Println(big >> 98) // 4: becomes an int here
1 << 100 cannot be stored in any Go integer, but as an untyped constant it
does not have to be, and only the final expression has to fit. This is why you
can write math.Pi * r * r with r a float32 and nothing complains.
iota is a counter that resets to zero at each const block and increments
per line. Go writes enumerations and sequences with it.
// Constant generators.
package main
import "fmt"
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
const (
_ = iota // skip 1
KB = 1 << (10 * iota) // 1 << 10
MB // 1 << 20
GB // 1 << 30
)
func main() {
fmt.Println(Sunday, Wednesday, Saturday)
fmt.Println(KB, MB, GB)
}
0 3 6
1024 1048576 1073741824
A line in a const block with no expression repeats the previous one, so MB
and GB need not restate the shift. Section 6.4 gives Weekday a String
method so it prints as Sunday rather than 0.
2.4 Conversions
Go converts nothing implicitly. Mixing an int and a float64 in an expression
is an error, not a promotion:
mismatch/main.go:8:14: invalid operation: n * f (mismatched types int and float64)
Every conversion is written T(v), and every one is visible at the point it
happens. Three of them lose information quietly, so know them:
// Conversions, and three of them that surprise people.
package main
import (
"fmt"
"math"
)
func main() {
i := 300
fmt.Println(float64(i) / 7) // 42.857142857142854
fmt.Println(byte(i)) // 44: 300 does not fit in a byte
var big int32 = math.MaxInt32
fmt.Println(big + 1) // -2147483648: wraps, silently
fmt.Println(string(rune(65))) // "A"
fmt.Println(fmt.Sprint(65)) // "65"
}
Converting to a narrower integer keeps the low bits and discards the rest, so
byte(300) is 44 with no complaint. Signed integer overflow wraps rather than
trapping. That is at least defined behavior, where C leaves it undefined, but
it is still a wrong answer arriving silently. And string(rune(65)) is "A"
because it converts a code point to the character it names, while turning the
number 65 into the text "65" is strconv.Itoa or fmt.Sprint. Writing
string(65) directly is a mistake common enough that go vet reports it.
Integer division truncates toward zero and the remainder takes the sign of the
dividend, so -7/2 is -3 and -7%2 is -1. Dividing an integer by zero
panics and stops the program; dividing a float by zero gives +Inf, -Inf, or
NaN, following IEEE 754 without complaint.
2.5 Operators
Arithmetic is + - * / %, with % on integers only. Comparison is `== != < <=
=
and yields abool. Logic is&& || !, and&&and||` short-circuit.
Bitwise operators are & and, | or, ^ xor, &^ and-not, and << and >>
for shifts. Unary ^ is bitwise complement, where C uses ~. The &^ operator
clears bits, so x &^ mask is C’s x & ~mask in one token.
There is no operator overloading. + on two of your own struct types is an
error, always, and a package that wants addition gives you an Add method. This
is a real limitation for anyone writing numeric code and a real gift to anyone
reading unfamiliar code, since a + b cannot be a function call in disguise.
== compares values, not references. Two structs are equal when their fields
are equal, and a struct is comparable when all its fields are:
fmt.Println(Point{1, 2} == Point{1, 2}) // true
Slices, maps, and functions are not comparable at all and may only be tested
against nil. When you need to compare two slices, slices.Equal does it.
2.6 Named Types
type gives a name to a type, and the result is a new type rather than an
abbreviation. This matters more than it looks.
// A named type is a new type, not an alias.
package main
import "fmt"
type Celsius float64
type Fahrenheit float64
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
func main() {
boiling := Celsius(100)
fmt.Printf("%g°C is %g°F\n", float64(boiling), float64(CToF(boiling)))
}
Celsius and Fahrenheit are both float64 underneath, and neither will
assign to the other or to a plain float64 without a conversion. So a function
taking a Celsius cannot be handed a temperature in the wrong scale, and the
compiler enforces a distinction that a comment would only request. This costs
you the conversions you can see in the Printf line, and buys you a class of
bug that cannot happen.
You can convert between two named types with the same underlying type freely,
because the conversion is what the reader needs to see. If you genuinely want a
second name for the same type rather than a new one, that is type Celsius = float64, an alias, and it is rare outside of migrations.
Named types are also where methods come from, since a method must be declared on a type defined in the same package. Chapter 6.
Exercise 2-1. Print the largest and smallest values of every integer type
without using the math constants, by shifting and subtracting.
Exercise 2-2. Write type Meters float64 and type Feet float64 with
conversions both ways, then try to add a Meters to a Feet and read the
error.
Exercise 2-3. Find the smallest positive integer that float64 cannot
represent exactly. Confirm it by adding 1 and comparing.