skip to content

Identity and Assignability

The rules that hold across every construct rather than inside one: when two types are the same, when one may be assigned to another, which values support == and may key a map, and what nil means.

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

explore

questions

12

Given `type UserID string`, why does `var id UserID = "u-1"` compile but `var s string = id` not?

level: juniorimportance: must knowfreq 70%

answer

  1. two names, one shape
  2. the rule needs one side unnamed
  3. string is a named type too
  4. the literal has no type yet

basics

~20 s

An untyped string constant is assignable to any type whose underlying type is string, so the literal works. But UserID and string are two distinct named types, so moving a value between them needs an explicit conversion: string(id).

solid answer

~50 s

Two different rules are at work. `"u-1"` is an untyped constant, and an untyped constant is assignable to any type it is representable in, so it happily becomes a `UserID`. The variable case falls under a different clause: a value of type V is assignable to type T when V and T have identical underlying types **and at least one of them is not a named type**. `UserID` and `string` have the same underlying type, but predeclared types like `string` count as named types too, so both sides are named and the assignment is rejected. You write `string(id)` instead — a conversion, which is the wider relation and costs nothing at run time here. The rule flips for composite types: with `type IDList []string`, `[]string` is an unnamed type literal, so `IDList` and `[]string` assign freely in both directions.

code

go · 15 lines
go
type UserID string
type IDList []string

// ok: untyped constant, representable as a string
var id UserID = "u-1"

// compile error: cannot use id (variable of type UserID) as string value
// var s string = id

// ok: conversion is the wider relation, and costs nothing here
var s = string(id)

// ok both ways: []string is an unnamed type
var l IDList = []string{"a", "b"}
var raw []string = l

go deeper

for a junior

Be ready to state that a type declared from string is a new type, not a nickname, and that moving a value between the two needs an explicit conversion while a string literal does not.

for a middle

Explain the actual clause: identical underlying types plus at least one side not a named type, and note that predeclared types count as named. Show the slice case where the rule flips.

for a senior

Show the judgment: where you place conversions at package boundaries so raw strings cannot leak into domain identifiers, and how the compiler error naming both types tells you it is an assignability failure.

for a principal

Own the tradeoff of introducing defined identifier types across a codebase: the conversion noise at every boundary is the price of the compile-time barrier, and it is worth paying only where mixing up two ids is a real incident.

## Two questions that look like one `var id UserID = "u-1"` and `var s string = id` look like the same operation, but Go checks them under different clauses of the assignability rule, and only one of them passes. ### Assignability, briefly A value `x` of type `V` is *assignable* to a variable of type `T` (which also covers passing an argument, returning a result, and sending on a channel) if one of a short list of conditions holds. The three that matter here: 1. `V` and `T` are **identical** types. 2. `V` and `T` have **identical underlying types** *and at least one of `V` or `T` is not a named type*. 3. `x` is an **untyped constant representable** by a value of type `T`. A *defined type* is one introduced by a `type` declaration: `type UserID string` creates a brand-new type whose **underlying type** is `string`. It is not a nickname for `string`; it is a distinct type that happens to be laid out the same way. ### Why the literal works `"u-1"` has no type of its own. It is an untyped string constant, and clause 3 applies: it is representable as a `UserID`, so it is assignable to one. The same is true of `var c Celsius = 36.6` for `type Celsius float64`, and of `id + "-suffix"`, where the untyped constant takes on the type of the other operand. Untyped constants are the reason defined types feel ergonomic at all — you almost never write `UserID("u-1")`. ### Why the variable does not `id` is not an untyped constant; it is a value of type `UserID`. Clause 1 fails: `UserID` and `string` are not identical. Clause 2 is the interesting one — the underlying types *are* identical (both `string`), so everything hinges on "at least one is not a named type". Go's definition of **named type** covers predeclared types, defined types and type parameters. `string` is predeclared, therefore named. `UserID` is defined, therefore named. Two named types, so clause 2 fails and the compiler rejects the assignment, naming both types in the error: ``` cannot use id (variable of type UserID) as string value in variable declaration ``` That error, naming both sides, is the fingerprint of an assignability failure rather than a syntax or method-set problem. ### Where the rule flips: unnamed types Composite type literals — `[]string`, `map[string]int`, `chan int`, `*T`, `struct{...}`, `func(int) error` — are **unnamed**. So with: ```go type IDList []string ``` both directions assign without a conversion, because `[]string` is unnamed and satisfies the "at least one" half of clause 2: ```go var l IDList = []string{"a", "b"} var raw []string = l ``` This is why wrapping a slice or map in a defined type is cheap to adopt, while wrapping a `string` or an `int` forces conversions at every boundary. It is also why the same trick does *not* rescue you between two defined types: `type A []string` and `type B []string` are both named, so `var a A = someB` fails even though both underlying types are `[]string`. ### The conversion, and what it costs `string(id)` is a **conversion**, and conversion is the wider relation: anything assignable is convertible, plus more. Converting between two types with identical underlying types is permitted regardless of how many of them are named, and at run time it is a no-op — the bytes are already in the right shape, nothing is copied and no allocation happens. That is very different from `[]byte(s)`, which really does copy, because a string and a byte slice have different representations and strings are immutable. ### Assignability to interfaces and nil Two more clauses of the same rule are worth knowing because they explain why so much Go code compiles without conversions: a value is assignable to an interface type if its type implements that interface (so `UserID` is assignable to `any`), and the predeclared identifier `nil` is assignable to any pointer, function, slice, map, channel or interface type. ### Why the language is like this The strictness is the whole point of declaring `type UserID string`. If `UserID` and `string` assigned freely, the defined type would buy you documentation and nothing else. Because they do not, a function taking a `UserID` cannot silently be handed an arbitrary string, and every place a raw string crosses into your domain has to say so out loud with a conversion — which is exactly the line a reviewer wants to see.

  • Why does `var b byte = 300` fail to compile when `var b byte = 200` is fine?
    Both are untyped integer constants, and that clause of the assignability rule requires the constant to be *representable* in the target type. 200 fits in a byte; 300 does not, so the compiler reports that the constant overflows. Representability is checked at compile time with arbitrary precision, which is why constant expressions can hold values no `byte` variable ever could.
  • Is a `UserID` assignable to a parameter of type `any` without a conversion?
    Yes. A separate clause of the same rule says a value is assignable to an interface type when its type implements that interface, and every type implements the empty interface `any`. That clause never asks about named versus unnamed types, so `fmt.Println(id)` works even though `var s string = id` does not.
  • Does `id + "-v2"` compile when `id` is a `UserID`?
    Yes. `"-v2"` is an untyped string constant, so it takes on the type of the other operand and the expression has type `UserID`. The same is true of comparisons against string literals. It stops working the moment the other side is a typed `string` variable, which is back to the two-named-types problem.

A defined type is a labelled jar, not a different substance: the sugar inside is identical, but you still have to pour it out of one jar and into the other on purpose.

saying these in an interview costs you the question

  • Says Go implicitly converts a defined type to its underlying type
  • Claims predeclared types like string are unnamed, so the assignment should work
  • Thinks string(id) copies bytes the way []byte(s) does
  • Believes the literal works because of a run-time conversion
open as a page

Which Go types can be compared with `==`, and which ones fail to compile?

level: juniorimportance: must knowfreq 74%

basics

~20 s

Booleans, numbers, strings, pointers, channels, interfaces, and structs or arrays built entirely from comparable parts support ==. Slices, maps and functions do not: the compiler rejects ==, and the only equality they allow is a comparison against nil.

open as a page

In Go, which kinds of type can hold nil, and does nil mean the same thing for each?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Six kinds of type can be nil: pointers, slices, maps, channels, functions and interfaces. nil is the zero value of each, not one shared value, so every kind has its own representation and its own rules about what you may safely do.

open as a page

Why can a `chan int` be assigned to a `<-chan int` variable, but never the reverse?

level: middleimportance: should knowfreq 45%

basics

~20 s

A bidirectional channel may be assigned to a receive-only or send-only variable, because that only removes capability. The reverse would invent capability the value never had, so it is neither assignable nor convertible. Direction is part of the static type.

open as a page

What must be true for two Go interface values to be equal with `==`?

level: middleimportance: should knowfreq 46%

basics

~20 s

Two interface values are equal only when both hold nothing at all, or when their dynamic types are identical and their dynamic values are equal. An any holding int(1) never equals an any holding int64(1).

open as a page

What makes a Go struct type usable as a map key, and what disqualifies it?

level: middleimportance: should knowfreq 58%

basics

~20 s

A struct can key a Go map only if every field type is comparable, applied recursively. One slice, map or function field disqualifies it at compile time; replace that field with an array, a string or another canonicalised value.

open as a page

Why can a Go method call on a nil *T pointer receiver work, and when does it panic?

level: middleimportance: should knowfreq 45%

basics

~20 s

A pointer receiver is just the method's first argument, and nil is a legal value for it, so the call itself never dereferences anything. It panics only if the body reads through the pointer - or immediately if the method has a value receiver.

open as a page

What is the difference between a nil slice and an empty non-nil slice in Go?

level: middleimportance: should knowfreq 62%

basics

~20 s

Both have length and capacity 0, and both work with len, range and append. The differences are that only the nil slice equals nil, and that encoding/json writes a nil slice as null and an empty non-nil slice as [].

open as a page

With `type Seconds float64` and `type Millis float64`, why does `Millis(s)` compile, and how do you stop unit mix-ups?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Conversion between two defined types with the same underlying type is a pure relabel: 30 seconds becomes 30 millis, unscaled. Defined types block accidental assignment, never a written conversion. Put the scaling in named methods.

open as a page

Why can Go convert between two struct types whose fields differ only in their tags, yet not assign one to the other?

level: middleimportance: nice to knowfreq 26%

basics

~10 s

Struct tags count toward type identity, so two structs differing only in tags are not assignable. The conversion rule explicitly ignores tags, so T(v) between them is legal. Conversion is the wider relation.

open as a page

Why does a Go `map[any]bool` compile fine but panic when some keys are inserted?

level: seniorimportance: nice to knowfreq 33%

basics

~20 s

The static key type any is comparable, so the map compiles. At run time the map must hash the key's dynamic type, and a slice, map or function value cannot be hashed, so the runtime panics naming that type.

open as a page

A Go CLI panics with a nil pointer dereference on a line calling a func-typed struct field. How do you diagnose and fix it?

level: seniorimportance: nice to knowfreq 34%

basics

~20 s

Calling a nil function value produces the same message as a nil pointer dereference, but the signal line shows pc=0x0 and the callee has no stack frame. The fix is to install no-op defaults in the constructor, or to guard the call with a nil check.

open as a page