Go, chapter by chapter · Lesson 2 of 62 · about 40 min
Types
Numbers, strings and booleans: what each type can hold, how Go stores it, and the traps that cause real bugs.
On this page · 9 sections
Integers#
An integer type has a fixed number of bits. More bits hold bigger numbers but use more memory.
| Type | Bits | Range |
|---|---|---|
int8 | 8 | -128 to 127 |
uint8 (alias byte) | 8 | 0 to 255 |
int16 / uint16 | 16 | about ±32 thousand / 0 to 65,535 |
int32 (alias rune) | 32 | about ±2.1 billion |
int64 | 64 | about ±9.2 × 10¹⁸ |
int / uint | 64 on modern machines | same as int64 / uint64 |
Signed types (int…) hold negatives; unsigned ones (uint…) do not, which doubles their positive range. In everyday code and in every algorithm in this course, use plain int. Reach for the sized types only when a format or protocol requires them.
Overflow wraps silently
If a value goes past the top of its range, it wraps around to the bottom. No error, no panic.
| Step | b (uint8) | binary |
|---|---|---|
var b uint8 = 254 | 254 | 11111110 |
b++ | 255 | 11111111 |
b++ | 0 | 00000000 (the 9th bit is lost) |
See it
package main
import (
"fmt"
"math"
)
func main() {
var b uint8 = 254
for range 3 {
fmt.Printf("b = %3d %08b\n", b, b)
b++
}
var small int8 = 127
small++ // 127 is the top of int8
fmt.Println("int8 127 + 1 =", small)
big := math.MaxInt64
fmt.Println("MaxInt64 + 1 =", big+1)
}b = 254 11111110
b = 255 11111111
b = 0 00000000
int8 127 + 1 = -128
MaxInt64 + 1 = -9223372036854775808The 64-bit case is the one that bites in real code: summing large values or multiplying two big numbers can quietly turn negative.
Integer arithmetic
+ - * work as you expect. / on integers truncates toward zero: 7 / 2 is 3, and -7 / 2 is -3. % gives the remainder: 7 % 2 is 1. You will use n % 10 (last digit) and n / 10 (drop last digit) constantly.
Try it: digits with / and %
n % 10 reads the last digit and n / 10 removes it. Repeat until nothing is left:
package main
import "fmt"
func main() {
n := 4096
sum := 0
for n > 0 {
d := n % 10
sum += d
fmt.Printf("n=%-4d last digit %d, n/10 = %d\n", n, d, n/10)
n /= 10
}
fmt.Println("digit sum:", sum)
fmt.Println(7/2, -7/2, 7%2, -7%2, 7.0/2)
}n=4096 last digit 6, n/10 = 409
n=409 last digit 9, n/10 = 40
n=40 last digit 0, n/10 = 4
n=4 last digit 4, n/10 = 0
digit sum: 19
3 -3 1 -1 3.5The sign of % follows the left operand, so -7 % 2 is -1. And 7.0/2 is a float division, giving 3.5.
Floating-point numbers#
float64 (the default) and float32 hold numbers with a fractional part. They store a binary approximation, so most decimals are not exact:
x, y := 0.1, 0.2
fmt.Println(x + y) // 0.30000000000000004
fmt.Println(x+y == 0.3) // falseCompare floats with a tolerance: math.Abs(a-b) < 1e-9. Floats also have special values: math.Inf(1) for infinity and math.NaN() for "not a number" (the result of 0.0/0.0). Go also has complex64 and complex128 for complex numbers, which you will rarely need.
Strings#
A string is an immutable sequence of bytes, almost always UTF-8 text. That one sentence explains every surprise:
len(s)counts bytes, not characters. ASCII letters take 1 byte; é takes 2; 日 takes 3; most emoji take 4.s[i]gives the byte at positioni(typebyte), not a character.s[a:b]gives a substring of bytesaup to (not including)b.- You cannot change a string in place.
s[0] = 'x'does not compile. Build a new string instead. +joins strings.
A rune is one Unicode character (code point), stored as an int32. To work with characters instead of bytes, range over the string or convert to []rune:
for i, r := range "héllo" {
fmt.Println(i, string(r)) // i is the byte offset: 0 1 3 4 5
}Notice the index jumps from 1 to 3: é occupies bytes 1 and 2.
String literals come in two kinds. Double quotes interpret escapes like \n and \t. Backticks make a raw string where backslashes are literal and newlines are allowed, handy for regular expressions and multi-line text.
See bytes and runes side by side
package main
import (
"fmt"
"strings"
"unicode/utf8"
)
func main() {
s := "Go日本"
fmt.Println("len (bytes):", len(s), " runes:", utf8.RuneCountInString(s))
fmt.Println("by byte:")
for i := 0; i < len(s); i++ {
fmt.Printf(" s[%d] = %3d %08b\n", i, s[i], s[i])
}
fmt.Println("by rune:")
for i, r := range s {
fmt.Printf(" byte offset %d: %c (U+%04X, %d bytes)\n", i, r, r, utf8.RuneLen(r))
}
// Strings are immutable: build a new one instead.
up := strings.ToUpper(s[:2]) + s[2:]
r := []rune(s)
r[0] = 'N'
fmt.Println(up, string(r), s) // s itself never changed
raw := `C:\new\table` // backticks: no escapes
fmt.Println(raw, len("\n"), len(`\n`))
}len (bytes): 8 runes: 4
by byte:
s[0] = 71 01000111
s[1] = 111 01101111
s[2] = 230 11100110
s[3] = 151 10010111
s[4] = 165 10100101
s[5] = 230 11100110
s[6] = 156 10011100
s[7] = 172 10101100
by rune:
byte offset 0: G (U+0047, 1 bytes)
byte offset 1: o (U+006F, 1 bytes)
byte offset 2: 日 (U+65E5, 3 bytes)
byte offset 5: 本 (U+672C, 3 bytes)
GO日本 No日本 Go日本
C:\new\table 1 2Each Japanese character takes 3 bytes, so the rune loop's offsets jump from 2 to 5. The last line shows that "\n" is one byte (a newline) while the raw `\n` is two: a backslash and an n.
Booleans#
bool is true or false, with three operators:
| Expression | Meaning |
|---|---|
a && b | true only if both are true |
a || b | true if either is true |
!a | the opposite of a |
Go short-circuits: in a && b, if a is false, b is never evaluated. That makes guards like i < len(s) && s[i] == 'x' safe, because the index is only read when it is in range.
Converting between types#
Go never converts implicitly. Adding an int to a float64 is a compile error. Convert explicitly:
| You have | You want | Write |
|---|---|---|
int | float64 | float64(n) |
float64 | int | int(f) (drops the fraction) |
int | text "42" | strconv.Itoa(n) |
text "42" | int | n, err := strconv.Atoi(s) |
string | bytes or runes | []byte(s), []rune(s) |
Try it: conversions that can fail
strconv.Atoi returns an error instead of guessing, so always check it. Float-to-int conversion never fails; it just drops the fraction:
package main
import (
"fmt"
"strconv"
)
func main() {
for _, s := range []string{"42", "-7", "4.5", "12abc", ""} {
n, err := strconv.Atoi(s)
if err != nil {
fmt.Printf("%-7q error: %v\n", s, err)
continue
}
fmt.Printf("%-7q ok: %d\n", s, n)
}
f, _ := strconv.ParseFloat("4.5", 64)
fmt.Println(f, int(f), int(-f), strconv.FormatFloat(f, 'f', 2, 64))
n := 7
avg := float64(n) / 2 // convert before dividing
wrong := float64(n / 2)
fmt.Println(avg, wrong)
}"42" ok: 42
"-7" ok: -7
"4.5" error: strconv.Atoi: parsing "4.5": invalid syntax
"12abc" error: strconv.Atoi: parsing "12abc": invalid syntax
"" error: strconv.Atoi: parsing "": invalid syntax
4.5 4 -4 4.50
3.5 3float64(n / 2) divides as integers first, getting 3, and only then converts. Convert before dividing when you want the fraction.
The full program#
package main
import (
"fmt"
"math"
"strconv"
"unicode/utf8"
)
func main() {
// Integers
var b uint8 = 255
b++ // wraps to 0
fmt.Println(b, math.MaxInt64, 7/2, -7/2, 7%2)
// Floats
x, y := 0.1, 0.2
fmt.Println(x+y == 0.3, math.Abs(x+y-0.3) < 1e-9)
f := 3.99
fmt.Println(float64(7)/2, int(f))
// Strings are bytes
s := "Halo, Dunia"
fmt.Println(len(s), s[0], string(s[0]), s[6:])
w := "héllo"
fmt.Println(len(w), utf8.RuneCountInString(w), []rune(w)[1] == 'é')
// Conversions
n, err := strconv.Atoi("42")
fmt.Println(n+1, err, strconv.Itoa(65), string(rune(65)))
// Booleans short-circuit: s[20] is never evaluated
i := 20
fmt.Println(i < len(s) s[i] == 'x', true false,true)
}0 9223372036854775807 3 -3 1
false true
3.5 3
11 72 H Dunia
6 5 true
43 <nil> 65 A
false true falseTrace the trickiest lines yourself: s[0] is the byte 72 (the code for H), and s[6:] starts after "Halo, " (6 bytes). int(f) with f := 3.99 is 3 because conversion truncates. (Writing int(3.99) directly does not even compile: Go refuses to lose a constant's fraction silently.)
Summary#
- Use
intfor integers andfloat64for decimals; sized types are for formats and protocols. - Integer overflow wraps silently; integer division truncates.
- Floats are approximate: compare with a tolerance and never use them for money.
- Strings are immutable bytes.
lenand indexing work on bytes;rangeand[]runework on characters. - All conversions are explicit:
float64(n),strconv.Itoa,strconv.Atoi.
Check your understanding
Answer before scrolling back up. If you miss one, reread the matching section, then try again.
Q1.
var b uint8 = 255thenb++. What isb?Q2.What does
len("héllo")return?Q3.What is
7 / 2in Go when both are integers?Q4.Why can
x + y == 0.3be false whenx := 0.1andy := 0.2?
Practice in Go
Write a table test first, then the solution. On LeetCode, choose Go as the language. Open a hint only after 15 minutes stuck.
- Add Digits ↗easy
Show hint
Use
n % 10to get the last digit andn / 10to drop it. Loop until one digit is left. - Reverse String ↗easy
Show hint
The input is a
[]byte. Swaps[i]ands[len(s)-1-i]for the first half. - Valid Anagram ↗easy
Show hint
Count letters in a
[26]intusings[i]-'a'as the index, add for one string and subtract for the other. - Overflow explorerbuild
Print the max value of every integer type using the
mathpackage constants, then show what happens when you add 1 to each.Show hint
math.MaxInt8,math.MaxUint16and friends. Use typed variables, because adding to a constant overflows at compile time instead.
Finished this lesson?
Mark it done to track your progress. It is saved in this browser only.