Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.