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
- https://go.dev/tour/welcome/1 — Interactive tour (start here!)
- https://go.dev/doc/tutorial/create-module — Module tutorial
- https://go.dev/blog/ — Language updates and tutorials
For Java Developers
- https://go.dev/doc/effective_go — Go idioms
- https://github.com/avelino/awesome-go — Library recommendations
Video Tutorials
- "Go: The Complete Developer Course" (Udemy)
- "Learn Go Programming" (YouTube: Programming with Mosh)
Books
- "The Go Programming Language" by Donovan & Kernighan
- "Go in Action" by Kenneth Blankenship
Quick Reference Card
| Task | Java | Go |
|---|---|---|
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
- Read more code than you write. Understand idioms before reinventing.
- Embrace
if err != nil. It's not annoying — it's explicit control flow. - Use
panicsparingly. Only for truly unrecoverable situations. - Prefer composition over inheritance. Small functions + composition beats deep hierarchies.
- Test often.
go testis fast — run it constantly.
Happy Go-ing! 🚀
Cheers
Guide is made by ai.