In Go, are byte and rune distinct types, or other names for uint8 and int32?
answer
- a nickname, not a different person
- one type, two spellings
- what does %T actually print
- no conversion needed either way
- one means data, the other a code point
basics
~20 sThey 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.
solid answer
~40 sThe Go spec declares `byte` as an alias for `uint8` and `rune` as an alias for `int32` — alias declarations, not new defined types. That means they are *identical* types, not merely convertible ones: you can assign a `byte` to a `uint8` variable with no conversion, a `[]byte` is a `[]uint8`, and `fmt.Printf("%T", byte('A'))` prints `uint8`. The two names exist purely to carry intent. `byte` says "this is raw binary data", which is why `io.Reader` and `io.Writer` traffic in `[]byte`; `rune` says "this is a Unicode code point", which is why an untyped character literal like `'A'` defaults to `rune` and why `utf8.MaxRune` is 0x10FFFF — comfortably inside int32. A defined type such as `type Flag byte` is the opposite: that one *is* distinct, needs an explicit conversion, and can carry its own methods.
code
go · 9 linesvar b byte = 'A'
var u uint8 = b // no conversion: byte and uint8 are one type
_ = u
fmt.Printf("%T %T\n", b, 'A') // prints: uint8 int32
type Flag byte // a defined type instead: distinct, needs a conversion
var f Flag = Flag(b)
_ = fgo deeper
Recall the two pairings cold — byte is uint8, rune is int32 — and that they are aliases, so no conversion is ever needed between the two spellings.
Explain what alias identity implies: []byte and []uint8 are the same type, %T reports the underlying name, and methods cannot be declared on either spelling without introducing a defined type first.
Show the judgment in API design — which spelling you choose in an exported signature and what it signals to callers, and when you promote a value to a real defined type so the compiler can police mixing.
Own the convention across a codebase: where raw byte slices are allowed to travel versus where a named domain type is required, and what the migration costs once other teams already depend on the exported signature.
## Alias versus defined type Go has two ways to introduce a type name, and they behave very differently. ```go type MyByte uint8 // defined type: new, distinct, convertible type MyAlias = uint8 // alias: the same type, another spelling ``` The predeclared identifiers `byte` and `rune` are the second kind. The specification states, in as many words, that `byte` is an alias for `uint8` and `rune` is an alias for `int32`. There is no wrapper, no zero-cost conversion, no distinct method set — the compiler treats the two spellings as one type from the moment it parses them. Several consequences follow directly: - **Assignment needs no conversion.** `var b byte = 'A'; var u uint8 = b` compiles. So does the reverse. - **Composite types match too.** A `[]byte` *is* a `[]uint8`, a `map[rune]int` *is* a `map[int32]int`, and a function declared `func(p []byte)` satisfies an interface method declared `func(p []uint8)`. - **Reflection and formatting show the underlying name.** `fmt.Printf("%T", byte('A'))` prints `uint8`, never `byte`, because there is no separate type for reflection to report. - **You cannot hang methods on them.** Methods may only be declared on a defined type in the same package, and `uint8` is predeclared. To attach behaviour you must first write `type Flag byte`, which creates a genuinely new type. ## Why two names for the same thing Because the names document intent, and in Go intent is most of what a type name buys you. `byte` means "an octet of data whose numeric value is incidental". This is why the whole I/O layer is built on `[]byte`: `io.Reader.Read(p []byte)`, `io.Writer.Write(p []byte)`, `bytes.Buffer`, `os.ReadFile`. When you see `[]byte` you should be thinking about bytes on a wire or on disk, not about small numbers. `rune` means "a single Unicode code point". Go source is UTF-8, and a character literal such as `'A'`, `'é'` or `'\u00e9'` is an untyped rune constant whose default type is `rune`. The `unicode` package works in runes: `unicode.IsLetter(r rune) bool`, `unicode.ToUpper(r rune) rune`. `utf8.MaxRune` is 0x10FFFF, the highest legal code point, and `utf8.RuneError` is the replacement character U+FFFD. When the numeric value genuinely *is* the point, the standard library uses the plain names instead. `math/bits` declares `bits.LeadingZeros8(x uint8) int`, not a byte parameter, because there the argument is a number to be counted, not data to be moved. ## Why int32 and not uint32 for rune Unicode code points run from 0 to 0x10FFFF, which needs 21 bits, so any 32-bit type is roomy enough. Signedness was the pragmatic choice: it keeps rune arithmetic consistent with Go's other default-signed integer types, and it leaves negative values available as out-of-band markers for code that needs one. Nothing about UTF-8 requires the sign bit; it is simply free. ## The mistake this question is hunting for Candidates who have only read code often assume `byte` is a distinct type that the compiler quietly converts, and are then surprised that `%T` reports `uint8`, or that a `[]byte` argument can be passed where `[]uint8` is declared. The deeper version of the same confusion is expecting `type Celsius float64` to behave like an alias — it does not. A defined type is a new type with its own identity and its own method set, and mixing it with its underlying type requires an explicit conversion. Alias, identical; defined type, distinct. `byte` and `rune` sit firmly on the alias side. ## Practical guidance Write `byte` when the value is data and `rune` when the value is a character; write `uint8` or `int32` when the value is a number whose width you are pinning deliberately. It changes nothing for the compiler and a great deal for the next reader. And when you want a type the compiler will actually police — a status code that must not be mixed with an ordinary count, say — reach for a defined type rather than an alias.
- If byte and uint8 are the same type, why does the standard library use both spellings?To document intent. The I/O layer takes `[]byte` because the argument is data being moved — `io.Reader.Read(p []byte)`, `io.Writer.Write(p []byte)`, `bytes.Buffer`. Packages where the argument is a number use the numeric spelling, as `math/bits` does with `bits.LeadingZeros8(x uint8) int`. The compiler sees no difference; the reader sees a large one.
- Can you declare a method on byte?No. Methods may only be declared on a defined type declared in the same package, and byte is just another spelling of the predeclared uint8. You have to introduce a real type first — `type Flag byte` — which is distinct from byte, requires an explicit conversion in both directions, and can then carry methods such as a String method.
- Why is rune signed?It does not need to be for range reasons: code points stop at utf8.MaxRune, 0x10FFFF, which fits in 21 bits. int32 was chosen so rune arithmetic behaves like Go's other, signed-by-default integers, and so a negative value remains available as an out-of-band marker. An unsigned 32-bit type would have worked equally well for storage.
It is a nickname rather than a second person: calling someone Bob instead of Robert does not create a new individual, and the passport still says Robert.
saying these in an interview costs you the question
- Says byte is a distinct type the compiler converts for you
- Expects fmt's %T to print byte or rune
- Thinks rune is unsigned or 16 bits wide
- Claims a []byte needs converting to pass as a []uint8
- Confuses an alias with a defined type such as type Flag byte