skip to content

Types, Values and Constants

The value layer of Go: which built-in types exist, what every variable starts out as, how a defined type differs from an alias, and how the untyped constant system works. Most Go trivia questions — nil maps, rune vs byte, iota enums — resolve at this level.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

explore

questions

29

In Go, what happens when an int8 or a uint8 exceeds its maximum value?

level: juniorimportance: must knowfreq 70%

answer

  1. the odometer rolls over
  2. no exception, no widening
  3. 127 plus one is not 128
  4. the compiler only stops constants
  5. high bits are simply discarded

basics

~20 s

Go integers wrap around silently: an int8 holding 127 becomes -128 when incremented, and a uint8 holding 0 becomes 255 when decremented. There is no panic and no promotion to a wider type. Only constant overflow is caught at compile time.

solid answer

~50 s

Go's integer types have fixed widths, and arithmetic that does not fit simply discards the high bits — two's-complement wrap-around. `var x int8 = 127; x++` leaves x at -128; `var u uint8 = 0; u--` leaves u at 255. Nothing panics and nothing widens to a bigger type on the way. Crucially this is *defined* behaviour in the Go spec, not undefined behaviour as signed overflow is in C, so the result is deterministic across platforms. The compiler only catches overflow in constant expressions, which are evaluated with arbitrary precision and must be representable in their target type: `var x int8 = 200` and `uint8(300)` fail to compile, while the same values reaching those types through variables wrap silently. If you need detection, bound-check against `math.MaxInt8` and friends before the operation, or use `math/bits` helpers like `bits.Add64`, which returns a carry out.

code

go · 9 lines
go
var x int8 = 127
x++
fmt.Println(x) // prints: -128

var u uint8 = 0
u--
fmt.Println(u) // prints: 255

// var y int8 = 200 // does not compile: the constant is out of range

go deeper

for a junior

Be ready to state the rule in one line and give the example without pausing: an int8 at 127 goes to -128, a uint8 at 0 goes to 255, and nothing complains.

for a middle

Explain the mechanism: fixed width, high bits discarded, two's complement. Know that constants are range-checked at compile time while variables are not, and that Go defines the result rather than leaving it undefined.

for a senior

Show where you place the guard in real code — bound-check at the boundary where an outside value arrives, compute in the wide type, and reach for math/bits when you need a carry. Name a counter or wire field where wrap has actually bitten you.

for a principal

Own the house rule: which widths the team uses for identifiers, sequence numbers and sizes in stored and wire formats, and what it costs to widen one later. Decide when deliberate modular arithmetic is a feature rather than a bug.

## Fixed width is the whole story Every Go integer type has a fixed size in bits. `int8`, `int16`, `int32` and `int64` are signed; `uint8`, `uint16`, `uint32` and `uint64` are unsigned; `int` and `uint` are one machine word wide on the target platform (64 bits on ordinary 64-bit builds); and `uintptr` is an unsigned integer wide enough to hold a pointer's bit pattern. `byte` is another name for `uint8` and `rune` is another name for `int32`. Because the width is fixed, a value physically cannot leave its range, so when an operation produces a result that does not fit, the bits above the width are simply thrown away. That is what "wrap-around" means. An `int8` holds values -128 through 127. Adding 1 to 127 gives the bit pattern `1000 0000`, which read as a two's-complement signed byte is -128. A `uint8` holds 0 through 255; subtracting 1 from 0 gives `1111 1111`, which is 255. An `int32` at 2147483647 rolls to -2147483648. A `uint16` at 65535 rolls to 0. ## Defined, not undefined This matters more than it first looks. In C, signed integer overflow is undefined behaviour, and optimising compilers are allowed to assume it never happens — which is how loops get deleted and bounds checks vanish. Go's specification instead says that these operations may overflow and that the resulting value exists: it is computed by discarding the high bits. So the answer is deterministic, identical on every architecture Go targets, and safe to reason about. (One curiosity that falls out of the same rule: for the most negative value of a signed type, dividing by -1 yields that same most negative value back, because the mathematically correct answer does not fit.) Wrap-around is not only a hazard — it is the point of the unsigned types. Hash mixing, checksums and pseudo-random generators are all built on deliberate modular arithmetic, and Go gives them well-defined semantics to build on. ## Where the compiler helps, and where it stops Go has untyped constants that are evaluated at compile time with arbitrary precision, and a constant must be *representable* in the type it lands in. So these are compile errors, caught before anything runs: - `var x int8 = 200` - `const big = 1 << 40; var y int32 = big` - `uint8(300)` — a conversion of a constant Once a value lives in a variable, that safety net is gone. `n := 300; b := uint8(n)` compiles happily and stores 44. The compiler emits a plain machine instruction with no check attached, because a check on every add would cost real performance in the common case where overflow cannot happen. ## There is no runtime check at all Go does not panic on integer overflow, does not set an inspectable flag, and does not return an error. The only arithmetic operation that panics is an integer division by zero. Overflow is not one of them. A counter declared `uint32` that ticks past 4294967295 quietly restarts at 0, and every downstream comparison keeps working on the wrong number. ## Detecting it when you need to Three practical techniques: 1. **Bound-check before the operation** against the `math` package limits — `math.MaxInt8`, `math.MaxInt32`, `math.MaxUint32`, `math.MaxInt64`, `math.MaxInt`. For addition of non-negative values: `if a > math.MaxInt64-b { return errOverflow }`. 2. **Compute in a wider type, then narrow after checking.** Multiply two `int32` values into an `int64`, verify the product fits, and only then convert back. 3. **Use `math/bits`.** `bits.Add64` and `bits.Sub64` return a carry/borrow alongside the result, and `bits.Mul64` returns the high and low halves of a full 128-bit product; a non-zero carry or high word *is* the overflow signal. The `min` and `max` builtins added in Go 1.21 are worth knowing here, but they solve a neighbouring problem: they pick the smallest or largest of their arguments, on any ordered type, so they help you *clamp* a value you already have. They cannot tell you that an addition already wrapped. ## What to carry into review The places wrap actually bites are boundaries and counters: a monotonic sequence number pinned to a 32-bit wire field, a duration in nanoseconds stuffed into an `int32`, a size or length that came from outside the process. Pick the width from the range the value can actually take, keep the arithmetic in the wide type, and check once at the edge rather than hoping the runtime will tell you.

  • Why does `var x int8 = 200` fail to compile while an int8 variable can reach 200's bit pattern at runtime?
    The 200 is an untyped constant, evaluated at compile time with arbitrary precision, and the spec requires it to be representable in the type it is assigned to — 200 does not fit in int8's -128..127 range, so it is an error. Runtime arithmetic on variables is a plain machine instruction with no representability check, so it wraps instead.
  • How would you detect an overflow before it happens?
    Bound-check against the limit before operating — for example `if a > math.MaxInt64-b` before `a+b` — or do the arithmetic in a wider type and verify the result fits before narrowing. For the widest types, `math/bits` gives you `bits.Add64` and `bits.Sub64`, which return a carry or borrow, and `bits.Mul64`, whose high word is non-zero exactly when the product overflowed 64 bits.
  • Do the min and max builtins help with overflow?
    No — they clamp, they do not detect. `min` and `max` were added in Go 1.21, are variadic, and work on any ordered type including strings and floats, unlike `math.Max`, which is float64-only. They are useful for capping a value into range before you convert it, but by the time an addition has wrapped there is nothing left for them to see.

It is a car odometer with a fixed number of dials. Pass the last number and it rolls quietly back to zero rather than growing an extra dial or refusing to move.

saying these in an interview costs you the question

  • Says Go panics or errors on integer overflow
  • Calls Go's signed overflow undefined behaviour, as in C
  • Expects operands to be promoted automatically to a wider type
  • Believes the compiler catches overflow in variable arithmetic
  • Thinks unsigned subtraction below zero clamps at zero
open as a page

What does iota do inside a Go const block, and what value does it start at?

level: juniorimportance: must knowfreq 78%

basics

~20 s

iota is a built-in counter usable only in const declarations: it is 0 on the first line of a const block and increases by one on each following line. Every new const block resets it to 0.

open as a page

In Go, what is the difference between type Celsius float64 and type Celsius = float64?

level: juniorimportance: must knowfreq 70%

basics

~20 s

type Celsius float64 declares a new, distinct type that shares float64's representation but has its own method set. type Celsius = float64 is an alias: Celsius and float64 are two names for one identical type.

open as a page

Why does `const r = 1.0 / 0.0` fail to compile in Go while a float64 variable divided by zero yields +Inf?

level: juniorimportance: must knowfreq 55%

basics

~20 s

The compiler evaluates constant expressions itself and rejects a constant division by zero outright. Float64 variables divide at run time under IEEE 754, so a nonzero value over zero gives positive or negative infinity, and zero over zero gives NaN.

open as a page

In Go, what do the expressions -7/2 and -7%2 evaluate to, and what rule fixes the sign of the remainder?

level: juniorimportance: must knowfreq 58%

basics

~10 s

Go truncates integer division toward zero, so -7/2 is -3, and the remainder takes the sign of the dividend, so -7%2 is -1. The identity x == (x/y)*y + x%y always holds.

open as a page

What does len("héllo") return in Go, and why isn't it 5?

level: juniorimportance: must knowfreq 85%

basics

~10 s

It returns 6. Go's len on a string counts bytes, not characters, and the letter é takes two bytes in UTF-8 while each ASCII letter takes one. Counting characters means decoding the bytes instead.

open as a page

What is a zero value in Go, and what are the zero values of int, string, bool and a pointer?

level: juniorimportance: must knowfreq 82%

basics

~20 s

Go sets every variable declared without an initializer to its type's zero value, so there is never uninitialised memory. That is 0 for numeric types, an empty string for string, false for bool, and nil for pointers, slices, maps, channels, functions and interfaces.

open as a page

What is an untyped constant in Go, and what type does it take when assigned to a variable?

level: middleimportance: must knowfreq 62%

basics

~20 s

An untyped constant has a kind - integer, float, rune, string, boolean - but no type, and the compiler evaluates it exactly. With no type available from context it takes its default type: int, float64, rune, string or bool.

open as a page

Why does ranging over the Go string "héllo" yield indices 0, 1, 3, 4, 5?

level: middleimportance: must knowfreq 66%

basics

~20 s

Because the loop hands back the byte offset where each character starts, not a sequential counter. The accented letter occupies bytes 1 and 2, so the next letter starts at 3. The second loop value is the decoded character.

open as a page

In Go, what does `var s Server` give you when Server holds an int, a nested struct, a [4]byte array and a map field?

level: middleimportance: must knowfreq 58%

basics

~20 s

You get a complete Server value with every field zeroed recursively: the int is 0, each field of the nested struct is its own zero value, all four array bytes are 0, and the map field is nil with nothing allocated for it.

open as a page

In Go, are byte and rune distinct types, or other names for uint8 and int32?

level: juniorimportance: should knowfreq 50%

basics

~20 s

They are aliases, not distinct types: byte is another name for uint8, rune another name for int32. Each pair is one type, so no conversion is ever needed and fmt's %T prints uint8 or int32.

open as a page

Why does Go use int for len and counts rather than uint, and when does unsigned bite?

level: middleimportance: should knowfreq 44%

basics

~20 s

Unsigned arithmetic wraps at zero, so subtracting a larger value gives a huge positive number instead of a negative one. Go returns int from len and cap so index arithmetic can go negative and be caught by an ordinary check.

open as a page

Does type MyMutex sync.Mutex give MyMutex a Lock method, and why?

level: middleimportance: should knowfreq 45%

basics

~20 s

No. A type definition copies the underlying type - here sync.Mutex's struct fields - but never the methods declared on the source type, so MyMutex starts with an empty method set. An alias or embedding keeps the methods.

open as a page

What precision do you lose choosing float32 over float64 in Go, and when is float32 still worth it?

level: middleimportance: should knowfreq 46%

basics

~20 s

A float32 carries a 24-bit significand, about seven decimal digits, against float64's 53 bits and about fifteen. Go never converts between the two implicitly, and the math package is float64-only, so float32 pays off only when memory or bandwidth dominates.

open as a page

In Go, what happens when an integer is divided by zero, at compile time and at run time?

level: middleimportance: should knowfreq 42%

basics

~20 s

A constant zero divisor is rejected by the compiler. A divisor that is zero only at run time causes a panic carrying a runtime.Error with the message 'integer divide by zero', which a deferred recover in the same goroutine can catch.

open as a page

Why can't you assign to s[0] on a Go string, and what do you do instead?

level: middleimportance: should knowfreq 56%

basics

~20 s

Go strings are immutable, so s[0] = 'G' is a compile error. Build a new value: convert to []byte, or to []rune when the character is not ASCII, edit it, and convert back. Both conversions copy.

open as a page

A Go service numbers ticket statuses with iota and stores them as ints. What breaks when someone inserts a new status in the middle of the const block?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Every constant below the insertion point shifts up by one, so rows written under the old numbering now read back as a different status, and nothing fails to compile. Pin persisted values explicitly and only append.

open as a page

Your units package declares Celsius and Fahrenheit as separate float64 types, yet the compiler now accepts mixing them. What changed?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Almost certainly a declaration became an alias: type Fahrenheit = Celsius. An alias makes the two names one identical type, so every unit mismatch type-checks. Look for the equals sign in the type declarations.

open as a page

A Go service's financial report renders NaN in every margin cell after one bad row. How do you find the source?

level: seniorimportance: should knowfreq 38%

basics

~20 s

NaN is contagious and silent: any arithmetic touching it yields NaN, and every comparison against it is false, so a threshold check never fires. Instrument stage boundaries with math.IsNaN and log math.Float64bits, then fix the division that made it.

open as a page

Which evaluation orders does the Go spec guarantee inside one expression, and which are left unspecified?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Go orders function calls, method calls, channel receives and the operands of && and || lexically left to right. The order of everything else, such as plain variable reads next to a call, is unspecified. Assignment evaluates all right-hand expressions and left-hand index operands first, then assigns left to right.

open as a page

A signup form truncates display names with name[:20] and some stored names now end in a replacement character. What went wrong?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Slicing a Go string cuts bytes, not characters, so name[:20] can land inside a multi-byte UTF-8 sequence. The partial bytes left behind are still a legal Go string, and only the layer that decodes them shows a replacement character.

open as a page

Your Go client library takes a Config with a Timeout time.Duration — how do you tell 'unset' from an explicit zero?

level: seniorimportance: should knowfreq 44%

basics

~20 s

From the value alone you cannot: Go zeroes every field, so an omitted Timeout and one explicitly set to 0 are identical bits. Either define zero to mean 'use the default', or make the field a pointer, or accept option functions.

open as a page

When you move a widely imported type to a new package, how do you decide between leaving an alias behind and making callers migrate?

level: principalimportance: should knowfreq 35%

basics

~20 s

Leave an alias when you cannot change every importer in one commit: it keeps the old and new names identical, so migrated and unmigrated code interoperate. Otherwise move the type and delete the old name.

open as a page

In a Go const block, how does iota repeat an omitted expression, and does _ still advance it?

level: middleimportance: nice to knowfreq 45%

basics

~20 s

Inside a parenthesized const block, a line with no expression repeats the previous line's expression, with iota re-evaluated for it. A line declaring only the blank identifier still counts, so it consumes an iota value.

open as a page

What can you write with a parameterized type alias like type Set[T comparable] = map[T]struct{} that older Go rejected?

level: middleimportance: nice to knowfreq 20%

basics

~10 s

Go 1.24 lets a type alias declare its own type parameters, so type Set[T comparable] = map[T]struct{} finally compiles. Set[string] is identical to map[string]struct{}: no conversion, and still no method set of its own.

open as a page

Why is a map entry stored under math.NaN() unreachable in Go, and how do you get rid of it?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

A float64 NaN is never equal to anything, itself included, so lookup and delete never match the entry. Each insert adds another one and len keeps growing; only clear or a fresh map removes them.

open as a page

In Go, what does a shift count larger than the operand's width produce, and what does a negative count do?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

Go does not mask shift counts. Shifting a uint64 left by 64 or more yields 0, and shifting a negative signed value right far enough yields -1. A shift count that is negative at run time panics.

open as a page

Why does `var buf bytes.Buffer` work with no constructor, and which other standard library types are usable at their zero value?

level: middleimportance: nice to knowfreq 40%

basics

~20 s

Because a bytes.Buffer's all-zero state is already a valid empty buffer: its internal byte slice is nil and writes append into a fresh one. sync.Mutex, sync.RWMutex, sync.WaitGroup, sync.Once, sync.Map, strings.Builder and http.Client are usable at zero too.

open as a page

In Go, what does uint8(n) give when n is 300, and why is that dangerous in a parser?

level: seniorimportance: nice to knowfreq 38%

basics

~20 s

It gives 44. A Go conversion between integer types never fails: it keeps the low bits and discards the rest. In a parser a hostile large value silently becomes a small plausible one that passes validation.

open as a page