Lesson 04 / 18
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.
Published
In this lesson
- 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
Contents
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
// 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.
// 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"
}
// 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
float64values added at runtime can comparefalseagainst a constant that looks mathematically equal; this result is left open. runeis an alias forint32; 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.
To keep your progress and take notes, Log in
My notes
Log in to take notes.