Output & Running
Hello, World
class Main {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
} fmt.Println("Hello, World!") Go's
fmt.Println() replaces Java's System.out.println(). A Go program needs only a package main declaration and a func main() — no class wrapping, no public modifier, no boilerplate.Formatted output
class Main {
public static void main(String[] args) {
String name = "Alice";
int age = 30;
System.out.printf("Name: %s, Age: %d%n", name, age);
System.out.println("Name: " + name + ", Age: " + age);
}
} name := "Alice"
age := 30
fmt.Printf("Name: %s, Age: %d\n", name, age)
fmt.Println("Name:", name+",", "Age:", age) Go's
fmt.Printf() uses the same %s, %d, %f format verbs as Java's String.format() and printf(). The %v verb prints any value with its default format — useful when you don't know or care about the exact type.Building strings (Sprintf)
class Main {
public static void main(String[] args) {
String name = "Alice";
int score = 95;
String message = String.format("Player %s scored %d", name, score);
System.out.println(message);
}
} name := "Alice"
score := 95
message := fmt.Sprintf("Player %s scored %d", name, score)
fmt.Println(message) fmt.Sprintf() is the Go equivalent of Java's String.format() — it returns a formatted string without printing it. The fmt package consistently uses the Print, Println, and Printf naming pattern for both console output and string building.Debug printing
import java.util.List;
class Main {
public static void main(String[] args) {
var numbers = List.of(1, 2, 3);
System.out.println(numbers); // [1, 2, 3]
System.out.println(numbers.getClass()); // class java.util.ImmutableCollections$List12
}
} numbers := []int{1, 2, 3}
fmt.Println(numbers) // [1 2 3]
fmt.Printf("%T\n", numbers) // []int
fmt.Printf("%#v\n", numbers) // []int{1, 2, 3} Go's
%T verb prints the type, and %#v prints a Go-syntax representation — more useful than Java's getClass(). Note that Go prints slices without commas: [1 2 3] not [1, 2, 3].Variables & Types
:= short variable declaration
class Main {
public static void main(String[] args) {
var count = 42; // Java 10+ type inference
var greeting = "Hello";
var ratio = 3.14;
System.out.println(count + " " + greeting + " " + ratio);
}
} count := 42 // short declaration — infers type
greeting := "Hello"
ratio := 3.14
fmt.Println(count, greeting, ratio) Go's
:= declares and assigns in one step — the compiler infers the type. := can only be used inside functions; package-level variables must use the var keyword. Unlike Java's var, := can declare multiple variables at once: x, y := 1, 2.Zero values (no null)
class Main {
public static void main(String[] args) {
int count; // uninitialized — compile error if read before assign
String text = null; // reference types default to null
if (text == null) System.out.println("text is null");
}
} var count int // zero value: 0
var text string // zero value: ""
var active bool // zero value: false
fmt.Println(count, text, active) Go has no
null for value types. Every variable has a zero value — 0 for numbers, "" for strings, false for booleans. This eliminates an entire class of NullPointerExceptions. Pointers, slices, maps, and interfaces have a zero value of nil.Constants
class Main {
static final int MAX_RETRIES = 3;
static final double PI = 3.14159;
public static void main(String[] args) {
System.out.println(MAX_RETRIES);
System.out.println(PI);
}
} const MaxRetries = 3
const Pi = 3.14159
fmt.Println(MaxRetries)
fmt.Println(Pi) Go constants are declared with
const at the package level or inside functions. Exported names start with an uppercase letter — Go's visibility mechanism replaces Java's public/private. There is no static final — Go has no static class members at all.Multiple assignment
class Main {
public static void main(String[] args) {
int x = 1, y = 2;
// swap requires a temp:
int temp = x;
x = y;
y = temp;
System.out.println(x + " " + y);
}
} x, y := 1, 2
x, y = y, x // swap in one line — no temp variable needed
fmt.Println(x, y) Go supports simultaneous assignment: the right side is fully evaluated before any assignment occurs. This makes swapping trivial and is how multiple return values are unpacked. Java requires a temporary variable for swaps.
Type conversion
class Main {
public static void main(String[] args) {
int intVal = 42;
double doubleVal = (double) intVal; // widening cast
int truncated = (int) 3.99; // narrowing cast
String text = Integer.toString(intVal);
int parsed = Integer.parseInt("123");
System.out.println(doubleVal + " " + truncated + " " + text + " " + parsed);
}
} intVal := 42
doubleVal := float64(intVal) // explicit conversion
price := 3.99
truncated := int(price) // truncates
text := strconv.Itoa(intVal) // int to string
parsed, _ := strconv.Atoi("123") // string to int
fmt.Println(doubleVal, truncated, text, parsed) Go has no implicit numeric conversions —
float64(x) and int(x) are function-call syntax, not cast syntax. There is no widening: adding an int and a float64 is a compile error. strconv.Itoa and strconv.Atoi replace Java's Integer.toString() and Integer.parseInt().Strings
String basics
class Main {
public static void main(String[] args) {
String greeting = "Hello";
String name = "World";
String combined = greeting + ", " + name + "!";
System.out.println(combined);
System.out.println(combined.length());
}
} greeting := "Hello"
name := "World"
combined := greeting + ", " + name + "!"
fmt.Println(combined)
fmt.Println(len(combined)) Go strings are immutable UTF-8 byte sequences. Concatenation with
+ works the same as Java, but len() returns the number of bytes, not characters — use utf8.RuneCountInString() for character (rune) count. Use strings.Builder for repeated concatenation in a loop.Common string operations
class Main {
public static void main(String[] args) {
String text = " Hello, World! ";
System.out.println(text.strip());
System.out.println(text.strip().toUpperCase());
System.out.println(text.strip().replace("World", "Go"));
System.out.println(text.strip().contains("Hello"));
System.out.println(text.strip().startsWith("Hello"));
}
} text := " Hello, World! "
fmt.Println(strings.TrimSpace(text))
fmt.Println(strings.ToUpper(strings.TrimSpace(text)))
fmt.Println(strings.Replace(strings.TrimSpace(text), "World", "Go", -1))
fmt.Println(strings.Contains(strings.TrimSpace(text), "Hello"))
fmt.Println(strings.HasPrefix(strings.TrimSpace(text), "Hello")) Go string operations live in the
strings package as standalone functions rather than methods on a string type. strings.TrimSpace() replaces strip()/trim(). The -1 in strings.Replace() means "replace all occurrences" — use strings.ReplaceAll() for the same effect.Split and join
import java.util.Arrays;
class Main {
public static void main(String[] args) {
String sentence = "one two three";
String[] words = sentence.split(" ");
System.out.println(Arrays.toString(words));
String joined = String.join(", ", words);
System.out.println(joined);
}
} sentence := "one two three"
words := strings.Split(sentence, " ")
fmt.Println(words)
joined := strings.Join(words, ", ")
fmt.Println(joined) strings.Split() and strings.Join() replace Java's String.split() and String.join(). Note that Java's split() takes a regex pattern while Go's takes a literal string — use strings.FieldsFunc() for regex-based splitting.StringBuilder equivalent
class Main {
public static void main(String[] args) {
var builder = new StringBuilder();
for (int i = 0; i < 5; i++) {
builder.append("item ").append(i).append("\n");
}
System.out.print(builder.toString());
}
} var builder strings.Builder
for i := range 5 {
fmt.Fprintf(&builder, "item %d\n", i)
}
fmt.Print(builder.String()) Go's
strings.Builder replaces Java's StringBuilder for efficient string construction in loops. fmt.Fprintf() writes formatted output directly into any io.Writer, including a strings.Builder. Use .WriteString() for plain string appends.Numbers
Numeric types
class Main {
public static void main(String[] args) {
byte small = 127;
short medium = 32767;
int normal = 2_147_483_647;
long large = 9_223_372_036_854_775_807L;
float ratio = 3.14f;
double pi = 3.14159265358979;
System.out.println(small + " " + medium + " " + normal + " " + large);
System.out.println(ratio + " " + pi);
}
} var small int8 = 127
var medium int16 = 32767
normal := 2_147_483_647 // int (platform-native: 64-bit on 64-bit systems)
large := int64(9_223_372_036_854_775_807)
ratio := float32(3.14)
pi := 3.14159265358979 // float64
fmt.Println(small, medium, normal, large)
fmt.Println(ratio, pi) Go has
int8, int16, int32, int64 (and unsigned uint variants). Bare int is platform-native width (64-bit on 64-bit systems). Unlike Java, Go has no Integer/Long wrapper types — all numeric types are value types.Arithmetic and integer division
class Main {
public static void main(String[] args) {
System.out.println(10 / 3); // 3 (integer division)
System.out.println(10.0 / 3); // 3.333...
System.out.println(10 % 3); // 1
System.out.println(Math.pow(2, 10)); // 1024.0
System.out.println(Math.abs(-42));
}
} fmt.Println(10 / 3) // 3 (integer division)
fmt.Println(10.0 / 3.0) // 3.333...
fmt.Println(10 % 3) // 1
fmt.Println(math.Pow(2, 10)) // 1024
fmt.Println(math.Abs(-42)) Integer division behavior is identical: dividing two integers truncates. Go has no
Math.pow() method — use math.Pow() (which returns float64). For integer exponentiation, use a loop or the standard library math/big package.Slices vs ArrayList
Creating and appending
import java.util.ArrayList;
class Main {
public static void main(String[] args) {
var numbers = new ArrayList<Integer>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
System.out.println(numbers);
System.out.println(numbers.size());
}
} numbers := []int{}
numbers = append(numbers, 1)
numbers = append(numbers, 2)
numbers = append(numbers, 3)
fmt.Println(numbers)
fmt.Println(len(numbers)) Go slices are dynamically sized like Java's
ArrayList. append() always returns the (possibly new) slice — you must assign the result back. The []int{1, 2, 3} literal creates a pre-populated slice. len() replaces .size().Slice literals and access
import java.util.ArrayList;
import java.util.List;
class Main {
public static void main(String[] args) {
var fruits = new ArrayList<String>(List.of("apple", "banana", "cherry"));
System.out.println(fruits.get(0));
System.out.println(fruits.get(fruits.size() - 1));
fruits.set(1, "blueberry");
System.out.println(fruits);
}
} fruits := []string{"apple", "banana", "cherry"}
fmt.Println(fruits[0])
fmt.Println(fruits[len(fruits)-1])
fruits[1] = "blueberry"
fmt.Println(fruits) Go slices use bracket index notation like arrays. There is no negative indexing — use
fruits[len(fruits)-1] for the last element. Index-out-of-bounds causes a runtime panic, not an IndexOutOfBoundsException.Slicing, sorting, searching
import java.util.*;
class Main {
public static void main(String[] args) {
var numbers = new ArrayList<>(List.of(3, 1, 4, 1, 5, 9));
Collections.sort(numbers);
System.out.println(numbers.subList(1, 4));
System.out.println(Collections.binarySearch(numbers, 4));
}
} numbers := []int{3, 1, 4, 1, 5, 9}
sort.Ints(numbers)
fmt.Println(numbers[1:4])
idx := sort.SearchInts(numbers, 4)
fmt.Println(idx) Go slicing syntax (
numbers[1:4]) uses exclusive end, same as Java's subList(1, 4). sort.Ints() sorts in-place (like Collections.sort()). Go's slice expression creates a view into the original — modifying it modifies the underlying array, unlike Java's subList().Iterating with range
import java.util.List;
class Main {
public static void main(String[] args) {
var colors = List.of("red", "green", "blue");
for (String color : colors) {
System.out.println(color);
}
for (int i = 0; i < colors.size(); i++) {
System.out.printf("%d: %s%n", i, colors.get(i));
}
}
} colors := []string{"red", "green", "blue"}
for _, color := range colors {
fmt.Println(color)
}
for index, color := range colors {
fmt.Printf("%d: %s\n", index, color)
} range over a slice yields both index and value — use _ to discard the index if you only need values. This replaces both Java's enhanced for loop (value only) and the indexed for loop (index + value). range works on slices, arrays, maps, strings, and channels.Maps vs HashMap
Creating and accessing maps
import java.util.HashMap;
class Main {
public static void main(String[] args) {
var scores = new HashMap<String, Integer>();
scores.put("Alice", 95);
scores.put("Bob", 87);
System.out.println(scores.get("Alice"));
System.out.println(scores.getOrDefault("Carol", 0));
System.out.println(scores.size());
}
} scores := map[string]int{}
scores["Alice"] = 95
scores["Bob"] = 87
fmt.Println(scores["Alice"])
fmt.Println(scores["Carol"]) // zero value: 0 (not an error)
fmt.Println(len(scores)) Accessing a missing key in a Go map returns the zero value for the value type — no exception, no
null. To distinguish "key present with value 0" from "key missing", use the two-value form: value, ok := scores["Carol"]. ok is false if the key was absent.Checking existence and deleting
import java.util.HashMap;
class Main {
public static void main(String[] args) {
var config = new HashMap<String, String>();
config.put("host", "localhost");
System.out.println(config.containsKey("host"));
System.out.println(config.containsKey("port"));
config.remove("host");
System.out.println(config.size());
}
} config := map[string]string{"host": "localhost"}
if value, ok := config["host"]; ok {
fmt.Println("found:", value)
}
_, ok := config["port"]
fmt.Println(ok) // false
delete(config, "host")
fmt.Println(len(config)) // 0 The two-value map lookup
value, ok := map[key] is idiomatic Go for existence checking — it replaces Java's containsKey(). delete(map, key) removes a key; deleting a non-existent key is a no-op. Maps are not safe for concurrent access — use sync.Map or a mutex for goroutine-shared maps.Iterating a map
import java.util.Map;
import java.util.TreeMap;
class Main {
public static void main(String[] args) {
var ages = new TreeMap<String, Integer>(Map.of("Alice", 30, "Bob", 25));
for (var entry : ages.entrySet()) {
System.out.printf("%s: %d%n", entry.getKey(), entry.getValue());
}
}
} ages := map[string]int{"Alice": 30, "Bob": 25}
for name, age := range ages {
fmt.Printf("%s: %d\n", name, age)
} range on a map yields key-value pairs — much cleaner than Java's entrySet() iteration. Map iteration order in Go is deliberately randomized on each run to prevent code from accidentally relying on it. Use sort.Strings(keys) to iterate in sorted key order.Control Flow
if / else if / else
class Main {
public static void main(String[] args) {
int score = 85;
if (score >= 90) {
System.out.println("A");
} else if (score >= 80) {
System.out.println("B");
} else {
System.out.println("C or below");
}
}
} score := 85
if score >= 90 {
fmt.Println("A")
} else if score >= 80 {
fmt.Println("B")
} else {
fmt.Println("C or below")
} Go's
if does not require parentheses around the condition — they are illegal by convention. Curly braces are always required. The opening brace must be on the same line as the if; Go's formatter (gofmt) enforces this.if with initialization statement
import java.util.Map;
class Main {
public static void main(String[] args) {
var config = Map.of("timeout", 30);
// Java requires a separate declaration:
Integer timeout = config.get("timeout");
if (timeout != null) {
System.out.println("timeout: " + timeout);
}
}
} config := map[string]int{"timeout": 30}
if timeout, ok := config["timeout"]; ok {
fmt.Println("timeout:", timeout)
}
// timeout is not in scope here Go's
if supports an optional initialization statement before the condition: if init; condition { }. Variables declared in the init statement are scoped to the entire if/else block. This pattern is ubiquitous for map lookups and error checks.switch (no fallthrough by default)
class Main {
public static void main(String[] args) {
String day = "Monday";
switch (day) {
case "Saturday": case "Sunday":
System.out.println("Weekend");
break; // required to prevent fallthrough
default:
System.out.println("Weekday");
}
}
} day := "Monday"
switch day {
case "Saturday", "Sunday":
fmt.Println("Weekend")
default:
fmt.Println("Weekday")
} Go's
switch does not fall through by default — no break required. Multiple values in one case are separated by commas. Use fallthrough to opt in to the Java-style behavior. Go's switch also works without an expression: switch { case x > 0: ... } acts like a cleaner if-else chain.defer (resource cleanup)
class Main {
public static void main(String[] args) {
// Java uses try-with-resources for cleanup:
System.out.println("opening resource");
try {
System.out.println("using resource");
} finally {
System.out.println("closing resource");
}
}
} fmt.Println("opening resource")
defer fmt.Println("closing resource") // runs when the function returns
fmt.Println("using resource") defer schedules a function call to run when the surrounding function returns — after any return statement, including panics. It is the idiomatic way to close files, release mutexes, and clean up resources. Multiple deferred calls execute in LIFO order. Java's try-with-resources is similar but tied to the block, not the function.Loops & Iteration
for loop (the only loop in Go)
class Main {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
System.out.print(i + " ");
}
System.out.println();
}
} for i := 0; i < 5; i++ {
fmt.Print(i, " ")
}
fmt.Println() Go has exactly one loop keyword:
for. It handles the Java for, while, and do-while cases through different forms. The classic three-part form is identical to Java's for — just without parentheses.while and do-while equivalents
class Main {
public static void main(String[] args) {
int count = 0;
while (count < 3) {
System.out.println(count);
count++;
}
}
} count := 0
for count < 3 { // Go's "while" — just omit init and post
fmt.Println(count)
count++
} A
for with only a condition is Go's while. A bare for { } (no condition) is an infinite loop — equivalent to while (true). Go has no do-while; use for { ... if cond { break } } instead.for-range over collections
import java.util.List;
class Main {
public static void main(String[] args) {
var fruits = List.of("apple", "banana", "cherry");
// Enhanced for (value only):
for (String fruit : fruits) System.out.println(fruit);
// Streams with index (Java 9+):
var stream = fruits.stream().toList();
for (int i = 0; i < stream.size(); i++) {
System.out.printf("%d: %s%n", i, stream.get(i));
}
}
} fruits := []string{"apple", "banana", "cherry"}
for index, fruit := range fruits {
fmt.Printf("%d: %s\n", index, fruit)
} range replaces both Java's enhanced for loop and index-based loops in one construct. It works uniformly on slices, arrays, maps, strings, and channels. Use _ to ignore either the index or the value: for _, fruit := range fruits.break, continue, and labeled loops
class Main {
public static void main(String[] args) {
outer:
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (j == 1) continue;
if (i == 2) break outer;
System.out.printf("(%d,%d) ", i, j);
}
}
System.out.println();
}
} outer:
for i := range 3 {
for j := range 3 {
if j == 1 { continue }
if i == 2 { break outer }
fmt.Printf("(%d,%d) ", i, j)
}
}
fmt.Println() Go supports labeled
break and continue for breaking out of nested loops — the same as Java's labeled statements. Labels are placed before the outer loop and referenced in the break/continue statement.Functions
Function definition
class Main {
static String greet(String name) {
return "Hello, " + name + "!";
}
public static void main(String[] args) {
System.out.println(greet("Alice"));
}
} func greet(name string) string {
return "Hello, " + name + "!"
}
fmt.Println(greet("Alice")) Go functions are declared with
func, parameters listed as name type, and the return type after the parameter list. No static keyword — all package-level functions are analogous to Java statics. Capitalized names are exported (public); lowercase names are package-private.Multiple return values
class Main {
// Java: return a pair via a record or array
record MinMax(int min, int max) {}
static MinMax minMax(int[] numbers) {
int min = numbers[0], max = numbers[0];
for (int n : numbers) {
if (n < min) min = n;
if (n > max) max = n;
}
return new MinMax(min, max);
}
public static void main(String[] args) {
var result = minMax(new int[]{3, 1, 4, 1, 5});
System.out.printf("min=%d max=%d%n", result.min(), result.max());
}
} func minMax(numbers []int) (int, int) {
min, max := numbers[0], numbers[0]
for _, n := range numbers[1:] {
if n < min { min = n }
if n > max { max = n }
}
return min, max
}
minimum, maximum := minMax([]int{3, 1, 4, 1, 5})
fmt.Printf("min=%d max=%d\n", minimum, maximum) Go functions can return multiple values natively — no wrapper record, pair, or array needed. This is the primary mechanism for returning both a result and an error. Java requires a
record or similar container; Go just lists the return types in parentheses.Variadic functions
class Main {
static int sum(int... numbers) {
int total = 0;
for (int n : numbers) total += n;
return total;
}
public static void main(String[] args) {
System.out.println(sum(1, 2, 3));
int[] values = {10, 20, 30};
System.out.println(sum(values)); // spread not needed
}
} func sum(numbers ...int) int {
total := 0
for _, n := range numbers { total += n }
return total
}
fmt.Println(sum(1, 2, 3))
values := []int{10, 20, 30}
fmt.Println(sum(values...)) // spread with ... Go variadic functions use
...Type syntax, the same as Java's Type... (just reversed). To spread a slice into a variadic call, use slice... — unlike Java, where an array is passed directly without spreading.First-class functions
import java.util.function.Function;
class Main {
static int apply(Function<Integer, Integer> func, int value) {
return func.apply(value);
}
public static void main(String[] args) {
Function<Integer, Integer> doubler = n -> n * 2;
System.out.println(apply(doubler, 5));
System.out.println(apply(n -> n * n, 4));
}
} func apply(operation func(int) int, value int) int {
return operation(value)
}
doubler := func(n int) int { return n * 2 }
fmt.Println(apply(doubler, 5))
fmt.Println(apply(func(n int) int { return n * n }, 4)) Go functions are first-class values — they can be assigned to variables and passed as arguments. The type of a function value is written
func(ParamTypes) ReturnType. This replaces Java's Function<T, R>, Predicate<T>, and other functional interfaces.Structs & Methods
Structs (no class keyword)
class Main {
record Point(double x, double y) {} // Java record (immutable)
public static void main(String[] args) {
var origin = new Point(0.0, 0.0);
System.out.println(origin.x() + " " + origin.y());
}
} type Point struct {
X, Y float64
}
origin := Point{X: 0.0, Y: 0.0}
fmt.Println(origin.X, origin.Y) Go
struct is the primary data type for grouping values — there are no classes. Go structs can be initialized with field names (Point{X: 0.0, Y: 0.0}) or positionally (Point{0.0, 0.0}). Field names starting with uppercase are exported; lowercase fields are package-private.Methods on structs
class Main {
static class Circle {
double radius;
Circle(double radius) { this.radius = radius; }
double area() { return Math.PI * radius * radius; }
String describe() { return "Circle(r=" + radius + ")"; }
}
public static void main(String[] args) {
var circle = new Circle(5.0);
System.out.println(circle.area());
System.out.println(circle.describe());
}
} type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Describe() string {
return fmt.Sprintf("Circle(r=%.1f)", c.Radius)
}
circle := Circle{Radius: 5.0}
fmt.Println(circle.Area())
fmt.Println(circle.Describe()) Methods in Go are defined separately from the struct, with a receiver parameter:
func (c Circle) Area(). The receiver plays the role of Java's this. Methods can be defined on any named type in the same package, not just structs.Pointer receivers (mutating methods)
class Main {
static class Counter {
private int count = 0;
void increment() { count++; }
int value() { return count; }
}
public static void main(String[] args) {
var counter = new Counter();
counter.increment();
counter.increment();
System.out.println(counter.value());
}
} type Counter struct {
count int
}
func (c *Counter) Increment() { c.count++ }
func (c Counter) Value() int { return c.count }
counter := &Counter{}
counter.Increment()
counter.Increment()
fmt.Println(counter.Value()) A pointer receiver (
*Counter) allows the method to modify the struct — equivalent to Java's mutable instance methods. A value receiver (Counter) receives a copy and cannot modify the original. Use pointer receivers consistently for all methods on a type if any method needs to mutate.Interfaces
Implicit interface satisfaction
class Main {
interface Greeter {
String greet(String name); // must declare "implements Greeter"
}
static class FormalGreeter implements Greeter {
public String greet(String name) { return "Good day, " + name; }
}
public static void main(String[] args) {
Greeter greeter = new FormalGreeter();
System.out.println(greeter.greet("Alice"));
}
} type Greeter interface {
Greet(name string) string
}
type FormalGreeter struct{}
func (g FormalGreeter) Greet(name string) string {
return "Good day, " + name
}
var greeter Greeter = FormalGreeter{}
fmt.Println(greeter.Greet("Alice")) Go interfaces are satisfied implicitly — a type implements an interface simply by having the required methods, with no
implements declaration. This is structural (duck) typing enforced at compile time. Any existing type from any package can satisfy your interface, without modifying it.Stringer (toString equivalent)
class Main {
static class Temperature {
double celsius;
Temperature(double celsius) { this.celsius = celsius; }
@Override
public String toString() {
return String.format("%.1f°C", celsius);
}
}
public static void main(String[] args) {
var temp = new Temperature(22.5);
System.out.println(temp);
}
} type Temperature struct {
Celsius float64
}
func (t Temperature) String() string {
return fmt.Sprintf("%.1f°C", t.Celsius)
}
temp := Temperature{Celsius: 22.5}
fmt.Println(temp) // calls String() automatically The
fmt.Stringer interface has one method: String() string. Any type implementing it controls how it prints via fmt.Println(), %v, and %s — exactly like Java's toString(). You satisfy it just by defining the method.any (empty interface)
class Main {
static void printAnything(Object value) {
System.out.println(value);
}
public static void main(String[] args) {
printAnything(42);
printAnything("hello");
printAnything(3.14);
}
} func printAnything(value any) {
fmt.Println(value)
}
printAnything(42)
printAnything("hello")
printAnything(3.14) any (an alias for interface{}) is Go's equivalent of Java's Object — it accepts any type. Use type assertions (value.(string)) or type switches (switch v := value.(type)) to recover the concrete type, similar to Java's casts and instanceof.Error Handling
Errors as return values
class Main {
static double divide(double x, double y) throws ArithmeticException {
if (y == 0) throw new ArithmeticException("division by zero");
return x / y;
}
public static void main(String[] args) {
try {
System.out.println(divide(10, 2));
System.out.println(divide(10, 0));
} catch (ArithmeticException e) {
System.out.println("Error: " + e.getMessage());
}
}
} func divide(x, y float64) (float64, error) {
if y == 0 {
return 0, errors.New("division by zero")
}
return x / y, nil
}
if result, err := divide(10, 2); err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println(result)
}
if result, err := divide(10, 0); err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println(result)
} Go returns errors as the last return value — there are no checked exceptions, no unchecked exceptions, no
throws clause. Callers must explicitly handle or ignore errors. The convention is if err != nil { handle } immediately after every call that can fail.Wrapping and unwrapping errors
class Main {
static class DatabaseException extends RuntimeException {
DatabaseException(String msg, Throwable cause) { super(msg, cause); }
}
public static void main(String[] args) {
var cause = new RuntimeException("connection refused");
var wrapped = new DatabaseException("query failed", cause);
System.out.println(wrapped.getMessage());
System.out.println(wrapped.getCause().getMessage());
}
} cause := errors.New("connection refused")
wrapped := fmt.Errorf("query failed: %w", cause)
fmt.Println(wrapped)
fmt.Println(errors.Is(wrapped, cause)) // true
fmt.Println(errors.Unwrap(wrapped) == cause) // true fmt.Errorf("...: %w", err) wraps an error with context, like Java's exception chaining (new Exception(msg, cause)). errors.Is() checks if any error in the chain matches a target (like Java's getCause() traversal). errors.As() extracts a specific error type from the chain.panic and recover
class Main {
static void validate(int value) {
if (value < 0) {
throw new IllegalArgumentException("negative value: " + value);
}
}
public static void main(String[] args) {
try {
validate(-1);
} catch (IllegalArgumentException e) {
System.out.println("caught: " + e.getMessage());
}
}
} func validate(value int) {
if value < 0 {
panic(fmt.Sprintf("negative value: %d", value))
}
}
func safeValidate(value int) (err error) {
defer func() {
if recovered := recover(); recovered != nil {
err = fmt.Errorf("recovered: %v", recovered)
}
}()
validate(value)
return nil
}
fmt.Println(safeValidate(-1)) panic is for programming errors and unrecoverable states — like Java's RuntimeException, not checked exceptions. recover() inside a defer catches a panic and converts it to an error, analogous to catching an unchecked exception. For expected failure conditions, always use the (value, error) return pattern instead.Goroutines & Channels
Goroutines vs threads
class Main {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> System.out.println("running in thread"));
thread.start();
thread.join();
System.out.println("done");
}
} var waitGroup sync.WaitGroup
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
fmt.Println("running in goroutine")
}()
waitGroup.Wait()
fmt.Println("done") A goroutine is launched with the
go keyword before any function call. Goroutines are multiplexed onto OS threads by the Go scheduler — you can run millions simultaneously with ~2 KB initial stack each. Java threads are 1:1 OS threads (~1 MB stack). sync.WaitGroup replaces Thread.join().Channels for communication
import java.util.concurrent.*;
class Main {
public static void main(String[] args) throws Exception {
var queue = new LinkedBlockingQueue<Integer>();
new Thread(() -> {
for (int i = 0; i < 3; i++) queue.offer(i * i);
}).start();
for (int i = 0; i < 3; i++) {
System.out.println(queue.take());
}
}
} channel := make(chan int, 3)
go func() {
for i := range 3 {
channel <- i * i
}
close(channel)
}()
for value := range channel {
fmt.Println(value)
} Channels are typed conduits for communicating between goroutines. The Go philosophy: "Don't communicate by sharing memory; share memory by communicating." This replaces Java's
BlockingQueue, ConcurrentLinkedQueue, and other concurrent data structures with a language-level primitive.Packages & Modules
Visibility: exported vs unexported
package com.example.mylib;
public class MyClass { // public — accessible everywhere
public int exported; // public field
protected int subtypes; // protected
private int hidden; // private
int packagePrivate; // package-private (no modifier)
} // package mylib
type MyStruct struct {
Exported int // uppercase first letter → exported (public)
packageOnly int // lowercase → unexported (package-private)
}
// There is no protected, no private within a struct:
// everything in the package can access unexported fields. Go has exactly two visibility levels: exported (starts with uppercase) and unexported (starts with lowercase). There is no
protected, no private within a struct, and no package hierarchy — a package is either all-accessible or not. This applies to types, functions, constants, variables, and struct fields.Imports and go.mod
// pom.xml / build.gradle:
// <dependency>
// <groupId>com.google.code.gson</groupId>
// <artifactId>gson</artifactId>
// <version>2.11.0</version>
// </dependency>
import com.google.code.gson.Gson; // then use Gson // go.mod:
// module myproject
// go 1.26
// require github.com/some/library v1.2.3
// Then: go get github.com/some/library
// import "github.com/some/library"
// (module path is the import path — no group IDs, no artifact IDs) Go modules use the full repository URL as the import path (
github.com/user/repo). Dependencies are declared in go.mod and pinned in go.sum. The toolchain (go get, go build, go test) is built in — no Maven, Gradle, or separate build tool needed.