DEV Community

Cover image for The Silent Convergence: Why Go, Rust, and Kotlin Are Headed in the Same Direction — And Why It Matters
Uy Trần
Uy Trần

Posted on

The Silent Convergence: Why Go, Rust, and Kotlin Are Headed in the Same Direction — And Why It Matters

The Feeling

If you've spent years in Java or C# and recently picked up Rust or Kotlin, you know the feeling. It's not that the language is easy. It's that the room is suddenly decluttered.

No more public static final. No Map<String, List<User>> registry = new HashMap<String, List<User>>(). No semicolons demanding tribute at the end of every line.

I remember writing my first non-trivial Rust function. I kept double-checking. Is this really enough? Did I forget something? Compiler said no. Code ran. That weightlessness is real — and it's not a fad. It's the result of half a century of computer science slowly answering one question:

Who should do the hard work — the human, or the machine?


A Quick Detour: Why Old Syntax Felt Heavy

Before we dunk on Java and C, let's be fair. They were right for their time.

  • C (1972) ran on PDP-11s with kilobytes of RAM. Compilers had to be dumb-fast translators. Type inference lived only in academic papers.
  • Java (1995) was designed for massive enterprise teams. Explicit types weren't redundancy — they were built-in documentation.
  • C# (2000) inherited that "explicit is safer" ethos, then started evolving faster than Java.

What we call "boilerplate" today was called "self-documenting code" 25 years ago. Then three things changed:

  1. Compilers got thousands of times smarter.
  2. Hardware got absurdly cheap.
  3. Developer time became the most expensive resource in the room.

That shift is the soil the next four pillars grew in.


Pillar 1: Type Inference — Let the Compiler Guess

The problem

// Java 6 — peak boilerplate
Map<String, List<User>> registry = new HashMap<String, List<User>>();
Enter fullscreen mode Exit fullscreen mode

Same information, stated three times. The compiler already knew. You knew. So why the ceremony? Because early compilers weren't smart enough. They are now.

The evolution

// Java 7 (2011) — diamond operator
Map<String, List<User>> registry = new HashMap<>();

// Java 10+ (2018) — var, finally
var registry = new HashMap<String, List<User>>();
Enter fullscreen mode Exit fullscreen mode
// Go — := does declaration + inference in one stroke
registry := make(map[string][]User)
Enter fullscreen mode Exit fullscreen mode
// Kotlin — val vs var distinguishes immutability
val registry = hashMapOf<String, List<User>>()
Enter fullscreen mode Exit fullscreen mode
// Rust — often no type annotation at all
let mut registry = HashMap::new();
registry.insert("alice".to_string(), vec![user1]);
// Compiler infers HashMap<String, Vec<User>> backward from usage
Enter fullscreen mode Exit fullscreen mode

Not all inference is equal

Language Inference type Direction
Rust Hindley-Milner variant Bidirectional (can infer backward)
Kotlin Local + flow-sensitive Mostly forward, some backward
Go Local only Right-to-left only

The smarter the inference, the shorter your code — but the error messages get harder to parse when things break. That's a deliberate trade-off, not a bug.

A subtle but huge detail: val vs var

  • val in Kotlin = assign-once (like final)
  • let in Rust = immutable by default — you must opt-in with let mut

This isn't cosmetic. Decades of debugging taught us: mutable state is where bugs hide, especially across threads. Modern languages default to immutable because the default matters.


Pillar 2: Expression-Oriented — Code as a Sentence

Statement vs expression

  • Statement = does something, returns nothing. if (x > 0) { doSomething(); }
  • Expression = evaluates to a value. 2 + 3 → 5

Classic Java/C/C# are statement-oriented: if, try, for are statements. Want a value out of one? Declare a mutable temp, or chain ternaries until your eyes bleed.

Rust and Kotlin (and Scala, F#) are expression-oriented: almost everything yields a value.

Side-by-side

Java — needs a mutable temp:

String status;
if (health > 50) {
    status = "Healthy";
} else if (health > 20) {
    status = "Warning";
} else {
    status = "Critical";
}
Enter fullscreen mode Exit fullscreen mode

Kotlinwhen is an expression:

val status = when {
    health > 50 -> "Healthy"
    health > 20 -> "Warning"
    else        -> "Critical"
}
Enter fullscreen mode Exit fullscreen mode

Rustmatch is an expression, and the last expression of a block (no ;) is the return value:

let status = match health {
    h if h > 50 => "Healthy",
    h if h > 20 => "Warning",
    _           => "Critical",
};
Enter fullscreen mode Exit fullscreen mode

That same rule applies to function bodies — no return keyword needed:

fn double(x: i32) -> i32 {
    x * 2  // no semicolon = this is the return value
}
Enter fullscreen mode Exit fullscreen mode

Why it matters beyond aesthetics

  1. Less mutable state. If if returns a value, you never declare var status and mutate it. Default to immutable. Thread-safe by accident.
  2. Reads like natural language. "The status is one of these" beats "create a variable, then conditionally modify it."
  3. Compiler catches missing branches. An expression must evaluate to something. Forget a case in Java? The variable just stays uninitialized.

This mindset is borrowed straight from functional programming (Haskell, Lisp, ML) — ideas that lived in academia for decades before mainstream caught up.


Pillar 3: Safety by Default — Move Errors to Compile-Time

This is the philosophical core. And the most economically impactful.

The war on null

In 2009, Tony Hoare — the guy who invented the null reference in 1965 — publicly called it his "billion-dollar mistake." NullPointerException has cost the industry billions, from app crashes to security CVEs.

Classic Java/C#/C++ let every reference be null. So you write defense everywhere:

The classic NPE staircase (click to expand)
if (user != null) {
    Address address = user.getAddress();
    if (address != null) {
        String street = address.getStreet();
        if (street != null) {
            // finally, do the thing
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Welcome to the staircase of doom.

How modern languages fix it

Kotlin — nullability is part of the type:

val name: String = "Dai"        // can never be null — compiler enforced
val nick: String? = null         // explicitly nullable — must be handled

val length = nick?.length ?: 0   // safe call + elvis operator
Enter fullscreen mode Exit fullscreen mode

Rust — no null exists. Period. Use Option<T>:

let nick: Option<String> = None;

match nick {
    Some(n) => println!("Nick: {}", n),
    None    => println!("No nickname"),
}
Enter fullscreen mode Exit fullscreen mode

Null didn't disappear. It got formalized into the type system. No more 3 AM debugging sessions.

Memory: three philosophies, three paths

1. Manual (C, C++)

char* buffer = malloc(1024);
// ... use ...
free(buffer);
Enter fullscreen mode Exit fullscreen mode

Forget free? Leak. Call twice? UB. Use after free? CVE. Microsoft once reported ~70% of patched security vulnerabilities in their products were memory safety bugs. Heartbleed, EternalBlue, every browser CVE — all the same root cause.

2. Garbage Collection (Java, C#, Go)

String[] buffer = new String[1024];
// no free needed — GC handles it
Enter fullscreen mode Exit fullscreen mode

Simple for the dev. But the GC introduces unpredictable pauses — fatal for games, trading, real-time systems. Modern GCs are very good (Java's ZGC hits sub-millisecond pauses on multi-TB heaps), but you're still paying.

3. Ownership (Rust) — the third way

let buffer = String::from("hello");
// ... use ...
// out of scope → destructor runs automatically. No GC. Zero runtime overhead.
Enter fullscreen mode Exit fullscreen mode

Every value has exactly one owner. When the owner goes out of scope, memory is freed. All checked at compile-time.

The famous lifetime annotations:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
Enter fullscreen mode Exit fullscreen mode

'a says: "the returned reference lives at least as long as the shorter input." The compiler uses this contract to prove you can't have a dangling pointer.

Yes, the syntax tax is steep. But you get no GC, no runtime cost, no memory bugs — a combination once considered impossible. Rust is the first mainstream language to prove you don't have to choose between performance and safety.


Pillar 4: Cut the Noise

The most surface-level pillar, but the one you touch the most every day.

Semicolons

Go, Kotlin, Swift, Python — gone or optional. Fun fact: Go does use semicolons internally. The lexer auto-inserts them. You just never see it.

Parens around conditions

// Java
if (x > 0 && y < 10) { /* ... */ }
Enter fullscreen mode Exit fullscreen mode
// Rust, Go
if x > 0 && y < 10 { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

Tiny. But multiply by the millions of if statements in your career.

Encapsulation: from 30 lines to 1

Java, classic:

30+ lines of Java boilerplate (click to expand)
public class User {
    private String name;
    private int age;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }

    @Override public boolean equals(Object o) { /* 10 lines */ }
    @Override public int hashCode() { /* 5 lines */ }
    @Override public String toString() { /* 3 lines */ }
}
Enter fullscreen mode Exit fullscreen mode

30+ lines for two fields. Lombok helped, but it's a band-aid — extra dep, IDE plugin, annotation processor.

Kotlin:

data class User(val name: String, val age: Int)
// auto-generates equals(), hashCode(), toString(), copy(), componentN()
Enter fullscreen mode Exit fullscreen mode

Rust:

#[derive(Debug, Clone, PartialEq)]
struct User {
    name: String,
    age: u32,
}
Enter fullscreen mode Exit fullscreen mode

Go — visibility from casing alone:

type User struct {
    Name string  // public (capitalized)
    age  int     // private (lowercase)
}
Enter fullscreen mode Exit fullscreen mode

To Java's credit, Java 14+ struck back with record:

public record User(String name, int age) {}
Enter fullscreen mode Exit fullscreen mode

A clear case of modern ideas pulling old languages forward.


The Counterpoint: The Old Guard Isn't Standing Still

Articles like this often forget: Java, C#, and C++ are aggressively learning.

Java in the last 5–7 years:

  • var (Java 10) — local type inference
  • Switch expressions (Java 14) — like Rust's match
  • Text blocks (Java 15) — multi-line strings
  • record (Java 16) — inspired by Kotlin data classes
  • Pattern matching for instanceof (16) and switch (21)
  • Sealed classes (17) — restricted hierarchies, like Rust enums
  • Virtual threads (Java 21) — concurrency model inspired by Go goroutines

C# moved even faster:

  • var (2007) — a decade before Java
  • Nullable reference types (C# 8) — straight from Kotlin
  • Records (C# 9) — before Java
  • Extensive pattern matching

C++ has auto, structured bindings, concepts (Rust-trait-like), std::optional, smart pointers (unique_ptr, shared_ptr as "ownership lite").

So why not just use modern Java?

Because legacy is a double-edged sword.

Java has var now — but it must stay compatible with 30 years of code. NPE still exists. GC still runs. You'll work in codebases that mix "old Java" and "new Java" styles in the same file.

Rust and Kotlin were designed on a blank slate around these ideas. Zero historical baggage. That's their power — and their weakness (smaller ecosystem, harder hiring, fewer libraries).

It's not winner-takes-all. It's healthy diversification.


The Trade-Offs (Nothing Is Free)

Every choice has a price. Be a mature dev — see both sides:

  • Powerful inference → cryptic errors. A Rust or Scala compiler chaining inference across dozens of steps can produce a 50-line error message that requires expertise to decode.
  • Expression-oriented → harder to debug for beginners. Nesting everything means fewer natural breakpoints.
  • Rust's ownership has a brutal learning curve. "Fighting the borrow checker" is a phase every Rustacean goes through. Prototype velocity suffers.
  • Go's minimalism → verbose at scale. Pre-generics Go was infamous: "Go is minimalist for small projects, the most verbose language for big ones." if err != nil is an eternal meme.
  • Kotlin's null safety leaks through Java interop — "platform types" the compiler can't verify.

Right tool for the job

Language Sweet spot Strength Weakness
Go Network services, CLI, microservices Fast to learn, very readable Verbose at scale
Rust Systems, embedded, low-latency, blockchain Correctness + performance Slow initial velocity
Kotlin Android, JVM backend Pragmatic, full Java interop Compromises for compat
Java/C# Enterprise, large teams, legacy Tooling, ecosystem, stability Legacy baggage
C/C++ Kernels, drivers, engines Total hardware control Memory safety risk

There is no best language. Only the most appropriate one for your problem.


Conclusion: Syntax Is Philosophy

Every syntactic choice is an answer to a deeper question about what programming should be.

  • C says: "The computer is a machine. Understand it like a mechanic."
  • Java says: "The computer serves the human. Let the runtime work."
  • Rust says: "There's a third path — push hard decisions to compile-time, keep runtime silent."
  • Kotlin says: "A billion-dollar mistake shouldn't be the default. Bake it into the type system."
  • Go says: "Simplicity is hard. It requires more discipline than we think."

The quiet convergence of Go, Rust, and Kotlin around the same four pillars — inference, expressions, safety, minimalism — isn't coincidence. It's our industry maturing after 50 years of empirical pain. Every sleepless NPE chase, every buffer overflow CVE, every hour writing getters — they all fed into this synthesis.

I'm not telling you to abandon Java or C#. They're phenomenal tools that ship real value. But if you haven't seriously tried Rust, Go, or Kotlin — meaning built a real project for a month, not a "Hello World" — give it a shot. Not for the resume. For the way it expands how you think about code.

"The limits of my language mean the limits of my world." — Wittgenstein

Picking a great language is giving yourself a wider world to think in.


📚 Dive Deeper

The official repos — worth a star if you haven't already:

GitHub logo rust-lang / rust

Empowering everyone to build reliable and efficient software.

This is the main source code repository for Rust. It contains the compiler standard library, and documentation.

Why Rust?

  • Performance: Fast and memory-efficient, suitable for critical services, embedded devices, and easily integrated with other languages.

  • Reliability: Our rich type system and ownership model ensure memory and thread safety, reducing bugs at compile-time.

  • Productivity: Comprehensive documentation, a compiler committed to providing great diagnostics, and advanced tooling including package manager and build tool (Cargo), auto-formatter (rustfmt), linter (Clippy) and editor support (rust-analyzer).

Quick Start

Read "Installation" from The Book.

Installing from Source

If you really want to install from source (though this is not recommended), see INSTALL.md.

Getting Help

See https://www.rust-lang.org/community for a list of chat platforms and forums.

Contributing

See CONTRIBUTING.md.

For a detailed explanation of the compiler's architecture…

GitHub logo JetBrains / kotlin

The Kotlin Programming Language.

official project TeamCity (simple build status) Maven Central GitHub license Revved up by Develocity

Kotlin Programming Language

Welcome to Kotlin!
Kotlin is a concise multiplatform language developed by JetBrains and contributors.

Some handy links:

Kotlin Multiplatform capabilities

Support for multiplatform programming is one of Kotlin’s key benefits. It reduces time spent writing and maintaining the same code for different platforms while retaining the flexibility and benefits of native programming.

Editing Kotlin

Build environment requirements

This repository is…

GitHub logo golang / go

The Go programming language

The Go Programming Language

Go is an open source programming language that makes it easy to build simple reliable, and efficient software.

Gopher image Gopher image by Renee French, licensed under Creative Commons 4.0 Attribution license.

Our canonical Git repository is located at https://go.googlesource.com/go There is a mirror of the repository at https://github.com/golang/go.

Unless otherwise noted, the Go source files are distributed under the BSD-style license found in the LICENSE file.

Download and Install

Binary Distributions

Official binary distributions are available at https://go.dev/dl/.

After downloading a binary release, visit https://go.dev/doc/install for installation instructions.

Install From Source

If a binary distribution is not available for your combination of operating system and architecture, visit https://go.dev/doc/install/source for source installation instructions.

Contributing

Go is the work of thousands of contributors. We appreciate your help!

To contribute, please read the contribution guidelines at https://go.dev/doc/contribute.

Note that the Go project uses the…


💬 Your turn

I'd love to hear in the comments:

  • What language do you reach for daily, and why?
  • Was there an "aha!" moment moving from an old language to a new one?
  • Or — what feature from an "older" language do you miss in modern environments?

If this resonated, drop a ❤️ or 🦀. And follow for more deep-dives on language design and systems thinking.

Read more essays at Lữ Khách Hoài Niệm

Top comments (0)