Go Guide

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
  1. 01Integers
  2. 02Floating-point numbers
  3. 03Strings
  4. 04Booleans
  5. 05Converting between types
  6. 06The full program
  7. 07Summary
  8. 08Check your understanding
  9. 09Practice in Go

Integers#

An integer type has a fixed number of bits. More bits hold bigger numbers but use more memory.

TypeBitsRange
int88-128 to 127
uint8 (alias byte)80 to 255
int16 / uint1616about ±32 thousand / 0 to 65,535
int32 (alias rune)32about ±2.1 billion
int6464about ±9.2 × 10¹⁸
int / uint64 on modern machinessame 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.

Stepb (uint8)binary
var b uint8 = 25425411111110
b++25511111111
b++000000000 (the 9th bit is lost)

See it

overflow.go
Go
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)
}
Output
b = 254  11111110
b = 255  11111111
b =   0  00000000
int8 127 + 1 = -128
MaxInt64 + 1 = -9223372036854775808

The 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:

digits.go
Go
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)
}
Output
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.5

The 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:

Go
x, y := 0.1, 0.2
fmt.Println(x + y)        // 0.30000000000000004
fmt.Println(x+y == 0.3)   // false

Compare 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 position i (type byte), not a character.
  • s[a:b] gives a substring of bytes a up 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:

Go
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

bytes_runes.go
Go
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`))
}
Output
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 2

Each 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:

ExpressionMeaning
a && btrue only if both are true
a || btrue if either is true
!athe 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 haveYou wantWrite
intfloat64float64(n)
float64intint(f) (drops the fraction)
inttext "42"strconv.Itoa(n)
text "42"intn, err := strconv.Atoi(s)
stringbytes 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:

convert.go
Go
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)
}
Output
"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 3

float64(n / 2) divides as integers first, getting 3, and only then converts. Convert before dividing when you want the fraction.

The full program#

types.go
Go
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)
}
Output
0 9223372036854775807 3 -3 1
false true
3.5 3
11 72 H Dunia
6 5 true
43 <nil> 65 A
false true false

Trace 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 int for integers and float64 for 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. len and indexing work on bytes; range and []rune work 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.

  1. Q1.var b uint8 = 255 then b++. What is b?

  2. Q2.What does len("héllo") return?

  3. Q3.What is 7 / 2 in Go when both are integers?

  4. Q4.Why can x + y == 0.3 be false when x := 0.1 and y := 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.

  • Show hint

    Use n % 10 to get the last digit and n / 10 to drop it. Loop until one digit is left.

  • Show hint

    The input is a []byte. Swap s[i] and s[len(s)-1-i] for the first half.

  • Show hint

    Count letters in a [26]int using s[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 math package constants, then show what happens when you add 1 to each.

    Show hint

    math.MaxInt8, math.MaxUint16 and 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.