Predeclared Types
Predeclared Types
Go gives you a short list of types with no import. The boring default is bool, int, and string for local work, int64 (or another sized type) when the width is part of the contract, and an explicit conversion whenever two names differ.
Mental model
A predeclared type is a named type that exists in every package. int and int64 are different types even when both hold eight bytes on a 64-bit machine. byte is an alias for uint8. rune is an alias for int32 and means “Unicode code point,” not “UTF-8 byte.”
A literal such as 4, true, or "toast" is untyped until you use it in a typed place. A variable always has a type. Conversions are written: int64(n), never implied.
Worked examples
Case 1: The three types you actually type all day
Save as desk_types.go. A desk is open or not (bool), it has a name (string), and it has a table count (int).
// desk_types.go
package main
import "fmt"
func main() {
open := true
name := "front"
tables := 12
fmt.Printf("open=%v (%T)\n", open, open)
fmt.Printf("name=%q (%T)\n", name, name)
fmt.Printf("tables=%d (%T)\n", tables, tables)
}Run:
go run desk_types.goOutput:
open=true (bool)
name="front" (string)
tables=12 (int)
int is the default integer. Its size is the size of a register: 8 bytes on the 64-bit machine this book was checked on, 4 bytes on 32-bit. Use it for counts, indexes, and anything that never leaves this process.
Case 2: int vs int64 — convert on the boundary
Save as ticket_id.go. Ticket IDs are stored as int64 because they are a contract with another system. Table numbers stay int. You cannot add them until one side converts.
// ticket_id.go
package main
import "fmt"
func main() {
var table int = 4
var ticket int64 = 1_000_001
fmt.Println(int64(table) + ticket)
fmt.Printf("%T + %T → %T\n", int64(table), ticket, int64(table)+ticket)
}Run:
go run ticket_id.goOutput:
1000005
int64 + int64 → int64
If you write table + ticket, the compile fails: invalid operation: table + ticket (mismatched types int and int64). That failure is the feature. Money in this book is integer cents (int or int64), not float64.
Case 3: byte and rune
Save as rune_byte.go. A byte is a small unsigned integer. A rune is a code point. The euro sign is one rune and three UTF-8 bytes.
// rune_byte.go
package main
import "fmt"
func main() {
var mark byte = 'A'
var euro rune = '€'
fmt.Printf("mark %q %d %T\n", mark, mark, mark)
fmt.Printf("euro %q %d %T %U\n", euro, euro, euro, euro)
}Run:
go run rune_byte.goOutput:
mark 'A' 65 uint8
euro '€' 8364 int32 U+20AC
%T prints uint8 and int32 because those are the real types. The aliases exist so you can say what you mean.
Case 4: Untyped constants, typed variables
Save as untyped.go. An untyped constant can land in more than one type. A typed constant cannot.
// untyped.go
package main
import "fmt"
func main() {
const seats = 4
var asInt int = seats
var asInt64 int64 = seats
fmt.Println(asInt, asInt64)
const typed int = 4
var also int64 = int64(typed)
fmt.Println(also)
fmt.Printf("%T %T %T\n", true, 12, "toast")
}Run:
go run untyped.goOutput:
4 4
4
bool int string
seats has no type of its own, so both assignments work. typed is an int; it needs int64(typed) to become int64. Passing 12 to fmt.Printf gives it the default type int — that is why %T prints int, not “untyped integer.”
Case 5: Sizes, once, so the names stop being magic
Save as sizes.go. unsafe.Sizeof reports how many bytes a variable occupies. For a string that is the header (pointer plus length), not the characters. This is a measuring tape, not a desk tool — do not sprinkle unsafe through business code.
// sizes.go
package main
import (
"fmt"
"unsafe"
)
func main() {
fmt.Println("bool", unsafe.Sizeof(false))
fmt.Println("int", unsafe.Sizeof(int(0)))
fmt.Println("int32", unsafe.Sizeof(int32(0)))
fmt.Println("int64", unsafe.Sizeof(int64(0)))
fmt.Println("rune", unsafe.Sizeof(rune(0)))
fmt.Println("byte", unsafe.Sizeof(byte(0)))
fmt.Println("string header", unsafe.Sizeof(""))
fmt.Println("float64", unsafe.Sizeof(float64(0)))
}Run:
go run sizes.goOutput on 64-bit:
bool 1
int 8
int32 4
int64 8
rune 4
byte 1
string header 16
float64 8
rune matches int32. byte matches uint8. int matches the machine. float64 exists; this book still keeps prices in cents.
The trap
Conversions compile whenever the types are numeric (or string/[]byte). They do not check that the value still means the same thing. Save as truncate.go:
// truncate.go
package main
import "fmt"
func main() {
euro := '€'
b := byte(euro)
fmt.Printf("rune %U %d\n", euro, euro)
fmt.Printf("as byte %d\n", b)
minutes := 2.9
fmt.Println("int(2.9) =", int(minutes))
}Run:
go run truncate.goOutput:
rune U+20AC 8364
as byte 172
int(2.9) = 2
8364 does not fit in a byte, so you get 8364 % 256. int of a float truncates toward zero; it does not round. Convert when you know the value fits, or when truncation is the specified behaviour. Otherwise keep the wider type.
The boring rule
- Use
intfor counts and indexes in this process. - Use
int64(orint32) when a file, a network, or another language must agree on width. - Store money as integer cents. Do not use
float64for prices. - Write conversions. If the compiler complains about mismatched types, do not silence it with a random cast — pick the type you meant.
- Use
runefor characters,bytefor raw bytes. They are not interchangeable just because both are “small integers.” - Leave
unsafefor diagnostics and the rare low-level package.
Try this
- In
ticket_id.go, deleteint64(table)and run. Read the compile error. Put the conversion back on the other side (int(ticket)) and decide which width you want. - In
untyped.go, tryvar also int64 = typedwithout the conversion. Confirm only the typed constant fails. - In
truncate.go, convert'A'tobyteand print it. Then convert a rune that does not fit ('本'or'€') and compare. - Add
fmt.Printf("%T\n", 1.5)tountyped.go. The default type of an untyped float isfloat64.