chad

Go for Java Developers: A Practical Guide

This guide assumes you're experienced with Java/Spring Boot and want to learn Go (Golang) quickly by comparison. This is made for me to learn golang.


1. Core Philosophy Differences

Concept Java/Spring Go
Philosophy OOP-heavy, interfaces as contracts, DI frameworks Composition over inheritance, "simple works"
Dependency Injection @Autowired, Spring beans Explicit passing, closure injection
Exceptions Checked/unchecked exceptions (throw/catch) Error values (if err != nil)
Generics Full generics since Java 8 Introduced in Go 1.18 (similar but simpler)
Annotations @RestController, @GetMapping Standard library http.HandleFunc()
Boilerplate Lots of XML/config or annotations Minimal — explicit is better
Error handling Try-catch, exception hierarchy Explicit checks, no stack traces

Go's motto: "The best code is the code never written." No abstractions unless necessary.


2. Basic Structure Comparison

Java vs Go: Hello World

Java:

// Requires class wrapper
public class Main {
    public static void main(String[] args) {
        System.out.println("Hello");
    }
}

Go:

package main

import "fmt"

func main() {
    fmt.Println("Hello")
}

Project Layout

Java (Maven/Spring):

src/
  main/java/com/example/app/
  main/resources/
pom.xml
application.properties

Go:

cmd/
  app/
    main.go
internal/
  handler/
  store/
pkg/
  config/
go.mod
go.sum

Go doesn't need Maven/Gradle — just go.mod.


3. Variables and Types

Variable Declaration

Type Java Go
Explicit String name = "John"; var name string = "John"
Short (local only) N/A name := "John"
Inferred type var x = 42; x := 42
var count int = 0
count := 42 // short form, local scope
var message string = "" // zero value if omitted

Key difference: Go has no default values except zero-value for types (0 for numbers, "" for strings, false for bool).

Named Types (like Java enums/classes)

// Type alias
type UserID int

// New type (different from int, even though same underlying type)
type User struct {
    ID   UserID
    Name string
    Age  int
}

// Struct initialization (Java-like constructor)
u := User{ID: 123, Name: "Alice", Age: 30}

Pointers (Explicit, not implicit like Java)

var i int = 10
p := &i        // pointer to i
*p = 20        // dereference

func increment(x *int) {
    *x++
}

Gotcha: Unlike Java, Go references are explicit with & and *.


4. Functions

Function Syntax

// Simple function
func greet(name string) string {
    return "Hello, " + name
}

// Multiple return values (common pattern!)
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

// Unnamed return values
func process(data []byte) (result string, err error) {
    // ... logic
    result = string(data)
    return // auto-returns result, err
}

Variadic Functions (like Java varargs)

func sum(numbers ...int) int {
    total := 0
    for _, n := range numbers {
        total += n
    }
    return total
}

sum(1, 2, 3)     // → 6
nums := []int{1,2,3}
sum(nums...)     // spread operator

5. Control Structures

Conditionals (Same as Java, minus else-if)

if x > 10 {
    fmt.Println("big")
} else if x < 5 {
    fmt.Println("small")
} else {
    fmt.Println("medium")
}

// Can declare variable before condition
if err := readFile(); err != nil {
    return err
}

Loops

// Only one loop type: for
for i := 0; i < 10; i++ {
    fmt.Println(i)
}

// While-style
i := 0
for i < 10 {
    fmt.Println(i)
    i++
}

// Infinite loop
for {
    select { /* channels */ }
}

// Range over slices/maps/channels (no foreach needed)
slice := []int{1, 2, 3}
for idx, val := range slice {
    fmt.Println(idx, val) // idx = 0, 1, 2
}

// Ignore index/value
for _, v := range slice {
    fmt.Println(v)
}

6. Data Structures

Arrays (Fixed size)

var arr [5]int          // [0, 0, 0, 0, 0]
arr := [3]int{1, 2, 3}
arr[0] = 99

Slices (Like ArrayList)

// Slice creation
slice := []int{1, 2, 3}
slice := make([]int, 5, 10) // len=5, cap=10

// Append (auto-grows, like ArrayList.add())
slice = append(slice, 4)

// Slicing (subset, copies data)
sub := slice[1:3] // elements at indices 1 and 2

// Length and capacity
len(slice)  // visible elements
cap(slice)  // underlying array size

Gotcha: make([]int, 5) creates 5 zeros; [5]int{} also creates 5 zeros. But make([]int, 5, 10) sets capacity beyond length.

Maps (Like HashMap)

// Create map
m := make(map[string]int)

// Add/modify
m["key"] = 42

// Check existence (two-value idiom)
val, ok := m["key"]
if !ok {
    // key not found
}

// Delete
delete(m, "key")

// Range (iteration)
for k, v := range m {
    fmt.Println(k, v)
}

Gotcha: Maps are reference types. Passing to functions passes the map header (not copied), so modifications persist.


7. Methods and Interfaces

Methods (Receiver syntax)

type User struct {
    Name string
    Age  int
}

// Value receiver (copy)
func (u User) Greet() string {
    return "Hello, " + u.Name
}

// Pointer receiver (modifies original)
func (u *User) Birthday() {
    u.Age++
}

// Call like Java method
user := User{Name: "Alice"}
user.Greet()    // copy
user.Birthday() // modifies user

Key difference: Go has no classes — structs have methods via receivers.

Interfaces (Implicit satisfaction)

// Interface definition
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Any type implementing those methods satisfies interface
var r Reader = os.File{}  // ✅ Automatically satisfied

// Empty interface (any type) - like Object in Java
var anyValue interface{} = 42
anyValue = "string"
anyValue = true

Gotcha: You don't declare implements. Just implement the methods.


8. Error Handling

The Big Difference

Java Go
Exceptions (try/catch/throw) Return error value
Checked exceptions force handling Caller decides whether to check
Stack traces on error Manual error messages

Pattern: Always check after function call

file, err := os.Open("data.txt")
if err != nil {
    return err // propagate up
}
defer file.Close()

// Or early return pattern
func readConfig(path string) (*Config, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, fmt.Errorf("open config: %w", err)
    }
    defer f.Close()
    
    // ... more processing
    return cfg, nil
}

Creating Errors

// Built-in
err := errors.New("something went wrong")

// With context
err := fmt.Errorf("reading file: %w", os.ErrPermission)

// Custom error types
type NotFoundError struct {
    Resource string
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s not found", e.Resource)
}

Checking Errors

if err != nil {
    log.Fatal(err)      // stop program
    return              // return to caller
}

// Type assertion for specific errors
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
    // handle path-specific logic
}

// Standard errors
errors.Is(err, io.EOF)       // compare against known errors
errors.Is(err, os.ErrNotExist)

Gotcha: No try-catch. Every operation that can fail must be checked manually.


9. Concurrency

Goroutines (Lightweight threads)

// Start goroutine
go func() {
    fmt.Println("Running in background")
}()

// Regular function as goroutine
go doWork(id)

Channels (Communication, not shared memory)

// Create channel
ch := make(chan int)         // unbuffered
chBuf := make(chan int, 10)  // buffered (capacity 10)

// Send/receive
ch <- 42                    // send
val := <-ch                 // receive

// Directional channels
func producer(ch chan<- int) { ch <- 42 }
func consumer(ch <-chan int) { val := <-ch }

// Close channel (signal completion)
close(ch)

// Range over closed channel
for val := range ch {
    fmt.Println(val)
}

Select Statement (Multiple channels)

select {
case msg := <-ch1:
    fmt.Println("from ch1:", msg)
case ch2 <- data:
    fmt.Println("sent to ch2")
default:
    fmt.Println("non-blocking")
}

// With timeout
select {
case result := <-work():
    fmt.Println(result)
case <-time.After(1 * time.Second):
    fmt.Println("timeout!")
}

WaitGroups (Thread joining)

var wg sync.WaitGroup

for i := 0; i < 5; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        doWork(id)
    }(i)
}

wg.Wait() // wait for all goroutines

Gotcha: Go uses "don't communicate by sharing memory; share memory by communicating." Use channels instead of locks when possible.


10. HTTP Server (No Spring MVC)

// Handler signature
type HandlerFunc func(w http.ResponseWriter, r *http.Request)

// Register route
mux := http.NewServeMux()
mux.HandleFunc("GET /users", listUsers)
mux.HandleFunc("POST /users", createUser)

// Start server
server := &http.Server{
    Addr:         ":8080",
    Handler:      mux,
    ReadTimeout:  15 * time.Second,
    WriteTimeout: 15 * time.Second,
}
log.Fatal(server.ListenAndServe())

Reading Request Body (vs Jackson ObjectMapper)

// JSON decode
type CreateUserReq struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

var req CreateUserReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
    http.Error(w, "invalid JSON", http.StatusBadRequest)
    return
}

Writing Response (vs ResponseEntity)

// JSON response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(User{ID: 1, Name: "Alice"})

// Error response
http.Error(w, "not found", http.StatusNotFound)

11. Context (Cancellation & Timeouts)

// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// Pass to database/http calls
row := db.QueryRowContext(ctx, "SELECT ...")

// Check for cancellation
select {
case <-ctx.Done():
    return ctx.Err()  // returns DeadlineExceeded or Canceled
default:
}

// Store request-scoped values (headers, IDs, etc.)
ctx = context.WithValue(ctx, "userID", 123)
userID := ctx.Value("userID") // weak typing!

Gotcha: Don't use context.Value for critical data (weak typing). Prefer passing explicit parameters.


12. Common Gotchas

Nil vs Zero Values

var s *User        // nil pointer
var m map[string]int  // nil map
var c chan int     // nil channel

// PANIC if you write to nil maps/channels!
m["key"] = 42 // ❌ panic
c <- 42       // ❌ panic (blocks forever for buffered)

// Solution: initialize maps and channels
m = make(map[string]int)
c = make(chan int)

Defer Execution Order

func test() {
    defer fmt.Println("second")
    defer fmt.Println("first")
    fmt.Println("middle")
}
// Output: middle, first, second (LIFO order)

Always defer cleanup near resource acquisition:

f, err := os.Open("file")
if err != nil { return err }
defer f.Close()  // ✅ guaranteed to run

Value vs Pointer Receivers

type Counter struct { value int }

func (c Counter) Increment() Counter {
    c.value++
    return c  // returns modified COPY
}

func (c *Counter) IncrementPtr() {
    c.value++  // modifies ORIGINAL
}

Rule: Use pointer receivers for large structs and when modifying state.

Panic and Recover (rarely needed)

func safeOperation() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recovered from:", r)
        }
    }()
    panic("something terrible")
}

13. Testing

// _test suffix
// filename: user_test.go

package main

import "testing"

func TestGreet(t *testing.T) {
    result := greet("Alice")
    expected := "Hello, Alice"
    if result != expected {
        t.Errorf("expected %q, got %q", expected, result)
    }
}

// Table-driven tests
func TestDivide(t *testing.T) {
    tests := []struct {
        name     string
        a, b     float64
        wantErr  bool
    }{
        {"valid", 10, 2, false},
        {"zero divisor", 10, 0, true},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result, err := divide(tt.a, tt.b)
            if (err != nil) != tt.wantErr {
                t.Fatalf("error = %v, wantErr %v", err, tt.wantErr)
            }
            if err == nil && result != tt.result {
                t.Errorf("result = %v, want %v", result, tt.result)
            }
        })
    }
}

Run tests:

go test ./...
go test -v ./...  # verbose
go test -cover    # coverage report

14. Build and Run

# Initialize module
go mod init github.com/yourname/app

# Download dependencies
go mod download
go get ./...

# Run
go run main.go

# Build binary
go build -o myapp cmd/app/main.go

# Test
go test ./...

# Format code
go fmt ./...

# Lint
golangci-lint run  # external tool

15. Tutorials and Resources

Official Docs

For Java Developers

Video Tutorials

Books


Quick Reference Card

Task Java Go
Print System.out.println() fmt.Println()
Get input Scanner sc = new Scanner(System.in) io.ReadAll(os.Stdin)
String concat "Hello " + name "Hello " + name or %s format
If-else Standard Same
Loop for(int i=0;...) for i:=0; i<10; i++
Array int[] arr = new int[5] arr := make([]int, 5)
Map Map<String, Integer> map[string]int
Class class MyClass {} type MyClass struct {}
Method void method() {} func method() {}
Exception try { } catch(Exception e) if err != nil { }
Thread new Thread(() -> {}).start() go func() {}()
HTTP Server @RestController http.HandleFunc()
Config files application.yml ENV vars, JSON, TOML

Final Advice

  1. Read more code than you write. Understand idioms before reinventing.
  2. Embrace if err != nil. It's not annoying — it's explicit control flow.
  3. Use panic sparingly. Only for truly unrecoverable situations.
  4. Prefer composition over inheritance. Small functions + composition beats deep hierarchies.
  5. Test often. go test is fast — run it constantly.

Happy Go-ing! 🚀

Cheers

Guide is made by ai.