Kotlin Basics

val/var, type inference, string templates, when, ranges, loops, functions, and the shape of an idiomatic Kotlin statement.

val / var Type inference String templates when Ranges Loops Functions Expressions
๐Ÿ”’

val & var

Kotlin 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: single assignment val
// 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: reassignable var
// 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--
}
๐Ÿ’ก
Default to 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.
๐Ÿน
Coming from Go: Go's := 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

Kotlin 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 vs explicit types Inference
// 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
โ„น๏ธ
Numeric literals default to the smallest type that fits idiomatically: whole numbers infer as Int, decimals as Double. Use suffixes to widen a literal: 42L for Long, 3.14f for Float, 100u for UInt.
๐Ÿ’ก
On public functions and properties, write the type explicitly even when inference would work. It's a stable contract for callers, and it stops an accidental change to the implementation from silently widening or narrowing the exposed type.
๐Ÿงต

String Templates

Instead 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.

Template syntax Templates
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
Raw strings (triple-quoted) """
// 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
๐Ÿน
Coming from Go: this replaces 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

when 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.

when as an expression Expression
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"
}
Branch forms: ranges, types, arbitrary conditions Patterns
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"
}
Exhaustiveness over sealed types and enums Exhaustive
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
}
๐Ÿ’ก
Exhaustive 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.
๐Ÿน
Coming from Go: Go's 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

A 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.

Range basics Ranges
// 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
๐Ÿ’ก
Prefer 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

Kotlin 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.

for: over ranges and collections for
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")
}
while & do-while while
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
Labeled break & continue Labels
// 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)
๐Ÿน
Coming from Go: Go's single 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

Functions 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.

Declaration & single-expression form fun
// 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) }
Default & named arguments Defaults
// 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 parameters vararg
// 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
๐Ÿ’ก
Default arguments are why Kotlin code almost never needs Java-style method overloading or the builder pattern for simple cases. A single function with sensible defaults covers what would otherwise be three or four overloaded signatures.
๐Ÿน
Coming from Go: Go has no default parameter values or named arguments โ€” the usual workaround is a variadic ...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

A 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
if as an expression if-expr
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"
Returning from a function directly return
// 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")
๐Ÿ’ก
Preferring expressions over statements is one of the clearest "idiomatic Kotlin" signals interviewers look for. It usually means fewer var declarations, less repeated logic across branches, and functions that read as a single flow of data instead of a sequence of imperative steps.
๐Ÿน
Coming from Go: this is the single biggest syntactic mental shift coming from Go. Go deliberately keeps 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

Concept Syntax Notes
Immutable bindingval x = 1Reference fixed; contents may still be mutable
Mutable bindingvar x = 1Reassignable, type fixed after inference
Explicit typeval x: Int = 1Required 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 expressionval x = when(y) { ... }Exhaustive over sealed/enum in expression position
Range (inclusive)1..51,2,3,4,5
Range (exclusive end)0..<50,1,2,3,4 โ€” Kotlin 1.7+
Reverse range5 downTo 15,4,3,2,1
Stepped range0..10 step 20,2,4,6,8,10
Membership testx in 1..10No loop; boolean expression
Indexed loopfor ((i, v) in list.withIndex())Avoids manual index bookkeeping
Labeled breakbreak@outerTargets an outer labeled loop
Single-expression functionfun f(x: Int) = x * 2Return type inferred
Default parameterfun f(x: Int = 0)Removes need for overloads
Named argumentf(x = 1, y = 2)Any order; self-documenting at call site
Variadic parameterfun f(vararg xs: Int)xs is Array<Int> inside f
Spread operatorf(*array)Expands an array into vararg position
if as expressionval x = if (c) a else bKotlin's ternary substitute
No-value typeUnitRoughly Go's absence of a return type