In our daily work we use some words interchangeably, and people still get what we mean and it’s fine. But I think it’s worth knowing the difference. Some of them are:
- declare, define, initialize
- argument, parameter
- Unicode, code point, UTF-8
- data race, race condition
- inference, convert, cast, coerce
Let’s check each one.
Variable declaration, definition, initialization
A declaration tells the compiler “this name exists, and here’s its type”. It doesn’t necessarily reserve any storage. In C, extern int foo; declares foo without allocating any memory, only promising that foo is defined somewhere else.
A definition is what actually reserves the memory. Where that memory ends up depends on the variable: a local usually lives on the stack, a global lives in static storage. Every definition is also a declaration, and for variables only extern keeps the two apart. Without it, one line does both jobs: int foo; declares foo and reserves memory for it.
Go and Rust don’t have this split for regular variables: var x int and let x; always declare and define. The difference mostly matters in C and C++.
Initialization gives a variable a value as part of its definition.
Assignment gives a variable a value after its definition, replacing the current one if it has one.
A definition either comes with an initializer, or it doesn’t, in which case you assign a value later.
// definition with initialization
int foo = 23;
// definition without initialization
int bar;
// assignment
bar = 42;
What happens if you read a variable before it ever gets a value depends on the language:
- C: a local variable is not initialized, and reading it is undefined behavior. Globals and
staticvariables start at zero. - Go: there is no such thing as an uninitialized variable. Every variable starts at its zero value:
0,"",false,nil, and so on. - Rust: the compiler tracks whether a binding is definitely initialized.
let x;followed later byx = 5;is fine, but readingxin between is a compile error.
Argument and parameter
A parameter is the name in the function’s definition. It belongs to the function scope and is bound to a new value on every call. An argument is the value you pass at the call site.
// x and y are parameters
func add(x, y int) int {
return x + y
}
add(2, 3) // 2 and 3 are arguments
add(x, y) // x and y are arguments
Old textbooks say formal parameter for the first one and actual parameter for the second, which is probably where the mixup started.
Unicode, code point, UTF-8
I won’t explain how Unicode works in detail, just the difference between these three words. (There are pretty good YouTube videos you can find)
Unicode is a big table. It contains basically every character you can think of: ASCII letters, Farsi, Chinese, emoji and gives each one a unique number.
That number is the code point. It’s usually written in hex with a U+ prefix (A = U+0041).
Unicode doesn’t say how that number is stored in memory or in a file, so Unicode itself is not an encoding.
That’s what UTF-8 does. It’s an encoding: a rule for turning a code point into bytes. It’s variable width, so a code point takes 1 to 4 bytes depending on how big it is.
So Unicode is the table, a code point is a number in that table, and UTF-8 is the way of writing that number as bytes.
Unicode table memory (UTF-8)
┌──────┬────────────┐
│ char │ code point │
├──────┼────────────┤
│ A │ U+0041 │ ───> [41]
│ س │ U+0633 │ ───> [D8][B3]
│ 😊 │ U+1F60A │ ───> [F0][9F][98][8A]
│ 👍 │ U+1F44D │ ───> [F0][9F][91][8D] ┐
│ 🏽 │ U+1F3FD │ ───> [F0][9F][8F][BD] ┘ 👍🏽 = 8 bytes
└──────┴────────────┘
- ASCII characters take 1 byte, the same byte as in ASCII, so every ASCII file is already valid UTF-8.
- Some characters are made by combining code points, for example emojis with skin tones:
👍🏽is👍(U+1F44D) + skin tone (U+1F3FD), so it takes 8 bytes, not 4.
Data race and race condition
A data race happens when two or more threads (or goroutines) access the same memory at the same time, without synchronization, and at least one of them writes to it.
num := 10
var wg sync.WaitGroup
wg.Go(func() { num++ })
wg.Go(func() { fmt.Println(num) })
wg.Wait()
go run -race will report it.
A race condition happens when an unpredictable order of operations leads to an incorrect system state. In a concurrent environment, we can’t control the exact order things happen. Still, we need to make sure that no matter the order, the system always ends up in the correct state.
balance := 100
var wg sync.WaitGroup
var mu sync.Mutex // Mutex prevents data races, but not race conditions
withdraw := func(amount int) {
// check
mu.Lock()
ok := balance >= amount
mu.Unlock()
if !ok {
fmt.Printf("failed to withdraw $%d: insufficient funds\n", amount)
return
}
// Simulating some processing delay
time.Sleep(10 * time.Millisecond)
// act
mu.Lock()
balance -= amount
mu.Unlock()
fmt.Printf("withdrew $%d\n", amount)
}
wg.Go(func() { withdraw(80) }) // Transaction 1
wg.Go(func() { withdraw(50) }) // Transaction 2
wg.Wait()
fmt.Println("final balance:", balance) // usually -30
Every read and write of balance is behind the mutex, so there’s no data race and go run -race won’t report it. But checking the balance and withdrawing are two separate steps. Both goroutines check while the balance is still 100, both pass, both withdraw, and the balance ends at -30.
Inference, convert, cast, coerce
- Inference: The compiler automatically figures out types that were not explicitly annotated (
let x = 5in Rust). It’s not a conversion; nothing changes type. - Conversion: Any mechanism for turning a value of one type into another, whether explicit or implicit.
- Cast: An explicit conversion written directly in code, usually using language syntax (
x as u64in Rust,(int)xin C). - Coercion: An implicit conversion performed automatically by the compiler without explicit syntax (
&Stringto&strin Rust). - Promotion: A special kind of coercion: small numeric types get converted to a bigger one automatically (
charandshorttointin C). Rust never does it.