val & var
val ยท var ยท immutabilityKotlin splits variable declaration into two keywords instead of one. val declares a reference you can assign once; var declares one you can reassign. There's no plain var-only world like Go's :=: you choose mutability at the point of declaration, and the compiler enforces it. This isn't ceremony โ it's a design bet that most bindings in a correct program never need to change, and making that visible catches bugs where a value was reassigned by accident.
// val binds a reference once; reassignment is a compile error val name = "Ada" val year: Int = 2024 // name = "Grace" // ERROR: val cannot be reassigned // val is not deep immutability: it freezes the REFERENCE, // not necessarily the object it points to val list = mutableListOf(1, 2, 3) list.add(4) // fine: mutating contents, not the reference // list = mutableListOf() // ERROR: can't rebind list itself
// var allows reassignment, same type only var count = 0 count = 1 count += 1 // 2 // count = "two" // ERROR: type is fixed after inference var attempts: Int = 3 while (attempts > 0) { attempts-- }
val. Reach for var only when you have a concrete reason to reassign โ a loop counter, an accumulator, mutable state you're intentionally modeling. Kotlin style guides and every linter treat an unnecessary var as a smell, because it widens the set of places a bug can hide.:= and var both produce mutable bindings โ there's no equivalent of "this can never be reassigned" baked into the syntax itself (you'd reach for a package-level const, which only works for compile-time primitives). Kotlin's val gives you that guarantee for any type, including objects built at runtime.Type Inference
inferred ยท explicit ยท NothingKotlin is statically typed, but you rarely write the type out. The compiler infers it from the initializer expression at the declaration site, and that inferred type is then fixed for the life of the binding โ this isn't dynamic typing wearing a disguise. You write the type explicitly when the initializer's type is ambiguous, when you want a wider type than what's inferred (like declaring a variable as an interface type even though you're assigning a concrete implementation), or simply for documentation on public API signatures.
// Inferred: the compiler reads the type off the right-hand side val age = 30 // Int val price = 19.99 // Double val label = "widget" // String val ready = true // Boolean // Explicit: needed when there's no initializer, or you want // a wider/different type than the default inference val id: Long // no initializer: must state the type id = 42L val handler: Runnable = Runnable { println("run") } // declared as the interface, even though the value is a lambda impl // Function return types are inferred for single-expression functions, // but required on any function with a block body fun square(n: Int) = n * n // inferred: Int fun cube(n: Int): Int { return n * n * n } // required
Int, decimals as Double. Use suffixes to widen a literal: 42L for Long, 3.14f for Float, 100u for UInt.String Templates
$var ยท ${expr} ยท raw stringsInstead of a separate formatting function, Kotlin lets you embed expressions directly inside string literals. $name substitutes a variable; ${expression} substitutes the result of any expression, including method calls. It compiles down to ordinary StringBuilder concatenation, so there's no runtime formatting cost beyond what you'd write by hand โ the syntax is just shorter and reads left-to-right the way the output will.
val name = "Ada" val age = 36 // $var: simple variable substitution println("Hello, $name! You are $age.") // prints: Hello, Ada! You are 36. // ${expr}: any expression, including calls and property access println("Name has ${name.length} letters") // prints: Name has 3 letters println("Next year: ${age + 1}") // prints: Next year: 37 // A literal $ or braces need escaping println("Price: \$${9.99}") // prints: Price: $9.99
// Triple-quoted strings need no escaping and can span lines val json = """ { "name": "$name", "age": $age } """.trimIndent() // Great for regexes, since backslashes stay literal val pattern = """\d{3}-\d{4}""".toRegex() // trimIndent() strips the common leading whitespace from every line, // so you can indent the literal to match surrounding code
fmt.Sprintf("Hello, %s! You are %d.", name, age). There's no format verb to get wrong โ the compiler type-checks the expression inside ${} the same way it type-checks any other code, and calling .toString() happens automatically.when
expression ยท statement ยท exhaustivewhen replaces both the C-style switch and long if/else if chains. It's not restricted to constant cases โ each branch can test an arbitrary condition, a type, a range, or a set of values, and it can be used as either a statement (for control flow) or an expression (that produces a value). When used as an expression over a sealed type or an enum, the compiler forces you to cover every case, which turns a whole category of runtime bugs into compile errors.
val score = 82 // No "break" needed: branches don't fall through val grade = when { score >= 90 -> "A" score >= 80 -> "B" score >= 70 -> "C" else -> "F" } println(grade) // B // Matching a subject value against multiple candidates val day = "SAT" val kind = when (day) { "SAT", "SUN" -> "weekend" else -> "weekday" }
fun describe(x: Any): String = when (x) { is Int -> "int: $x" // type check, x is smart-cast to Int here is String -> "string of length ${x.length}" in 1..10 -> "small number" // range check null -> "nothing" else -> "unknown: $x" } // Branches can hold arbitrary boolean expressions too fun classify(n: Int) = when { n < 0 -> "negative" n == 0 -> "zero" n % 2 == 0 -> "even" else -> "odd" }
enum class Direction { NORTH, SOUTH, EAST, WEST } // Used as an expression, when must cover every enum constant. // Add a new constant later and this fails to compile until you handle it. fun heading(d: Direction): Int = when (d) { Direction.NORTH -> 0 Direction.EAST -> 90 Direction.SOUTH -> 180 Direction.WEST -> 270 // no else needed: every constant is covered }
when is one of Kotlin's best interview-relevant habits, and it pairs directly with sealed classes (see Classes & Objects). When a when is used as a statement (its result discarded), the compiler does not require exhaustiveness or an else โ only expression position triggers the check. Assign the result to something, even just returning it, to get the safety net.switch doesn't fall through by default either, so the control flow feels familiar. The real difference is that Kotlin's when can be an expression that returns a value, and combined with a sealed hierarchy it gives you exhaustiveness checking that Go's type system has no equivalent for โ Go has no sum types, so a missing case in a type switch just silently falls to default.Ranges in Control Flow
.. ยท ..< ยท downTo ยท stepA range is a first-class value describing an interval between two endpoints, and it's the idiomatic replacement for a C-style counting loop. 1..5 builds an IntRange you can iterate, test membership against with in, or pass around like any other object. The full operator set โ construction, step size, and direction โ is covered in depth in the Ranges & Progressions guide; here's what you need to read and write loops.
// 1..5 is inclusive on both ends: 1, 2, 3, 4, 5 for (i in 1..5) print("$i ") // 1 2 3 4 5 // ..< excludes the end (Kotlin 1.7+); reads like most index loops for (i in 0..<5) print("$i ") // 0 1 2 3 4 // downTo counts backward for (i in 5 downTo 1) print("$i ") // 5 4 3 2 1 // step changes the increment for (i in 0..10 step 2) print("$i ") // 0 2 4 6 8 10 // in tests membership: no loop involved val valid = 7 in 1..10 // true
0..<n over the older 0 until n in new Kotlin 2.x code โ same behavior, shorter, and it visually mirrors the mathematical half-open interval notation [0, n).Loops
for ยท while ยท do-while ยท labelsKotlin has the three loop forms you'd expect โ for, while, do-while โ but no C-style three-clause for (init; cond; step). for only iterates over something iterable: a range, a collection, or anything exposing an iterator() function. That constraint is deliberate โ it removes an entire class of off-by-one bugs that come from hand-rolled counters.
val fruits = listOf("apple", "banana", "cherry") // Direct element iteration: no index bookkeeping for (fruit in fruits) { println(fruit) } // withIndex() when you need both for ((index, fruit) in fruits.withIndex()) { println("$index: $fruit") } // 0: apple, 1: banana, 2: cherry // indices gives you a range over valid index positions for (i in fruits.indices) { println("${fruits[i]} at $i") } // Destructuring a Map's entries directly in the loop header val ages = mapOf("Ada" to 36, "Grace" to 85) for ((name, age) in ages) { println("$name is $age") }
var n = 1 while (n < 100) { n *= 2 } println(n) // 128 // do-while runs the body at least once, // checking the condition after var attempts = 0 do { attempts++ } while (attempts < 3) println(attempts) // 3
// A label lets break/continue target an outer loop outer@ for (i in 1..3) { for (j in 1..3) { if (j == 2) continue@outer println("$i,$j") } } // 1,1 2,1 3,1 (j==2 skips straight to the next i)
for keyword covers all three forms by varying its clauses; Kotlin spells them out as for/while/do-while. Kotlin's for is closer to Go's for range than to Go's counting for โ there's no direct equivalent of for i := 0; i < n; i++ because Kotlin pushes you toward iterating a range instead.Functions
fun ยท default args ยท named args ยท varargFunctions are declared with fun and can live at the top level of a file โ Kotlin doesn't force every function into a class the way Java does. Two features do a lot of work here: default parameter values remove the need for overloads, and named arguments let a call site read like a sentence regardless of parameter order. Together they largely replace the "builder pattern" and telescoping-constructor problems you'd solve differently in other languages.
// Block body: explicit return type required fun add(a: Int, b: Int): Int { return a + b } // Single-expression body: "= expr" instead of a block, // return type is inferred fun multiply(a: Int, b: Int) = a * b // Unit is Kotlin's "no meaningful value" type (like Go's absence // of a return type). It's almost always omitted. fun logMessage(msg: String): Unit { println(msg) } // equivalent, and idiomatic: fun logMessage2(msg: String) { println(msg) }
// Parameters can have default values fun greet(name: String, greeting: String = "Hello", punctuation: String = "!") { println("$greeting, $name$punctuation") } greet("Ada") // Hello, Ada! greet("Ada", "Hi") // Hi, Ada! // Named arguments: skip earlier defaults, any order, self-documenting greet(name = "Ada", punctuation = "?!") // Hello, Ada?! greet(punctuation = ".", name = "Ada") // order doesn't matter with names // Named args are especially valuable when several params // share a type: prevents silently swapped arguments fun createUser(firstName: String, lastName: String, isAdmin: Boolean = false) { /* ... */ } createUser(firstName = "Grace", lastName = "Hopper", isAdmin = true)
// vararg collects trailing args into an Array inside the function fun sum(vararg numbers: Int): Int = numbers.sum() sum(1, 2, 3) // 6 sum() // 0: zero args is fine // Spread operator * expands an existing array into vararg position val nums = intArrayOf(4, 5, 6) sum(*nums) // 15
...Option functional-options pattern or a config struct. Kotlin gives you the same ergonomics directly in the function signature, no extra types required.Expressions vs Statements
if-expr ยท when-expr ยท UnitA statement is executed for its effect and produces no usable value; an expression evaluates to a value you can assign, pass, or return. In Go, if is purely a statement โ you can't write x := if cond { 1 } else { 2 }. In Kotlin, if and when are expressions: they evaluate to a value when every branch supplies one, and this single fact eliminates a lot of pre-declared-mutable-variable boilerplate.
Statement Expression
if (cond) { val x = if (cond) {
doWork() 1
} } else {
2
โ runs for effect }
โ produces nothing usable
โ evaluates to a value
โ assignable, returnable, passable
val a = 10 val b = 20 // The old way: mutable placeholder, reassigned in each branch var max: Int if (a > b) { max = a } else { max = b } // Idiomatic: if is an expression, both branches must produce a value val max2 = if (a > b) a else b // Kotlin has no ternary operator (cond ? a : b): if-expression is it val label = if (max2 % 2 == 0) "even" else "odd"
// A single-expression function body is itself an expression, // so an if/when as the body needs no explicit "return" fun max(a: Int, b: Int) = if (a > b) a else b fun sign(n: Int) = when { n > 0 -> 1 n < 0 -> -1 else -> 0 } // Everything (even a block) technically has a type in Kotlin; // statements like assignments and loops evaluate to Unit, // the type with exactly one value, roughly Go's absence of a result val nothingUseful: Unit = println("side effect")
var declarations, less repeated logic across branches, and functions that read as a single flow of data instead of a sequence of imperative steps.if/switch as statements to keep control flow uniform and easy to scan; Kotlin trades that uniformity for the ability to compute a value inline. Neither is wrong โ but expect to unlearn the "declare mutable, then branch-assign" reflex.Quick Reference
Syntax cheat-sheet| Concept | Syntax | Notes |
|---|---|---|
| Immutable binding | val x = 1 | Reference fixed; contents may still be mutable |
| Mutable binding | var x = 1 | Reassignable, type fixed after inference |
| Explicit type | val x: Int = 1 | Required when there's no initializer |
| String template | "Hi $name" | Use ${expr} for anything beyond a bare identifier |
| Raw string | """text""" | No escaping needed; pair with .trimIndent() |
| when expression | val x = when(y) { ... } | Exhaustive over sealed/enum in expression position |
| Range (inclusive) | 1..5 | 1,2,3,4,5 |
| Range (exclusive end) | 0..<5 | 0,1,2,3,4 โ Kotlin 1.7+ |
| Reverse range | 5 downTo 1 | 5,4,3,2,1 |
| Stepped range | 0..10 step 2 | 0,2,4,6,8,10 |
| Membership test | x in 1..10 | No loop; boolean expression |
| Indexed loop | for ((i, v) in list.withIndex()) | Avoids manual index bookkeeping |
| Labeled break | break@outer | Targets an outer labeled loop |
| Single-expression function | fun f(x: Int) = x * 2 | Return type inferred |
| Default parameter | fun f(x: Int = 0) | Removes need for overloads |
| Named argument | f(x = 1, y = 2) | Any order; self-documenting at call site |
| Variadic parameter | fun f(vararg xs: Int) | xs is Array<Int> inside f |
| Spread operator | f(*array) | Expands an array into vararg position |
| if as expression | val x = if (c) a else b | Kotlin's ternary substitute |
| No-value type | Unit | Roughly Go's absence of a return type |