Hello World & Running
Hello, World
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
} class Main {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
} Go needs only a
package clause and a top-level func main. Java requires every piece of code to live inside a class, so even the simplest program needs a class Main wrapping a public static void main(String[] args) entry point — there is no such thing as a free-standing function in Java.Building & running
// Compile and run in one step:
// go run main.go
// Build a binary:
// go build -o app main.go && ./app
// Dependencies live in go.mod. // Compile then run (two separate steps):
// javac Main.java
// java Main
// A single-file shortcut (compiles in memory, Java 11+):
// java Main.java
// Dependencies are managed by Maven or Gradle, not the JDK itself. Go produces one self-contained native binary with
go build — nothing else needs to be installed on the machine that runs it. Java compiles to platform-independent bytecode (.class files) with javac, and the JVM interprets and JIT-compiles that bytecode with java. The tradeoff is portability (the same .class file runs anywhere a JVM exists) for the requirement that a JVM be installed at all.Comments
package main
import "fmt"
func main() {
// Single-line comment
/* Block comment */
count := 0
fmt.Println(count)
} class Main {
public static void main(String[] args) {
// Single-line comment
/* Block comment */
/** Javadoc comment (documents the item below) */
int count = 0;
System.out.println(count);
}
} Java shares Go's
// and /* */ comment forms and adds a documentation form: /** ... */ Javadoc comments attach to the following declaration and are rendered by the javadoc tool into HTML API documentation, similar in spirit to a doc comment above a Go declaration but with dedicated tooling and tags like @param and @return.Imports & packages
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.ToUpper("java"))
} import java.util.ArrayList;
import java.util.List;
class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("java");
System.out.println(names);
}
} Both languages organize code into packages and import what they need. Go's package is simply the directory a file lives in, and every file in that directory shares one namespace. Java's package is a namespace declared explicitly at the top of the file (
package com.example;) that is conventionally, but not necessarily, mirrored by the directory structure, and classpath resolution — not the filesystem — is what actually locates an imported class at compile and run time.Variables & Types
Variables & type inference
package main
import "fmt"
func main() {
count := 5 // type inferred from the value
var ready bool = true
fmt.Println(count, ready)
} class Main {
public static void main(String[] args) {
var count = 5; // type inferred, but fixed forever at declaration
boolean ready = true;
System.out.println(count + " " + ready);
}
} Java's
var (Java 10+) mirrors Go's :=: both infer the type once, at the point of declaration, and the variable is statically typed from then on — this is not dynamic typing in either language. A key difference is scope: Java's var works only for local variables inside a method body, never for fields, parameters, or return types, whereas Go's := has the same restriction to local scope by construction (package-level declarations always use var).Primitive types
package main
import "fmt"
func main() {
var small int8 = 120
var big int64 = 9000000000
var ratio float64 = 3.14
var flag bool = true
var letter rune = 'A'
fmt.Println(small, big, ratio, flag, letter)
} class Main {
public static void main(String[] args) {
byte small = 120;
long big = 9_000_000_000L;
double ratio = 3.14;
boolean flag = true;
char letter = 'A';
System.out.println(small + " " + big + " " + ratio + " " + flag + " " + letter);
}
} Both languages have a family of fixed-width numeric types (
int8/byte, int64/long, float64/double) and a boolean type spelled differently (Go's bool versus Java's boolean). Java's char is a 16-bit UTF-16 code unit, whereas Go's rune is a 32-bit Unicode code point — a Java char cannot hold every possible character on its own, which is why some Unicode characters require a Java String or a surrogate pair to represent correctly.Constants
package main
import "fmt"
const MaxRetries = 3
const Greeting = "hello"
func main() {
fmt.Println(MaxRetries, Greeting)
} class Main {
static final int MAX_RETRIES = 3;
static final String GREETING = "hello";
public static void main(String[] args) {
System.out.println(MAX_RETRIES + " " + GREETING);
}
} Java has no dedicated
const keyword; the idiomatic equivalent is a static final field, static so it belongs to the class rather than each instance, and final so it cannot be reassigned after initialization. Go constants can be package-level or function-local and are evaluated at compile time; a Java static final primitive or String initialized with a constant expression is inlined by the compiler in much the same way.Fixed-size arrays
package main
import "fmt"
func main() {
var fixed [3]int = [3]int{1, 2, 3}
fmt.Println(fixed, len(fixed))
} class Main {
public static void main(String[] args) {
int[] fixed = {1, 2, 3};
System.out.println(fixed.length);
for (int value : fixed) {
System.out.print(value + " ");
}
System.out.println();
}
} Both languages have fixed-length arrays: Go's
[3]int bakes the length into the type, while a Java array's length is fixed once allocated but is not part of its static type — int[] can point to an array of any length. Java exposes the length through a length field (no parentheses, unlike a method call), where Go uses the built-in len() function that also works on slices, strings, and maps.Strings
Strings are immutable
package main
import "fmt"
func main() {
greeting := "hello"
shout := greeting + ", WORLD"
fmt.Println(greeting, shout)
} class Main {
public static void main(String[] args) {
String greeting = "hello";
String shout = greeting + ", WORLD";
System.out.println(greeting + " " + shout);
}
} Strings are immutable in both languages: concatenation always produces a new string, leaving the originals untouched. Under the hood a Go string is a read-only view over a byte slice, while a Java
String is a full object backed by an internal byte[] — which is why Java strings support methods (greeting.length()) directly, whereas Go keeps string operations as free functions in the strings package.String methods
package main
import (
"fmt"
"strings"
)
func main() {
sentence := "the quick brown fox"
fmt.Println(strings.ToUpper(sentence))
fmt.Println(strings.Split(sentence, " "))
fmt.Println(strings.Contains(sentence, "quick"))
} import java.util.Arrays;
class Main {
public static void main(String[] args) {
String sentence = "the quick brown fox";
System.out.println(sentence.toUpperCase());
System.out.println(Arrays.toString(sentence.split(" ")));
System.out.println(sentence.contains("quick"));
}
} Where Go collects string operations as free functions in the
strings package (strings.ToUpper(text)), Java attaches them as instance methods directly on String (text.toUpperCase()). split returns a Java array rather than a slice, and printing an array directly shows its memory address, so Arrays.toString() is needed for a readable result — a common early surprise for anyone used to Go's automatic slice formatting.Formatting
package main
import "fmt"
func main() {
name := "Java"
year := 2026
fmt.Printf("%s since %d\n", name, year)
line := fmt.Sprintf("%s-%d", name, year)
fmt.Println(line)
} class Main {
public static void main(String[] args) {
String name = "Java";
int year = 2026;
System.out.printf("%s since %d%n", name, year);
String line = "%s-%d".formatted(name, year);
System.out.println(line);
}
} Both languages use C-style format specifiers (
%s, %d), so the verbs translate almost directly from Go's Printf/Sprintf to Java's printf/String.formatted(). One easy trap: Go's literal \n works fine in Printf, but idiomatic Java formatting uses %n instead, which expands to the host platform's line separator rather than always emitting a bare LF.Building strings efficiently
package main
import (
"fmt"
"strings"
)
func main() {
var builder strings.Builder
builder.WriteString("Hello")
builder.WriteString(", ")
builder.WriteString("World!")
fmt.Println(builder.String())
} class Main {
public static void main(String[] args) {
StringBuilder builder = new StringBuilder();
builder.append("Hello");
builder.append(", ");
builder.append("World!");
System.out.println(builder.toString());
}
} Both languages provide a mutable string-building type for the same reason: repeated
+ concatenation in a loop allocates a new string on every iteration, which is wasteful for both a Go string and an immutable Java String. Go's strings.Builder and Java's StringBuilder both accumulate into an internal, resizable buffer and only materialize the final string when asked for it — builder.String() versus builder.toString().Numbers
Arithmetic & integer division
package main
import "fmt"
func main() {
fmt.Println(10 + 3)
fmt.Println(10 / 3) // integer division truncates
fmt.Println(10 % 3)
fmt.Println(10.0 / 3.0)
} class Main {
public static void main(String[] args) {
System.out.println(10 + 3);
System.out.println(10 / 3); // integer division truncates
System.out.println(10 % 3);
System.out.println(10.0 / 3.0);
}
} Arithmetic rules are identical: dividing two integers truncates toward zero in both languages, and mixing an integer with a floating-point literal (
10.0 / 3.0) produces a floating-point result. Neither language does Python-style automatic promotion of int / int to a float — the programmer must convert explicitly to get a fractional answer.Integer overflow
package main
import "fmt"
func main() {
var value int8 = 127
value++ // silently wraps to -128
fmt.Println(value)
} class Main {
public static void main(String[] args) {
byte value = 127;
value++; // silently wraps to -128
System.out.println(value);
}
} Both languages wrap silently on integer overflow rather than throwing or panicking — a Go
int8 and a Java byte behave identically here. Java offers an opt-in alternative Go lacks: Math.addExact(a, b) and its siblings throw ArithmeticException on overflow, useful when silent wraparound would be a bug rather than intended modular arithmetic.Parsing numbers from strings
package main
import (
"fmt"
"strconv"
)
func main() {
number, err := strconv.Atoi("42")
if err != nil {
fmt.Println("not a number")
return
}
fmt.Println(number + 8)
} class Main {
public static void main(String[] args) {
try {
int number = Integer.parseInt("42");
System.out.println(number + 8);
} catch (NumberFormatException error) {
System.out.println("not a number");
}
}
} This is the first clear preview of the error-handling divide covered later in this cheatsheet. Go's
strconv.Atoi returns a plain (int, error) pair that the caller inspects with an if; Java's Integer.parseInt instead throws NumberFormatException on invalid input, which must be caught with try/catch — the same underlying failure, but two fundamentally different control-flow mechanisms for reporting it.Math functions
package main
import (
"fmt"
"math"
)
func main() {
fmt.Println(math.Sqrt(16))
fmt.Println(math.Pow(2, 10))
fmt.Println(math.Abs(-42))
fmt.Println(math.Max(10, 20))
} class Main {
public static void main(String[] args) {
System.out.println(Math.sqrt(16));
System.out.println(Math.pow(2, 10));
System.out.println(Math.abs(-42));
System.out.println(Math.max(10, 20));
}
} The math libraries line up almost one-to-one: Go's
math package exposes free functions (math.Sqrt), while Java groups the same operations as static methods on the Math class (Math.sqrt) — a structural difference (package versus class) around functions with identical names and behavior.Collections
Slices vs ArrayList
package main
import "fmt"
func main() {
numbers := []int{1, 2, 3}
numbers = append(numbers, 4)
fmt.Println(numbers, len(numbers))
} import java.util.ArrayList;
import java.util.List;
class Main {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3));
numbers.add(4);
System.out.println(numbers + " " + numbers.size());
}
} A Go slice is a lightweight, built-in growable view over an array, resized by reassigning the result of
append. Java has no growable sequence built into the language itself — ArrayList<T> is an ordinary generic class from the standard library, and it mutates in place with add() rather than returning a new reference. Crucially, Java generics require a boxed reference type, so a list of numbers must be declared List<Integer>, never List<int>.Iterating with an index
package main
import "fmt"
func main() {
fruits := []string{"apple", "banana"}
for index, fruit := range fruits {
fmt.Printf("%d: %s\n", index, fruit)
}
} import java.util.List;
class Main {
public static void main(String[] args) {
List<String> fruits = List.of("apple", "banana");
for (int index = 0; index < fruits.size(); index++) {
System.out.printf("%d: %s%n", index, fruits.get(index));
}
}
} Go's
range yields the index and value together in one loop header. Java's enhanced for loop (for (String fruit : fruits)) yields only the value with no index, so getting both back requires either falling back to an old-style indexed loop as shown here, or streaming with IntStream.range(0, fruits.size()) — there is no single built-in construct that mirrors Go's two-value range exactly.Maps vs HashMap
package main
import "fmt"
func main() {
ages := map[string]int{"Alice": 30}
ages["Bob"] = 25
if age, ok := ages["Alice"]; ok {
fmt.Println(age)
}
} import java.util.HashMap;
import java.util.Map;
class Main {
public static void main(String[] args) {
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
if (ages.containsKey("Alice")) {
System.out.println(ages.get("Alice"));
}
}
} Go's
map is a built-in type with literal syntax and the comma-ok idiom (value, ok := m[key]) for checking presence in a single expression. Java's HashMap is an ordinary library class: put/get replace index syntax, and checking for a key takes two separate calls, containsKey then get — or one call to getOrDefault when a fallback value is acceptable. get on a missing key returns null rather than raising, which is its own trap covered later.Iterating over a map
package main
import "fmt"
func main() {
capitals := map[string]string{"France": "Paris", "Germany": "Berlin"}
for country, capital := range capitals {
fmt.Printf("%s: %s\n", country, capital)
}
} import java.util.Map;
class Main {
public static void main(String[] args) {
Map<String, String> capitals = Map.of("France", "Paris", "Germany", "Berlin");
for (Map.Entry<String, String> entry : capitals.entrySet()) {
System.out.printf("%s: %s%n", entry.getKey(), entry.getValue());
}
}
} Go's
range over a map yields the key and value directly. Java requires iterating entrySet() and pulling the key and value back out of each Map.Entry — more ceremony for the same result. Neither language guarantees iteration order for a plain hash map: Go randomizes it deliberately (to stop code from accidentally depending on it), while Java's HashMap order simply falls out of the hash table's internal layout and should not be relied upon either.Sets
package main
import "fmt"
func main() {
// Go has no built-in set type; a map[T]struct{} is the idiom.
unique := map[string]struct{}{}
unique["apple"] = struct{}{}
unique["banana"] = struct{}{}
unique["apple"] = struct{}{} // duplicate, no effect
fmt.Println(len(unique))
} import java.util.HashSet;
import java.util.Set;
class Main {
public static void main(String[] args) {
Set<String> unique = new HashSet<>();
unique.add("apple");
unique.add("banana");
unique.add("apple"); // duplicate ignored
System.out.println(unique.size());
}
} Java has a dedicated
Set<T> interface with a HashSet implementation, matching the mathematical notion of a set directly. Go has no set type at all — the standard idiom is a map[T]struct{}, using the empty struct as a zero-byte value because only the keys matter. Anyone coming from Java tends to find this idiom surprising the first time they see it.Control Flow
If / else
package main
import "fmt"
func main() {
temperature := 22
if temperature > 30 {
fmt.Println("Hot")
} else if temperature > 20 {
fmt.Println("Warm")
} else {
fmt.Println("Cool")
}
} class Main {
public static void main(String[] args) {
int temperature = 22;
if (temperature > 30) {
System.out.println("Hot");
} else if (temperature > 20) {
System.out.println("Warm");
} else {
System.out.println("Cool");
}
}
} The branching structure is identical, but the syntax differs in a detail Go programmers must consciously relearn: Go forbids parentheses around the condition and requires braces, while Java requires parentheses around the condition,
if (temperature > 30), and makes the braces optional for a single statement (though nearly every style guide keeps them for clarity).Switch expressions
package main
import "fmt"
func main() {
day := "Monday"
var dayType string
switch day {
case "Saturday", "Sunday":
dayType = "Weekend"
default:
dayType = "Weekday"
}
fmt.Println(dayType)
} class Main {
public static void main(String[] args) {
String day = "Monday";
String dayType = switch (day) {
case "Saturday", "Sunday" -> "Weekend";
default -> "Weekday";
};
System.out.println(dayType);
}
} Modern Java (14+) added a
switch expression using -> arrows that directly produces a value, closing much of the gap with Go's switch statement — both allow comma-separated case values and both stop after a matching case with no fall-through by default. The traditional Java switch statement using colons still exists and does fall through without an explicit break, a legacy trap that the arrow form was specifically designed to eliminate.Loops
package main
import "fmt"
func main() {
for index := 0; index < 3; index++ {
fmt.Println(index)
}
total := 0
for total < 3 {
total++
}
fmt.Println("total:", total)
} class Main {
public static void main(String[] args) {
for (int index = 0; index < 3; index++) {
System.out.println(index);
}
int total = 0;
while (total < 3) {
total++;
}
System.out.println("total: " + total);
}
} Go famously folds every kind of loop — counted, conditional, and infinite — into a single
for keyword, distinguished only by which clauses are present. Java keeps the traditional trio of loop keywords from C: for for counted loops, while for condition-first loops, and do/while for condition-last loops, plus the enhanced for for iterating a collection.Ternary operator
package main
import "fmt"
func main() {
// Go deliberately has no ternary operator; if/else fills that role.
score := 75
var grade string
if score >= 60 {
grade = "Pass"
} else {
grade = "Fail"
}
fmt.Println(grade)
} class Main {
public static void main(String[] args) {
int score = 75;
String grade = score >= 60 ? "Pass" : "Fail";
System.out.println(grade);
}
} Java has a ternary operator,
condition ? whenTrue : whenFalse, inherited from C. Go's designers deliberately left it out, judging that a full if/else statement is clear enough and that a ternary tends to be overused for compound conditions — so the same one-line decision that is idiomatic Java requires four lines of Go.Methods
Functions vs methods
package main
import "fmt"
func greet(name string) string {
return "Hello, " + name
}
func main() {
fmt.Println(greet("Java"))
} class Main {
static String greet(String name) {
return "Hello, " + name;
}
public static void main(String[] args) {
System.out.println(greet("Java"));
}
} Go has genuine free-standing functions declared at package level. Java has no equivalent — every method must live inside a class, so a helper called from
main must be declared static on that class to be callable without an instance. This is a direct consequence of Java being class-rooted: there is simply no syntax for code that exists outside of some class.No multiple return values
package main
import "fmt"
func divide(numerator, denominator int) (int, int) {
return numerator / denominator, numerator % denominator
}
func main() {
quotient, remainder := divide(17, 5)
fmt.Println(quotient, remainder)
} class Main {
record DivisionResult(int quotient, int remainder) {}
static DivisionResult divide(int numerator, int denominator) {
return new DivisionResult(numerator / denominator, numerator % denominator);
}
public static void main(String[] args) {
DivisionResult result = divide(17, 5);
System.out.println(result.quotient() + " " + result.remainder());
}
} Go methods can return multiple values directly,
(int, int), unpacked with multiple assignment at the call site — the mechanism the language leans on heavily for its (value, error) convention. Java methods can only return one value, so returning several related values means bundling them into an object, and a Java record (16+) is the concise, idiomatic way to do that without hand-writing a full class with a constructor, getters, and equals/hashCode.Variadic parameters
package main
import "fmt"
func sum(numbers ...int) int {
total := 0
for _, number := range numbers {
total += number
}
return total
}
func main() {
fmt.Println(sum(1, 2, 3, 4))
} class Main {
static int sum(int... numbers) {
int total = 0;
for (int number : numbers) {
total += number;
}
return total;
}
public static void main(String[] args) {
System.out.println(sum(1, 2, 3, 4));
}
} Variadic parameters are nearly identical in spelling and behavior: Go's
numbers ...int becomes Java's int... numbers, and inside the method both are treated as an ordinary sequence (a slice in Go, an array in Java). Both require the variadic parameter to be last in the parameter list, and both let the caller pass zero or more arguments directly without wrapping them in a literal.Function literals vs lambdas
package main
import "fmt"
func main() {
double := func(number int) int { return number * 2 }
fmt.Println(double(21))
} import java.util.function.Function;
class Main {
public static void main(String[] args) {
Function<Integer, Integer> doubler = number -> number * 2;
System.out.println(doubler.apply(21));
}
} Go treats functions as ordinary values with their own function type, so
func(int) int can be assigned to a variable directly. Java has no true function type; a lambda is really syntactic sugar for an anonymous implementation of a single-method interface — here Function<Integer, Integer> from java.util.function — and it must be invoked through that interface's method (.apply(21)), never called directly like doubler(21).Structs vs Classes
Structs vs classes
package main
import "fmt"
type Point struct {
X, Y int
}
func main() {
point := Point{X: 1, Y: 2}
fmt.Printf("%+v\n", point)
} class Point {
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return "Point{x=" + x + ", y=" + y + "}";
}
}
class Main {
public static void main(String[] args) {
Point point = new Point(1, 2);
System.out.println(point);
}
} This is one of the biggest structural differences between the languages. Go has no classes at all — a
struct is purely a collection of fields, and there is no constructor syntax (a struct literal fills fields directly, and unset fields simply take their zero value). Java's class bundles fields with a mandatory constructor, and printing an instance directly calls toString(), which must be overridden explicitly to produce readable output — Go's %+v verb gives readable field-by-field output automatically, with no method to write.Methods with receivers
package main
import "fmt"
type Rectangle struct {
Width, Height int
}
func (rectangle Rectangle) Area() int {
return rectangle.Width * rectangle.Height
}
func main() {
fmt.Println(Rectangle{Width: 3, Height: 4}.Area())
} class Rectangle {
int width;
int height;
Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
int area() {
return width * height;
}
}
class Main {
public static void main(String[] args) {
System.out.println(new Rectangle(3, 4).area());
}
} A Go method is a regular function with an extra
(rectangle Rectangle) receiver clause attaching it to a type — the method and the type it operates on are declared separately and can even live in different files. A Java method is defined inside the class body itself and refers to the instance implicitly through this rather than a named receiver parameter. Go additionally distinguishes value receivers (a copy) from pointer receivers (a reference) explicitly in the signature; Java objects are always accessed through a reference, so that distinction does not arise.Zero values vs constructors
package main
import "fmt"
type Counter struct {
Count int
}
func main() {
var counter Counter // zero value: Count is 0, no constructor needed
counter.Count++
fmt.Println(counter.Count)
} class Counter {
int count; // defaults to 0, but only as a field, not a local variable
void increment() {
count++;
}
}
class Main {
public static void main(String[] args) {
Counter counter = new Counter();
counter.increment();
System.out.println(counter.count);
}
} Every Go type has a well-defined zero value —
0 for numbers, "" for strings, nil for pointers and slices — so var counter Counter is immediately usable with no constructor call at all. Java gives instance fields a default value too (0, false, null), but an object itself must always be brought into existence with new; there is no way to have a usable Counter without calling some constructor, even an implicit no-argument one the compiler generates for you.Visibility: capitalization vs keywords
package main
import "fmt"
// Exported: starts with an uppercase letter, visible outside the package.
type Account struct {
Balance int // exported field
pin string // unexported: package-private
}
func main() {
account := Account{Balance: 100, pin: "1234"}
fmt.Println(account.Balance)
} class Account {
public int balance; // visible everywhere
private String pin; // visible only inside this class
Account(int balance, String pin) {
this.balance = balance;
this.pin = pin;
}
}
class Main {
public static void main(String[] args) {
Account account = new Account(100, "1234");
System.out.println(account.balance);
}
} Go signals visibility with capitalization alone: an identifier starting with an uppercase letter is exported (visible outside its package); lowercase is package-private. There is no
public/private keyword at all. Java instead has an explicit set of access-modifier keywords — public, protected, private, and package-private (the default, no keyword) — applied individually to each field and method, giving finer-grained control than Go's all-or-nothing-per-identifier rule at the cost of far more boilerplate.Composition vs Inheritance
Struct embedding vs extends
package main
import "fmt"
type Animal struct {
Name string
}
func (animal Animal) Speak() string {
return animal.Name + " makes a sound"
}
type Dog struct {
Animal // embedded — Dog gets Animal's fields and methods "for free"
}
func main() {
dog := Dog{Animal{Name: "Rex"}}
fmt.Println(dog.Speak()) // promoted method call
fmt.Println(dog.Name) // promoted field access
} class Animal {
protected final String name;
Animal(String name) {
this.name = name;
}
String speak() {
return name + " makes a sound";
}
}
class Dog extends Animal {
Dog(String name) {
super(name);
}
}
class Main {
public static void main(String[] args) {
Dog dog = new Dog("Rex");
System.out.println(dog.speak());
System.out.println(dog.name);
}
} Go has no
extends and no class hierarchy at all. Instead, a struct can embed another struct as an anonymous field, and the outer type automatically gains ("promotes") the embedded type's fields and methods, callable as if they belonged to it directly — but a Dog is never actually an Animal in the type system, it merely has one built in. Java's extends creates a true is-a relationship: a Dog reference can be assigned to an Animal variable, and virtual dispatch means overriding speak() in Dog changes what runs even when called through an Animal-typed reference — a form of polymorphism embedding does not provide.No virtual dispatch through embedding
package main
import "fmt"
type Base struct{}
func (base Base) Describe() string { return "I am Base" }
func (base Base) Announce() string { return base.Describe() }
type Derived struct {
Base
}
func (derived Derived) Describe() string { return "I am Derived" }
func main() {
derived := Derived{}
// Announce is defined on Base, so it calls Base's Describe,
// NOT Derived's — there is no virtual dispatch through embedding.
fmt.Println(derived.Announce())
fmt.Println(derived.Describe())
} class Base {
String describe() { return "I am Base"; }
String announce() { return describe(); }
}
class Derived extends Base {
@Override
String describe() { return "I am Derived"; }
}
class Main {
public static void main(String[] args) {
Derived derived = new Derived();
// announce() is defined on Base, but Java's dynamic dispatch means
// it calls Derived's override of describe() at runtime.
System.out.println(derived.announce());
System.out.println(derived.describe());
}
} This example exposes the sharpest practical difference between embedding and inheritance. In Go,
Base.Announce() calls Base.Describe() — embedding is not polymorphic, so overriding Describe on Derived has no effect on what Base's own methods call internally. In Java, every non-final, non-private method is virtual by default, so Base.announce() resolves describe() dynamically and picks up Derived's override at runtime. A Go programmer new to Java should expect this behavior change, not assume embedding and inheritance are interchangeable.Abstract classes
package main
import (
"fmt"
"math"
)
// Go has no abstract classes; an interface plus a helper function
// is the usual replacement for "shared behavior over a required contract".
type Shape interface {
Area() float64
}
func Describe(shape Shape) string {
return fmt.Sprintf("Area: %.2f", shape.Area())
}
type Circle struct{ Radius float64 }
func (circle Circle) Area() float64 { return math.Pi * circle.Radius * circle.Radius }
func main() {
fmt.Println(Describe(Circle{Radius: 5}))
} abstract class Shape {
abstract double area();
String describe() {
return "Area: %.2f".formatted(area());
}
}
class Circle extends Shape {
private final double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
double area() {
return Math.PI * radius * radius;
}
}
class Main {
public static void main(String[] args) {
System.out.println(new Circle(5).describe());
}
} Java's
abstract class can mix required methods (abstract double area();, no body, must be implemented) with concrete shared methods (describe(), implemented once and inherited by every subclass) — a middle ground between a pure interface and a fully-implemented class. Go has nothing directly equivalent, since it lacks inheritance entirely; the idiomatic replacement splits the same idea into an interface for the required contract and a free function for the shared behavior, as shown on the Go side.Interfaces
Implicit vs explicit implementation
package main
import "fmt"
type Speaker interface {
Speak() string
}
// Dog never mentions Speaker anywhere — it satisfies the interface
// simply by having a matching Speak() method. This is structural typing.
type Dog struct{}
func (dog Dog) Speak() string { return "Woof!" }
func announce(speaker Speaker) {
fmt.Println(speaker.Speak())
}
func main() {
announce(Dog{})
} interface Speaker {
String speak();
}
// Dog must explicitly declare "implements Speaker" — the compiler checks
// the contract is fully satisfied at the point of declaration, not at
// the point of use.
class Dog implements Speaker {
@Override
public String speak() {
return "Woof!";
}
}
class Main {
static void announce(Speaker speaker) {
System.out.println(speaker.speak());
}
public static void main(String[] args) {
announce(new Dog());
}
} This is the single biggest philosophical gap between Go and Java. Go interfaces are satisfied structurally:
Dog implements Speaker simply by having a method with a matching name and signature — there is no implements clause, no declared relationship anywhere, and Dog may not even know Speaker exists. Java interfaces are satisfied nominally: a class must explicitly write implements Speaker, and the compiler checks that contract exactly where it is declared. Go's approach means interfaces can be defined after the fact by whoever needs one (even in a completely different package than the implementing type); Java's approach means the relationship is always visible right at the class declaration, at the cost of requiring foresight or after-the-fact edits to add a new interface.One type satisfying many interfaces
package main
import "fmt"
type Reader interface{ Read() string }
type Writer interface{ Write(text string) }
// File satisfies BOTH interfaces just by having both methods —
// no declaration required, and neither interface needs to know
// the other exists.
type File struct{ contents string }
func (file *File) Read() string { return file.contents }
func (file *File) Write(text string) { file.contents = text }
func main() {
var file File
var writer Writer = &file
writer.Write("hello")
var reader Reader = &file
fmt.Println(reader.Read())
} interface Readable {
String read();
}
interface Writable {
void write(String text);
}
// File must explicitly implement both interfaces in its declaration.
class File implements Readable, Writable {
private String contents = "";
@Override
public String read() {
return contents;
}
@Override
public void write(String text) {
contents = text;
}
}
class Main {
public static void main(String[] args) {
File file = new File();
Writable writer = file;
writer.write("hello");
Readable reader = file;
System.out.println(reader.read());
}
} Go's structural typing means a single type can satisfy any number of interfaces without ever naming them, which is why the Go standard library defines so many tiny, single-method interfaces (
io.Reader, io.Writer, fmt.Stringer) — any type with the right method automatically qualifies. Java allows the same outcome, a class implementing multiple interfaces, but every one of them must be listed explicitly in a comma-separated implements clause, so extending a class to satisfy a newly discovered interface always requires editing that class's source.Default methods
package main
import "fmt"
// Go interfaces are pure contracts with no implementation at all;
// shared behavior has to live in a free function or an embedded struct.
type Named interface {
Name() string
}
func Greet(named Named) string {
return "Hi, " + named.Name()
}
type Person struct{ PersonName string }
func (person Person) Name() string { return person.PersonName }
func main() {
fmt.Println(Greet(Person{PersonName: "Sam"}))
} interface Named {
String name();
// A default method: Java interfaces CAN carry a body since Java 8.
default String greet() {
return "Hi, " + name();
}
}
class Person implements Named {
private final String personName;
Person(String personName) {
this.personName = personName;
}
@Override
public String name() {
return personName;
}
}
class Main {
public static void main(String[] args) {
Person person = new Person("Sam");
System.out.println(person.greet());
}
} Since Java 8, an interface method marked
default can carry an implementation, so greet() works for every implementer that supplies name(), with no free-standing helper function needed. Go interfaces cannot do this at all — they remain pure method signatures with zero implementation, so any shared behavior across implementers must live outside the interface, either as a free function (as shown on the Go side) or via struct embedding.Type switches vs instanceof
package main
import "fmt"
type Shape interface{ Area() float64 }
type Circle struct{ Radius float64 }
func (circle Circle) Area() float64 { return 3.14159 * circle.Radius * circle.Radius }
type Square struct{ Side float64 }
func (square Square) Area() float64 { return square.Side * square.Side }
func describe(shape Shape) string {
switch value := shape.(type) {
case Circle:
return fmt.Sprintf("circle, radius %.1f", value.Radius)
case Square:
return fmt.Sprintf("square, side %.1f", value.Side)
default:
return "unknown shape"
}
}
func main() {
fmt.Println(describe(Circle{Radius: 2}))
} sealed interface Shape permits Circle, Square {
double area();
}
record Circle(double radius) implements Shape {
public double area() { return 3.14159 * radius * radius; }
}
record Square(double side) implements Shape {
public double area() { return side * side; }
}
class Main {
static String describe(Shape shape) {
return switch (shape) {
case Circle circle -> "circle, radius %.1f".formatted(circle.radius());
case Square square -> "square, side %.1f".formatted(square.side());
};
}
public static void main(String[] args) {
System.out.println(describe(new Circle(2)));
}
} Go's type switch,
switch value := shape.(type), inspects the concrete type behind an interface value and binds it in each case — the idiomatic way to recover specific behavior from a general interface. Modern Java (21+) offers a strikingly similar-looking feature: pattern matching for switch, which deconstructs a sealed interface's permitted implementations. The key difference is exhaustiveness — because Shape is declared sealed permits Circle, Square, the compiler knows those are the only two possible cases and requires no default branch at all, catching a missing case at compile time rather than letting it fall through silently as Go's type switch would without an explicit default.Error Handling
Error values vs exceptions
package main
import (
"errors"
"fmt"
)
func divide(numerator, denominator int) (int, error) {
if denominator == 0 {
return 0, errors.New("division by zero")
}
return numerator / denominator, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(result)
} class Main {
static int divide(int numerator, int denominator) {
if (denominator == 0) {
throw new ArithmeticException("division by zero");
}
return numerator / denominator;
}
public static void main(String[] args) {
try {
int result = divide(10, 0);
System.out.println(result);
} catch (ArithmeticException error) {
System.out.println("error: " + error.getMessage());
}
}
} This is the other headline philosophical difference alongside interfaces. Go has no exceptions for ordinary error conditions: a failing operation returns an
error as an ordinary value alongside its result, and the caller is expected — by convention, not compiler enforcement — to check if err != nil after every call that can fail. Java routes failures through a completely separate control-flow channel: throw unwinds the stack immediately, skipping all remaining code until a matching catch block is found, however many function calls away that may be. A Go programmer's instinct to check a return value for failure has no direct target in Java — the failure never appears in the return value at all.Checked vs unchecked exceptions
package main
import (
"errors"
"fmt"
)
// Go has exactly one error handling convention: every fallible function
// returns an error, checked or not entirely at the caller's discretion —
// there is no compiler-enforced distinction like Java's checked exceptions.
func readConfig(path string) (string, error) {
if path == "" {
return "", errors.New("empty path")
}
return "contents", nil
}
func main() {
contents, err := readConfig("")
if err != nil {
fmt.Println("failed:", err)
return
}
fmt.Println(contents)
} class ConfigException extends Exception {
ConfigException(String message) {
super(message);
}
}
class Main {
// "throws ConfigException" is part of the method's contract: any
// caller MUST either catch it or declare it themselves. The compiler
// enforces this — it will not compile otherwise.
static String readConfig(String path) throws ConfigException {
if (path.isEmpty()) {
throw new ConfigException("empty path");
}
return "contents";
}
public static void main(String[] args) {
try {
String contents = readConfig("");
System.out.println(contents);
} catch (ConfigException error) {
System.out.println("failed: " + error.getMessage());
}
}
} Java splits its exceptions into two enforced categories with no Go equivalent to either. A checked exception (any subclass of
Exception other than RuntimeException) must appear in a throws clause on every method that can propagate it, and every caller must either catch it or declare it themselves — the compiler rejects code that ignores the possibility. An unchecked exception (RuntimeException and its subclasses, like ArithmeticException or NullPointerException) requires no declaration and can be silently ignored by the caller with the code still compiling. Go simply has none of this machinery: every error is a plain value, and nothing in the language forces a caller to inspect it — the closest Go gets to "checked" is the community convention (and linters like errcheck) that flag an unchecked error, not the compiler itself.Wrapping errors
package main
import (
"errors"
"fmt"
)
func loadUser(id int) error {
if id < 0 {
return fmt.Errorf("loading user %d: %w", id, errors.New("invalid id"))
}
return nil
}
func main() {
err := loadUser(-1)
fmt.Println(err)
fmt.Println(errors.Unwrap(err))
} class Main {
static void loadUser(int id) {
if (id < 0) {
// The original exception is passed as the "cause", preserving
// the full stack trace chain for debugging.
throw new RuntimeException("loading user " + id, new IllegalArgumentException("invalid id"));
}
}
public static void main(String[] args) {
try {
loadUser(-1);
} catch (RuntimeException error) {
System.out.println(error.getMessage());
System.out.println(error.getCause().getMessage());
}
}
} Both languages let a higher-level failure carry a lower-level one as context, but the mechanism differs. Go's
%w verb in fmt.Errorf wraps an existing error inside a new one, and errors.Unwrap (or errors.Is/errors.As) walks that chain back to the original — errors remain ordinary values throughout. Java's exception constructors accept an optional cause argument, and getCause() retrieves it; because exceptions carry a captured stack trace at the point they were constructed, the cause chain shown by an uncaught exception's printed trace typically gives a Java programmer more context for free than Go's wrapped error chain does, at the cost of the stack-trace-capture overhead on every throw.Cleanup: deferred error checks vs finally
package main
import "fmt"
func process() (err error) {
defer func() {
fmt.Println("cleanup always runs")
}()
return fmt.Errorf("something failed")
}
func main() {
if err := process(); err != nil {
fmt.Println("caught:", err)
}
} class Main {
static void process() {
try {
throw new RuntimeException("something failed");
} finally {
System.out.println("cleanup always runs");
}
}
public static void main(String[] args) {
try {
process();
} catch (RuntimeException error) {
System.out.println("caught: " + error.getMessage());
}
}
} Both languages guarantee cleanup code runs whether or not the function completes normally. Go's
defer schedules a call to run when the enclosing function returns, regardless of whether it returned normally or is unwinding from a panic. Java's finally block, attached to a try, runs whether the try completed normally, threw an exception, or hit a return — the same guarantee, expressed as a dedicated clause of the exception-handling syntax rather than a general-purpose statement scheduler.Resource Cleanup
defer vs try-with-resources
package main
import "fmt"
type Resource struct{ Name string }
func (resource *Resource) Close() {
fmt.Println("closing", resource.Name)
}
func openResource(name string) *Resource {
fmt.Println("opening", name)
return &Resource{Name: name}
}
func main() {
resource := openResource("file.txt")
defer resource.Close() // runs when main() returns, in LIFO order
fmt.Println("using", resource.Name)
} class Resource implements AutoCloseable {
private final String name;
Resource(String name) {
this.name = name;
System.out.println("opening " + name);
}
@Override
public void close() {
System.out.println("closing " + name);
}
}
class Main {
public static void main(String[] args) {
// try-with-resources: close() runs automatically when this block
// exits, in reverse order of declaration if there are several.
try (Resource resource = new Resource("file.txt")) {
System.out.println("using file.txt");
}
}
} Go's
defer is a general-purpose mechanism: any function call can be deferred, and it runs when the enclosing function returns, in last-in-first-out order relative to other deferred calls. Java's try-with-resources is narrower but more automatic: any object implementing AutoCloseable declared in the parentheses of a try is closed automatically at the end of that block, not the whole method, and requires no explicit close() call to remember. Where a Go function typically pairs an open... call with an immediately following defer resource.Close(), idiomatic Java expresses the same open-use-close lifecycle as the shape of the try statement itself.Closing multiple resources
package main
import "fmt"
type Resource struct{ Name string }
func (resource *Resource) Close() {
fmt.Println("closing", resource.Name)
}
func main() {
first := &Resource{Name: "first"}
second := &Resource{Name: "second"}
// Deferred calls run LIFO: second closes before first.
defer first.Close()
defer second.Close()
fmt.Println("using both resources")
} class Resource implements AutoCloseable {
private final String name;
Resource(String name) {
this.name = name;
}
@Override
public void close() {
System.out.println("closing " + name);
}
}
class Main {
public static void main(String[] args) {
// try-with-resources also closes in reverse (LIFO) order:
// second closes before first.
try (Resource first = new Resource("first");
Resource second = new Resource("second")) {
System.out.println("using both resources");
}
}
} Both mechanisms agree on ordering — the most recently acquired resource is released first — which matters when resources depend on one another (a connection opened after a socket must close before that socket does). Go achieves this because
defer is a stack: each deferred call is pushed and popped in reverse order. Java achieves the identical LIFO order because try-with-resources closes each declared resource in the reverse of its declaration order, semicolon-separated in the same parentheses.Generics
Generic functions
package main
import "fmt"
func Max[T int | float64](first, second T) T {
if first > second {
return first
}
return second
}
func main() {
fmt.Println(Max(3, 7))
fmt.Println(Max(2.5, 1.5))
} class Main {
static <T extends Comparable<T>> T max(T first, T second) {
return first.compareTo(second) > 0 ? first : second;
}
public static void main(String[] args) {
System.out.println(max(3, 7));
System.out.println(max(2.5, 1.5));
}
} Go generics arrived in 1.18 and are deliberately simple: a type parameter is constrained by a type set, such as
int | float64, listing exactly which concrete types are allowed. Java generics predate Go's by well over a decade and are constrained by bounds expressed through interfaces, such as T extends Comparable<T>, which admits any type implementing that interface rather than an explicit enumerated list — more open-ended, but also why Java needed an interface like Comparable to express "anything orderable" where Go can just list the primitive types directly.Generic types
package main
import "fmt"
type Pair[T any] struct {
First, Second T
}
func main() {
pair := Pair[string]{First: "a", Second: "b"}
fmt.Println(pair.First, pair.Second)
} class Pair<T> {
T first;
T second;
Pair(T first, T second) {
this.first = first;
this.second = second;
}
}
class Main {
public static void main(String[] args) {
Pair<String> pair = new Pair<>("a", "b");
System.out.println(pair.first + " " + pair.second);
}
} Generic structs and generic classes look nearly identical:
Pair[T any] versus Pair<T>, with the unconstrained placeholder spelled any in Go and simply bare T in Java. The instantiation syntax differs slightly — Go repeats the type argument explicitly, Pair[string]{...}, while Java infers it from the diamond operator <> when the constructor arguments make it unambiguous, as in new Pair<>("a", "b").Type erasure vs monomorphization
package main
import "fmt"
// Go generics are monomorphized at compile time: the compiler
// generates a specialized version of PrintAll for each concrete
// type it's called with, so runtime type information is exact.
func PrintAll[T any](items []T) {
for _, item := range items {
fmt.Println(item)
}
}
func main() {
PrintAll([]int{1, 2, 3})
} import java.util.List;
class Main {
// Java generics are erased at compile time: at runtime there is only
// one printAll method, and List<T> is really just List — the type
// parameter T does not exist once compiled, so you cannot do
// "new T()" or "items instanceof List<Integer>" here.
static <T> void printAll(List<T> items) {
for (T item : items) {
System.out.println(item);
}
}
public static void main(String[] args) {
printAll(List.of(1, 2, 3));
}
} This is a deep implementation difference that occasionally surfaces as a real limitation. Go compiles a distinct specialized version of a generic function for each concrete type it is instantiated with (monomorphization), so the compiled code always has full, exact type information at runtime. Java instead erases type parameters after compile-time checking — at runtime,
List<Integer> and List<String> are both just List, with no way to recover T via reflection or instanceof. This is precisely why Java cannot write new T() inside a generic method, and why arrays of a generic type parameter are disallowed — limitations Go's generics do not share.Concurrency
Goroutines vs threads
package main
import (
"fmt"
"sync"
)
func main() {
var waitGroup sync.WaitGroup
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
fmt.Println("hello from a goroutine")
}()
waitGroup.Wait()
fmt.Println("main done")
} class Main {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("hello from a thread");
});
thread.start();
thread.join();
System.out.println("main done");
}
} Go's
go keyword launches a goroutine: a lightweight function scheduled cooperatively by the Go runtime, cheap enough that a program can spawn hundreds of thousands of them, starting at only a few kilobytes of stack each. Java's Thread traditionally maps to a real, comparatively heavyweight operating-system thread, so spawning thousands of them was historically expensive — a genuine capability gap that only closed with Java 21's virtual threads (see the next entry). waitGroup.Wait() and thread.join() serve the same purpose: blocking until the concurrent work finishes.Virtual threads — Java's bridge to goroutines
package main
import (
"fmt"
"sync"
)
func main() {
var waitGroup sync.WaitGroup
results := make([]int, 1000)
for index := 0; index < 1000; index++ {
waitGroup.Add(1)
go func(position int) {
defer waitGroup.Done()
results[position] = position * position
}(index)
}
waitGroup.Wait()
fmt.Println(results[999])
} import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.ArrayList;
import java.util.List;
class Main {
public static void main(String[] args) throws Exception {
// Virtual threads (Java 21+) are cheap, JVM-managed threads —
// Java's answer to Go's goroutines. Spawning 1000 of them is
// now practical, where 1000 platform Threads would be wasteful.
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<Integer>> futures = new ArrayList<>();
for (int index = 0; index < 1000; index++) {
int position = index;
futures.add(executor.submit(() -> position * position));
}
System.out.println(futures.get(999).get());
}
}
} Virtual threads, finalized in Java 21, are the modern bridge point between the two languages' concurrency models. Like a goroutine, a virtual thread is scheduled by the JVM onto a small pool of real OS threads rather than consuming one itself, so a Java program can now spawn hundreds of thousands of virtual threads just as cheaply as a Go program spawns goroutines. The programming model still looks like traditional Java threads (
Thread, ExecutorService, blocking calls) rather than adopting Go's channel-based style, but the cost profile that used to make "one thread per request" impractical in Java no longer applies.Channels vs blocking queues
package main
import "fmt"
func main() {
messages := make(chan int)
go func() {
for index := 0; index < 3; index++ {
messages <- index * index
}
close(messages)
}()
for value := range messages {
fmt.Println(value)
}
} import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
class Main {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> messages = new ArrayBlockingQueue<>(1);
Thread producer = new Thread(() -> {
try {
for (int index = 0; index < 3; index++) {
messages.put(index * index);
}
} catch (InterruptedException error) {
Thread.currentThread().interrupt();
}
});
producer.start();
for (int received = 0; received < 3; received++) {
System.out.println(messages.take());
}
producer.join();
}
} Go's channels are a language-level primitive with dedicated syntax (
chan, <-, range, close) built around the philosophy "do not communicate by sharing memory; share memory by communicating." Java has no channel syntax in the language itself, but java.util.concurrent.BlockingQueue provides the same fundamental capability as an ordinary library class: put() blocks when the queue is full (mirroring a send on an unbuffered or full channel), and take() blocks when it is empty (mirroring a receive). Java has no built-in equivalent of closing a channel to signal "no more values" — the producer and consumer must agree on their own termination protocol, such as a sentinel value or a separate completion signal.select vs waiting on multiple futures
package main
import (
"fmt"
"time"
)
func main() {
fast := make(chan string)
slow := make(chan string)
go func() { time.Sleep(1 * time.Millisecond); fast <- "fast result" }()
go func() { time.Sleep(5 * time.Millisecond); slow <- "slow result" }()
select {
case result := <-fast:
fmt.Println(result)
case result := <-slow:
fmt.Println(result)
}
} import java.util.concurrent.CompletableFuture;
class Main {
public static void main(String[] args) throws Exception {
CompletableFuture<String> fast = CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(1); } catch (InterruptedException ignored) {}
return "fast result";
});
CompletableFuture<String> slow = CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(5); } catch (InterruptedException ignored) {}
return "slow result";
});
// anyOf races the futures; whichever completes first wins.
Object winner = CompletableFuture.anyOf(fast, slow).get();
System.out.println(winner);
}
} Go's
select statement waits on multiple channel operations at once and proceeds with whichever is ready first, a language-level construct with no direct Java equivalent. The closest match in Java's concurrency library is CompletableFuture.anyOf(...), which completes as soon as any of several asynchronous computations finishes. The Java version is coarser-grained — it races entire results rather than individual channel sends and receives, and it lacks select's default case for a non-blocking attempt — but it addresses the same underlying need: proceeding with whichever of several concurrent operations finishes first.Gotchas for Go Programmers
Everything is an Object
package main
import "fmt"
// Go has no universal base type — an int, a struct, and an interface
// value are unrelated unless you explicitly ask for interface{}/any.
func describe(value any) {
fmt.Printf("%v has type %T\n", value, value)
}
func main() {
describe(42)
describe("hello")
} class Main {
public static void main(String[] args) {
// Every reference type in Java implicitly extends java.lang.Object,
// which is why every object has toString(), equals(), and
// hashCode() whether or not its class defines them.
Object number = 42; // autoboxed to Integer, an Object subtype
Object text = "hello";
System.out.println(number.getClass().getSimpleName());
System.out.println(text.getClass().getSimpleName());
}
} Every Java class implicitly extends
java.lang.Object unless it names a different superclass, so every reference type automatically inherits toString(), equals(), hashCode(), and a handful of other methods — which is why an unfamiliar object can always be printed or compared, even badly, without writing any code. Go has no such universal root type; every type stands alone unless a piece of code explicitly asks for the empty interface any (an alias for interface{}), which is structurally satisfied by literally everything but confers no methods of its own.Autoboxing and Integer caching
package main
import "fmt"
func main() {
// Go has no boxing at all: an int is always an int, and comparing
// two ints with == always compares their values directly.
first := 200
second := 200
fmt.Println(first == second) // true, always
} class Main {
public static void main(String[] args) {
Integer small1 = 100;
Integer small2 = 100;
System.out.println(small1 == small2); // true — cached, same object
Integer big1 = 200;
Integer big2 = 200;
System.out.println(big1 == big2); // false! different objects
System.out.println(big1.equals(big2)); // true — always compare boxed values with equals()
}
} This surprises nearly every programmer coming from a language without boxed primitives. The Java compiler autoboxes an
int literal into an Integer object, and the JVM caches (and reuses) Integer instances for the range -128 to 127 as a performance optimization — so == on two boxed 100s happens to return true because they are literally the same cached object, while == on two boxed 200s returns false because they fall outside the cache and are separate objects. Go has no boxing to speak of: an int is always a plain value, and == always compares those values directly with no object-identity trap lurking underneath. The reliable fix in Java is the same one described in the next entry: never use == to compare boxed values, always use .equals().== vs .equals()
package main
import "fmt"
func main() {
// Go's == on structs and strings always compares values field-by-field
// (for comparable types) — there is no separate identity-comparison
// method to reach for.
first := "hello"
second := "hello"
fmt.Println(first == second) // true — value comparison
} class Main {
public static void main(String[] args) {
String first = "hello";
String second = new String("hello"); // force a distinct object
System.out.println(first == second); // false — different objects
System.out.println(first.equals(second)); // true — same content
// The classic bug: using == out of habit and getting away with
// it sometimes, because string literals happen to be interned.
String third = "hello";
System.out.println(first == third); // true, but don't rely on this!
}
} This is the single most common Java bug for programmers arriving from a language where
== means value equality. In Java, == on reference types (which includes String and every custom class) compares object identity — are these the exact same object in memory — never content. .equals() is the method that must be called for content comparison, and a class only gets meaningful content-based .equals() behavior if it overrides the default (identity-based) implementation inherited from Object. Go sidesteps this entire class of bug: == on strings and on comparable structs always performs a genuine value comparison, so there is no separate method to remember to call.null vs nil and NullPointerException
package main
import "fmt"
type Person struct {
Name string
}
func findPerson(id int) *Person {
if id == 1 {
return &Person{Name: "Alice"}
}
return nil
}
func main() {
person := findPerson(99)
if person == nil {
fmt.Println("not found")
return
}
fmt.Println(person.Name)
} class Person {
String name;
Person(String name) {
this.name = name;
}
}
class Main {
static Person findPerson(int id) {
if (id == 1) {
return new Person("Alice");
}
return null;
}
public static void main(String[] args) {
Person person = findPerson(99);
if (person == null) {
System.out.println("not found");
return;
}
// Forgetting the null check above throws NullPointerException here:
System.out.println(person.name);
}
} Both languages have an absent-reference value spelled almost identically (Go's
nil, Java's null) and both let a programmer forget to check for it. The consequence differs sharply: dereferencing a nil pointer in Go triggers a runtime panic that, left unhandled, crashes the whole program with a stack trace pointing at the dereference. Dereferencing null in Java throws a catchable NullPointerException that unwinds like any other exception and can be caught with an ordinary try/catch — genuinely recoverable, unlike an unrecovered Go panic, though in practice a NullPointerException almost always indicates a bug that should be fixed rather than caught.Verbosity compared to Go's minimalism
package main
import "fmt"
type Point struct {
X, Y int
}
func main() {
points := []Point{{1, 2}, {3, 4}, {5, 6}}
fmt.Println(points)
} import java.util.List;
class Main {
record Point(int x, int y) {}
public static void main(String[] args) {
List<Point> points = List.of(new Point(1, 2), new Point(3, 4), new Point(5, 6));
System.out.println(points);
}
} Go was designed around a small, deliberately minimal keyword set and a strong cultural bias toward brevity — no annotations, no checked-exception boilerplate, no access-modifier ceremony for a simple data holder. Traditional Java earned a reputation for verbosity precisely because a simple data class once needed a hand-written constructor, getters,
equals(), hashCode(), and toString() — dozens of lines for what a Go struct expresses in three. Modern Java has narrowed this gap considerably: a record (shown here, Java 16+) generates all of that boilerplate automatically from a single-line declaration, though Java's surface area — interfaces, abstract classes, annotations, four access-modifier levels, checked exceptions — remains considerably larger than Go's twenty-five keywords by design.Checked exceptions forcing signature changes
package main
import (
"errors"
"fmt"
)
// Go's convention: adding a new failure mode to a low-level function
// just means adding it to the returned error — every caller already
// has an error to check, so no signature needs to change shape.
func readSetting(name string) (string, error) {
if name == "" {
return "", errors.New("empty setting name")
}
return "value", nil
}
func loadConfig() (string, error) {
return readSetting("timeout")
}
func main() {
value, err := loadConfig()
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(value)
} class SettingException extends Exception {
SettingException(String message) { super(message); }
}
class Main {
// readSetting declares "throws SettingException" in its signature...
static String readSetting(String name) throws SettingException {
if (name.isEmpty()) {
throw new SettingException("empty setting name");
}
return "value";
}
// ...which forces loadConfig to ALSO declare "throws SettingException",
// even though loadConfig itself never directly throws one. Every
// caller up the chain must repeat the declaration or catch it.
static String loadConfig() throws SettingException {
return readSetting("timeout");
}
public static void main(String[] args) {
try {
System.out.println(loadConfig());
} catch (SettingException error) {
System.out.println("error: " + error.getMessage());
}
}
} This is a real maintenance cost that surprises Go programmers the first time they hit it. In Go, every fallible function already returns an
error, so adding a new failure case deep in a call chain never changes any function's signature — the caller was always free to check or ignore it. In Java, a checked exception forces every intermediate method between where it is thrown and where it is caught to add it to their own throws clause, purely to satisfy the compiler, even if that intermediate method has no interest in the exception itself — a change several layers down can ripple upward through signatures that otherwise would not need to change at all. This cost is exactly why many modern Java libraries and style guides favor unchecked exceptions for most application-level failures, reserving checked exceptions for cases the caller genuinely must handle.