---
title: 'Basic Types'
source: 'https://academia.sh/en/courses/go-fundamentals/basic-types'
course: 'Go Fundamentals'
language: en
updated: '2026-09-13T05:08:45+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures which default type an untyped constant falls back to when given no context'
    - 'Runs proof that separates integer overflow, a defined wraparound, from floating-point comparison, which is left open'
    - 'Shows that the implicit-conversion rejection does not apply to untyped constants by comparing them against a variable carrying the same value'
---

# Basic Types

Four unwritten cases are run through the same frame: an untyped constant's default type turns out to be int, a sum that overflows an int8 wraps to -128, two float64 values added at runtime do not come out equal to 0.3, and a character literal's type is actually int32 — rune is only an alias. Limiting measurement: the implicit-conversion rejection does not hold for constants.

The previous lesson always inferred a variable's type from a value, but the types themselves
were never questioned. This lesson asks four questions at once: what is the default type of a
number written with no context, what happens when an integer overflows its bound, does adding
and comparing two floating-point values give the result we expect, and what actually is the
`rune` type we keep naming? All four are read with the same three classes — defined, rejected,
open — but these four are not part of the course's central fifteen-item frame; they are this
lesson's own observation.

The Programming Foundations course (M01/K02) built basic data types, operator precedence, and
explicit versus implicit conversion using Python. **Those concepts are not rebuilt here** — one
sentence is enough to state the difference: there, types were flexible at runtime and when a
value overflowed the result widened into a bigger integer; in Go every integer type has a
**fixed width**, and overflow is met not with widening but with wraparound. This lesson
measures what happens at the edge of that width.

## Four Unwritten Cases, One Frame

```go
// basics.go — four unwritten cases: default type, overflow, floating-point comparison, rune
package main

import "fmt"

const Number = 5

func main() {
	fmt.Printf("%-28s type=%-8s value=%v\n", "untyped constant default", fmt.Sprintf("%T", Number), Number)

	var b int8 = 127
	b++
	fmt.Printf("%-28s type=%-8s value=%v\n", "integer overflow", fmt.Sprintf("%T", b), b)

	var x, y float64 = 0.1, 0.2
	sum := x + y
	fmt.Printf("%-28s equal=%-6v sum=%v\n", "floating-point comparison", sum == 0.3, sum)

	c := 'A'
	fmt.Printf("%-28s type=%-8s value=%v\n", "character literal", fmt.Sprintf("%T", c), c)
}
```

```
untyped constant default     type=int      value=5
integer overflow             type=int8     value=-128
floating-point comparison    equal=false  sum=0.30000000000000004
character literal            type=int32    value=65
```

**LB10.** All four lines are read with the same three classes (defined, rejected, open), but
they are not part of the fifteen-item core; they are a new observation added to the shared
frame. The 8/4/3 split in the Zero Values lesson does not include these four lines.

Three of the four lines are **defined**, one is **open**. None of them is **rejected** — all
four are valid Go source, all four compile and run; what differs among them is whether the
result comes from the source, from a fixed rule, or from the representation at runtime. Let us
read them in order.

## The Default Type of an Untyped Constant

The `Number` constant is defined with the value `5`, and no type is ever written —
`const Number = 5`, not `const Number int = 5`. A constant like this is **untyped** in Go: on
its own it is neither an `int` nor a `float64` nor any other type, it is only a numeric value,
and it takes shape based on where it is used. When `Number` passes as an `interface{}`
argument to `fmt.Printf`, a concrete type is needed to format it, and the compiler brings in
the untyped constant's **default type** at that point: for integer constants that default is
`int`.

This is a different mechanism from the short declaration measured in the previous lesson.
There, the type was inferred from the assigned value's **own** type; here the value has no
type at all to begin with, and a type appears only when a context forces it to. If we had
assigned `Number` to a `float64` variable, the same constant would have become `float64`
without writing any conversion at all — because an untyped constant fits **by itself** into
whatever type the target allows. This distinction will reappear in the lesson's limiting
measurement.

The default type is not limited to `int`; every kind of constant has its own default. A
constant in quotes takes `string` as its default, `true`/`false` takes `bool` as its default, a
constant written with a decimal point takes `float64` as its default. None of these are ever
written in the source — the previous lesson showed short declaration inferring a type from a
**value**; here what we see is that the value itself, unless some context forces it, stays
**untyped** on its own. The type only appears once it is placed somewhere.

## Integer Overflow: A Defined Wraparound

The `int8` type is 8 bits wide and, being signed, holds values between -128 and 127. When we
give `b` the value 127 and increment it by one, the result falls outside that range — but the
program does not crash, and it produces no error either. The result becomes **-128**: the
wraparound turns from the top end of the range back to the bottom.

**LB11.** The overflow here is produced deliberately. Go does not count integer overflow as an
error; it wraps around in a **defined** way, using two's complement arithmetic. This adds a
third answer to the question of "what stands in for the unwritten": no check was written to
prevent the overflow, but the language does not put a random value into that gap, it puts an
arithmetically predictable one.

The wraparound rule is simple: a value that exceeds `int8`'s upper bound (127) by one continues
from the lower bound (-128). This is a natural consequence of two's complement representation —
the same bit pattern corresponds to a negative number when read as signed and a positive number
when read as unsigned, and addition stays the same at the bit level, only its interpretation
changes. The compiler adding no check here is not an oversight — the wraparound behavior merely
makes visible, as is, an operation the hardware was already doing.

The wraparound being predictable does not make it **safe**. The result `-128` is not some
approximate version of `b`'s real mathematical value (128) — it is an entirely different
number, and its sign has flipped too. If a computation crosses this boundary unnoticed, nowhere
is the result flagged as **wrong**; the overflow does not belong to the family of rejections
measured in the first lesson, it belongs to the `defined` class the measurement core names.
The language puts something there, but what it puts is not always the arithmetically expected
thing.

This behavior shows why an integer type's width needs to be chosen **explicitly** in the
source. Had a wider type (`int32`, `int64`) been chosen instead of `int8`, the same addition
would never have produced any wraparound at all; the wraparound comes not from the value
itself but from the width chosen. This also shows that choosing an integer type is a decision
that sits **outside** short declaration, measured in the previous lesson — short declaration
never lands on `int8` unless `b := int8(127)` is written, it always falls back to `int` by
default. A narrow type like `int8` can only be chosen with `var` or an explicit conversion, and
that choice also brings its overflow bound along with it.

## Floating-Point Comparison: A Result Left Open

The third line initializes two `float64` variables, `x` and `y`, with `0.1` and `0.2`, adds
them, and checks whether the sum equals `0.3`. The result is `false`. The sum itself is not
`0.3` either — it is `0.30000000000000004`.

**LB12.** The decimal digits printed here are not environment-dependent data — this is the
**deterministic** result of IEEE 754 binary fractional representation, and it reaches the same
digits on every run. `0.1` and `0.2` cannot be written as a finite fraction in binary, the same
way `1/3` cannot be written as a finite fraction in base ten; both round to the nearest
`float64` value, and their sum is not the true mathematical sum but the sum of those two
rounded values. This rounding is not specific to Go — the `float64` representation is not the
language's own specification, it is the IEEE 754 standard itself, and the language inherits
that representation as is.

This line falls into the **open** class, because the language puts nothing here and the
compiler rejects nothing either. `==` is a valid comparison, it compiles and runs; only whether
the result is "what one would mathematically expect" depends on the representation at runtime.
The code says what it does — it compares two `float64` values for equality — but it does not
say in advance whether that will match the reader's mental expectation that "0.1 plus 0.2 makes
0.3."

There is an interesting detail here: the untyped-constant mechanism we saw with `Number` could
also have affected this line. If `0.1 + 0.2 == 0.3` had been written with all three sides as
untyped constants, the compiler would have computed that sum **at compile time**, with
arbitrary precision, and the result really would have come out `true` — because constant
arithmetic relies on exact fractional computation, not binary rounding. The behavior left open
is a property not of constants but of **runtime variables**: because `x` and `y` are variables,
their addition happens at runtime, with binary values that were already rounded beforehand.

## A Character Being an Integer

The fourth line writes `c := 'A'`. A character in single quotes is, in Go, a **rune literal**,
and when its type is inferred by short declaration it comes out `int32` — no separate `rune`
type appears, because `rune`, in the language's own specification, is nothing more than an
**alias** for `int32`. `%T` prints both under the same name, `int32`.

The value itself confirms this too: the output shows `value=65`, not `A`. The `%v` format verb
of `fmt.Printf` prints a value according to its own type, and `int32`'s default printing is a
decimal number — to see the character we would need to write `%c`. This means the claim "a rune
is an integer" shows up not only in the type system but also in formatting: the language does
not keep a `rune` in some special "character" box, it treats it like an ordinary number, and
its appearing as a readable character is entirely the choice of the formatting call.

This fourth line comes from the same family as the first line: both measure a literal's
**default type**. The difference is that in the first, the default type is `int`, while in the
character literal, the default type is directly `rune` itself (`int32`). When `'A'` is
written, the language knows this is a code point, and code points are represented as integers;
the difference between writing `65` and writing `'A'` is, for this reason, purely one of
**readability** — both land on the same type, the same value.

This also explains why the word `rune` is never translated in this course: `rune` is not one of
Go's own keywords, it is a **type alias** the standard specification defines — the same way
`byte` is an alias for `uint8`. An alias, once written in the source, is resolved by the
compiler to its real name **immediately**; no separate `rune` type tag is carried at runtime,
and `%T` confirms this. Throughout this lesson we avoid using the word "character" for anything
measured, because what we are measuring is exactly that a character is actually an integer —
the course's Composite Types topic will deepen this distinction in its Strings and Runes
lesson, together with why a string is a byte sequence and not a character sequence.

## Limiting Measurement: The Implicit-Conversion Rejection Does Not Apply to Constants

One of the four rejections measured in the first lesson was the rejection of implicit type
conversion: an `int32` variable could not be assigned to an `int64` variable without a written
conversion. This rule does not hold for **untyped constants**. To measure this, we return to
the first lesson's compiler call.

```go
// compiler.go — helper that calls the compiler from inside the program
package main

import (
	"os"
	"os/exec"
	"path/filepath"
)

func compiles(source string) bool {
	dir, err := os.MkdirTemp("", "trial")
	if err != nil {
		return false
	}
	defer os.RemoveAll(dir)
	os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module trial\n\ngo 1.24\n"), 0o644)
	os.WriteFile(filepath.Join(dir, "code.go"), []byte(source), 0o644)
	cmd := exec.Command("go", "build", "./...")
	cmd.Dir = dir
	cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
	return cmd.Run() == nil
}

func classify(ok bool) string {
	if ok {
		return "compiled"
	}
	return "not compiled"
}
```

```go
// limiter.go — the implicit conversion rejection does not apply to constants
package main

import "fmt"

func main() {
	constantFits := `package trial
func F() { var target int64 = 5; _ = target }`
	variableMismatch := `package trial
func F() { var source int32 = 5; var target int64 = source; _ = target }`

	fmt.Printf("%-32s %s\n", "untyped constant fits target", classify(compiles(constantFits)))
	fmt.Printf("%-32s %s\n", "variable carrying same value", classify(compiles(variableMismatch)))
}
```

```
untyped constant fits target     compiled
variable carrying same value     not compiled
```

Both lines carry the same numeric value (`5`) and assign it to the same target type
(`int64`). In the first line the source is an **untyped constant** — the literal `5` is not
bound to any type, it fits directly into `int64`, and the compiler counts this as an initial
value assignment, not a conversion. In the second line the same `5` value is first assigned to
an `int32` variable; from that point on `5` is no longer untyped, it is a value **materialized
as `int32`**, and its move to `int64` runs into the implicit-conversion rejection measured in
the first lesson.

This shows the difference between "same value" and "same type." The compiler's rejection
decision looks not at the numeric value being carried but at the source's **type**. An untyped
constant carries no width at all until it is materialized; a variable, on the other hand, locks
into a specific width the moment it is defined. The rule measured in the first lesson needs to
be rewritten for this reason: an implicit conversion is rejected between two **typed** values;
an untyped constant fitting its target is not a conversion, it is a consequence of the
constant's own nature.

This boundary can also be tested with a "what if there were a fourth" question: what would
happen if we wrote `500` instead of `5` and made the target `int8`? In that case the untyped
constant would not **fit** the target, and compilation would still be rejected — but this would
not be a conversion rejection, it would be an **overflow rejection**: since the compiler knows
a constant's value at compile time, it sees in advance that it will not fit the target and
never compiles the source at all. This is a completely different mechanism from the runtime
overflow measured in the second section (`int8` exceeding 127): there the overflow **wrapped**
while running; here the overflow is **rejected** while compiling, because the constant's value
is already written in the source and the compiler does not need to run the program to compute
it.

All four measurements show the same distinction from different angles: as long as a constant's
type is not written in the source it stays **suspended** and only becomes fixed once some
context materializes it; a variable's type, on the other hand, becomes fixed the moment it is
defined and, from that point on, travels together with its width. The default type in the
first line, the character being an integer in the fourth line, and the constant-versus-variable
distinction in the limiting measurement — all three are different views of this same suspended
state. Only the second and third lines stand apart from this family — they are not about being
left suspended, they are about the bounds of an already-materialized type (`int8`'s width,
`float64`'s binary representation).

## Summary

- An untyped integer constant's default type, when used with no context, is `int`.
- A sum that exceeds `int8`'s bound wraps to -128 without producing an error — the overflow is
  defined, but not the one arithmetically expected.
- Two `float64` values added at runtime can compare `false` against a constant that looks
  mathematically equal; this result is left open.
- `rune` is an alias for `int32`; a character literal's default type is directly this.
- The implicit-type-conversion rejection applies only between typed values — an untyped
  constant fits its target type on its own.

## Next Step

Three of the four unwritten cases measured in this lesson found a defined counterpart, one was
left open. But none of them asked the case where a variable **never** receives a value at
all — if we wrote `var b int8` and never gave it a value, what would `b` become? The next
lesson carries that question to the center of the course: it gathers fifteen separate unwritten
cases into a single frame and measures that eight are defined, four are rejected, three are
left open.
