Contents

Java, Go, Rust - comparison

I’m mainly a Java programmer. After +20 years Java continues to be my language of choice and a language I’m using professionally. Sometimes, however, I indulge my curiosity and try new and shiny languages just enough to grasp the “look and feel” of it.

Java vs Go vs Rust. Java is still strong?… ./java_go_rust.png

I played a lot more in Go than in Rust (or at least I felt more productive in Go). Tooling and documentation are great. The language is stable and minimal. I had a few “mental issues” with it:

  • looking at a struct, you never know what interface it is implementing (it is not obvious)
  • goroutines - I still have a feeling of awkwardness when I use it (probably due to lack of experience)
  • error handling - just very different and convoluted, as well as the whole “deferred” functions concept

While playing with Rust, I really appreciated the compiler - error messages are great. The std library documentation is very detailed and nice on my eyes. Cargo seemed a bit like Maven to me (at least that’s how I was using it - simply as package manager). However:

  • I got frustrated a bit after not being able to implement recursive data structures util I learned about Cell<T> or RefCell<T> (for interior mutbaility) and Rc (for reference counting)
  • working with basic int types I somehow ended up with a lot of (unexpected) casting (due to not being careful enough with my own functions’ signatures)

Little sisters: Go stronger (read: easier) than Rust.. ./go_rust.png

A Comparative Analysis with Go and Rust

In this blog post I’d like to look closer at specific aspects of those three languages. It is a rather personal comparison and I encourage you to do your own research, especially if you are about to make a technical decision that would influence your project (and a team).

Let’s start with some basic aspects of those languages that you will encounter very early whenever you try to approach and learn any of them:

Aspect Java Go Rust
Concurrency thread-based concurrency with java.util.concurrent
- effective
- error-prone, leading to performance bottlenecks
- lightweight goroutines
- simplify concurrent programming
- more accessible
- efficient
- channels: communication between goroutines
- very readable code.
- ownership system and lifetimes: no data races
- safe concurrent programming
- you have control and still have performance
Memory Management - garbage collector
- introduces pauses
- needs fine-tuning
- hard to write real-time applications
- hard to do it well in performance-critical scenarios
- garbage collector
- designed for low-latency
- can be used in real-time apps
- gc is simple
- so code is efficient at runtime
- ownership system
- no garbage
- manual control over memory
- high-performance code is easy
- predictability
- responsiveness
Error Handling - exceptions for error handling
- complex and verbose code
- checked exceptions: lots of boilerplate
- explicit error returns
- more readable coding style
- reduced risk of unnoticed errors
- forces error handling
- “Result” type
- error handling is explicit
- pattern matching on Results
Compilation Speed - slow compilation (especially for large projects)
- slows down development cycles
- decreases developer productivity
- great compilation speed
- statically linked binaries
- good for agile development
- faster iteration possible
- fast incremental compilation
- fast development cycles
- memory-safety makes it reliable (when it compiles, it runs)
Tooling and Ecosystem - very mature ecosystem
- lots of libraries and frameworks
- build tools (Maven, Gradle): heavyweight and complex
- simplicity also applies to tooling
- single “go” command
- package manager Cargo
- high quality libraries
- community of experienced devs
Usecases - enterprise software
- back-end applications
- Android app development (well… perhaps not anymore: Kotlin and Dart)
- large-scale systems
- web apps
- network services
- microservices
- concurrent applications
- systems programming
- game development
- scenarios where low-level memory control and safety are crucial

Java is a very versatile language, it is everywhere and it is relatively easy to find a Java-speaking human. However, it is a good idea to explore alternatives like Go and Rust.

This comparative analysis highlights areas where Java may lag behind, but in the end you need to always consider the overall requirements of a project before choosing a language. Each language has its strengths and weaknesses, and the decision ultimately depends on the specific needs and goals (and the knowledge) of the development team.

Type system comparison

Can we somehow compare type systems of Java, Rust and Go? What aspects of the language make it adhere (or not) to a strong typing system? Do all three languages have strong type system? Let’s have a look how a type systems in those three languages looks like (disclaimer: I’m not a type system expert and don’t understand Curry-Howard correspondence, so this is very basic):

Java:

Type System: Java employs a statically-typed system, meaning variable types must be declared explicitly at compile-time. It is considered to have a strong type system, as the compiler enforces type constraints, reducing the likelihood of runtime errors.

Aspects of Strong Typing:

Static Typing:

Java’s static typing requires variable types to be declared before compilation. For example:

1
int age = 25; String name = "John";

Type Safety:

Java’s strong type system prevents many common programming errors. For instance, attempting to assign a string to an integer variable will result in a compilation error.

1
int number = "Hello"; // Compilation error

Generics:

Java supports generics, enabling the creation of classes and methods that operate on types specified at compile-time, providing additional type safety.

1
List<String> names = new ArrayList<>();

Rust:

Type System: Rust also utilizes a statically-typed system with a strong emphasis on memory safety. It is often described as having an “ownership” system that ensures memory safety without garbage collection.

Aspects of Strong Typing:

Ownership and Borrowing:

Rust’s ownership system is a unique feature. It enforces strict rules about variable ownership, borrowing, and lifetimes, ensuring memory safety without runtime overhead.

1
2
3
4
fn main() {
  let s1 = String::from("hello");
  let s2 = s1; // Ownership transfer, s1 is no longer valid     
  // println!("{}", s1); // Error, s1 is no longer valid }

Pattern Matching and Enums:

Rust’s enums and pattern matching contribute to strong typing, allowing developers to handle different types explicitly and comprehensively.

1
2
3
4
enum Result<T, E> {
	Ok(T), 
	Err(E), 
}

Go:

Type System: Go uses a statically-typed system as well, but it is often characterized as having a more lightweight and flexible type system compared to Java and Rust.

Aspects of Strong Typing:

Static Typing with Implicit Declaration:

Go’s static typing requires explicit type declaration, but it also supports implicit declaration using the := operator. While it still enforces type safety, it adds a degree of flexibility.

1
2
var age int = 25 
name := "John"`

Interface Types:

Go’s interfaces allow types to be defined by the methods they implement rather than explicitly, providing a flexible way to achieve polymorphism.

1
2
3
type Shape interface {  
    Area() float64 
}

Summary

All three languages—Java, Rust, and Go—have strong type systems, however they exhibit different characteristics:

  • Java: static typing and a focus on type safety
  • Rust: unique ownership system, strong type system with an emphasis on memory safety
  • Go: statically-typed but with lightweight feel; flexibility in type declarations The nuances in their type systems make each language suitable for different use cases and developer preferences (see table above)

Doubts: How typesafe Java really is?

While Java is often considered a type-safe language due to its use of static typing and the enforcement of type constraints by the compiler, there are scenarios where Java’s type system allows for unsafe operations, potentially leading to runtime errors. Let’s explore a few examples that highlight situations where Java’s type safety can be compromised:

Example 1: Type Casting

Java allows explicit type casting, and if not done carefully, it can result in runtime errors:

1
2
3
4
5
6
7
8
public class TypeSafetyExample {
	public static void main(String[] args) {
		Object myObject = "Hello, Java!";
	    // Type casting without proper checking 
	    Integer myInteger = (Integer) myObject; 
	    // Compiles, but throws ClassCastException at runtime     
	} 
}

In this example, an attempt to cast an Object to an Integer is made without proper type checking. At compile-time, this code is valid, but it will throw a ClassCastException at runtime, demonstrating a potential type safety issue.

Example 2: Raw Types in Generics

Java supports raw types, which allow mixing generic and non-generic code. This can lead to unsafe operations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import java.util.ArrayList; 
import java.util.List;  
public class RawTypeExample {
	public static void main(String[] args) {
        List rawList = new ArrayList();
        rawList.add("Java");
        rawList.add(42);
        // No compile-time type checking for elements         
        for (Object element : rawList) {
            String str = (String) element; 
            // Compiles, but may throw ClassCastException at runtime
            System.out.println(str);
        }
	}
}

In this example, a raw List is used, allowing elements of different types to be added. During iteration, an attempt is made to cast each element to a String. This code compiles without errors, but it may throw a ClassCastException at runtime.

Example 3: Array Covariance

Java arrays are covariant, which means that an array of a supertype can hold elements of its subtype. This can lead to unexpected behavior:

1
2
3
4
5
6
7
public class ArrayCovarianceExample {
	public static void main(String[] args) {
		Object[] objects = new String[]{"Java", "is", "type-safe"};
		// Compiles, but throws ArrayStoreException at runtime
		objects[2] = 42;
	}
}

In this example, an array of Object is declared, but it’s initialized with an array of String. While this compiles without errors, attempting to store an Integer in the array at runtime will result in an ArrayStoreException.

Conclusion

Java’s type safety can be compromised. Practitioners often emphasize the importance of careful programming practices. There are patterns, good practices, golden rules and whole culture that was created and is promoted to ensure type safety in real-world applications. In conclusion, Java provides mechanisms for type safety, however these mechanisms are leaky and developers (instead of compiler) need to be vigilant to avoid potential pitfalls.

Doubts: How can Golang type safety be compromised?

Go (Golang) is designed to be a statically-typed language with a focus on simplicity and safety. However, like any programming language, there are certain scenarios where type safety in Go can be compromised.

Example 1: Type Assertion

Go allows type assertions to convert an interface value to another type. If used carelessly, it can lead to runtime errors. The below code compiles:

1
2
3
4
5
6
7
8
package main  
import "fmt"
func main() {
    var value interface{} = 42 
    // Incorrect type assertion, may panic at runtime
    result := value.(string)
    fmt.Println(result) 
}

In this example, the value interface is assigned an integer (42), but a type assertion is attempted to convert it into a string. If the actual type is not a string, a runtime panic will occur. This demonstrates a potential type safety issue when using type assertions without proper checks.

Example 2: Unchecked Type Conversions

Go allows explicit type conversions between compatible types. However, unchecked type conversions can lead to unexpected behavior:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

package main 
import "fmt"  
func main() { 	
	var x int = 42 	
	var y float64 = float64(x)  	
	// Incorrect type conversion, may result in loss of precision 	
	var z int = int(y) 	
	fmt.Println(z) 
}

In this example, a float64 is explicitly converted to an int without proper checking for potential precision loss. While Go allows such conversions, developers need to be cautious to ensure that the conversion does not compromise the integrity of the data.

Example 3: Using the unsafe Package

Go provides the unsafe package for low-level programming. So, the type safety goes away and you don’t have your guarantees anymore. It is intended for special usecases.

In the small example below the unsafe package is used to cast an int32 to a float64. Such operations bypass Go’s type system and can lead to undefined behavior if not used with a deep understanding of memory layout and alignment.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main  
import ( 	
	"fmt" 	
	"unsafe" 
)  
func main() {
	var x int32 = 42  	
	// Unsafe type casting, may lead to unexpected behavior 	
	y := *(*float64)(unsafe.Pointer(&x)) 	
	fmt.Println(y) 
}

Go programming language is “designed with simplicity and safety in mind”. The language specification says that using unsafe might result in non-portable code
Developers should however be aware of type casting (the above mentioned type assertions and conversions and exercise caution in their code. Or reconsider if they really have to use these.

Doubts: How can Rust type safety be compromised?

The only place where Rust allows developers to go around its type safety (that I’m aware of) is the use of unsafe blocks.

Example 1: Unchecked unsafe Code

Rust provides the unsafe keyword for operations that are not subject to the usual safety checks. It is necessary in some scenarios. Here you can see how its misuse can compromise type safety:

1
2
3
4
5
6
fn main() {
	let raw_pointer: *const i32 = std::ptr::null();
	// Dereferencing raw pointer without proper validation
	let value: i32 = unsafe { *raw_pointer }; 
	println!("Value: {}", value);
}

Here a null raw pointer is dereferenced using unsafe. This can lead to undefined behavior.

Example 2: Incorrect Type Casting

Rust allows explicit type casting using the as keyword, but carelessness can lead to type safety concerns:

1
2
3
4
5
6
fn main() {
	let value: f64 = 42.0;
	// Incorrect type casting, may result in data corruption 
	let integer_value: i32 = value as i32; 
	println!("Integer Value: {}", integer_value); 
}

Here, a f64 value is explicitly cast to an i32. If the floating-point value cannot be accurately represented as an integer, this may lead to data corruption and a loss of precision.

Example 3: Misusing transmute in unsafe Code

The transmute function in Rust (see https://doc.rust-lang.org/std/mem/fn.transmute.html) can be powerful but dangerous. As the documentation says, it can be incredibly unsafe. I haven’t hear about this function before. What it does? It “Reinterprets the bits of a value of one type as another type.” Oh. This certainly needs to be used with extreme care.

Let’s have a look:

1
2
3
4
5
6
fn main() {     
    let value: i32 = 42;
    // Incorrect use of transmute, may lead to type safety issues     
    let transmuted_value: f64 = unsafe { std::mem::transmute(value) };
    println!("Transmuted Value: {}", transmuted_value);
}

Here, the transmute function is used to cast an i32 to a f64. This can result in undefined behavior and if you do such stuff, you are circumventing type safety completely. Actual recommendations are clear: don’t use it unless you really know what you are doing.

How developers compare Golang and Rust?

I would call it “Developer Experience”. The term has been taken, but I don’t care, I just like the expression :)

It is hard to find a balanced opinion - Golang and Rust seem to be separate worlds with lots of young, motivated developers (or: believers) that just hate the other camp.

However, when senior devs (or people that had programmed in both) are asked an explicit question, the Go language seem to win simply due to its simplicity:

  • easier to onboard new developers in the team
  • easier to learn
  • easy threading model in the language, no libs needed
  • ecosystem of Go seem to be more mature than of Rust
  • more familiar Go syntax
  • concepts in Go are easier to reason about
  • core devs of Go have strong opinions (this is limiting for discussions) and change things very infrequently (compatibility)
  • harder to introduce bugs due to too much abstraction
  • code analysis, cross-compilation, profiling tools

My own opinion

Rust is for those brilliant guys that write low-level code and do a real system-level programming. It is fascinating, with beautifully designed API, rich std lib and good quality crates.

Go is for average programmers that want the stuff done. For minimalists. Allows straight, easy reasoning about the code. Language is fast, tools are awesome, ecosystem is great.

Java is for everyone who just need a job. Teaches abstractions, patterns and structure. It is modern Cobol and you will certainly find a (well-paid) job in Java in 20 years from now (unless AI will win). It is hard to find high quality resources - web is filled with newbie tutorials.

And it is worth to try out all three and have your own opinion.

Happy coding!

References

Core Rust: (no spec yet… there is a spec vision)

Core Go:

Random: