Skip to content
academia.sh

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 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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close