Go for Java Developers: What I Wish I Knew

When I started learning Go after years of Java, I expected a quick transition. Both are statically typed, garbage collected, and designed for building services. How different could they be?

Very different, it turns out. Not in syntax—that's easy to pick up—but in philosophy. Go and Java solve problems differently, and fighting that difference makes for frustrating code. This is what I wish someone had told me.

Forget Everything About OOP

This was my biggest stumbling block. Coming from Java, I instinctively reached for classes, inheritance, and design patterns. Go doesn't do OOP the way Java does.

There are no classes

Go has structs with methods, which feels similar but isn't. You can't have constructors (use factory functions), there's no this (methods have explicit receivers), and there's no inheritance.

// Java thinking
public class User {
    private String name;

    public User(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}

// Go thinking
type User struct {
    Name string  // Exported if uppercase
}

func NewUser(name string) *User {
    return &User{Name: name}
}

func (u *User) GetName() string {
    return u.Name
}

Composition over inheritance

Go doesn't have inheritance. At all. You can embed structs, which is not inheritance—it's composition that happens to promote methods.

type Animal struct {
    Name string
}

func (a *Animal) Speak() string {
    return "..."
}

type Dog struct {
    Animal  // Embedding, not inheritance
    Breed string
}

// Dog now has Animal's fields and methods
dog := Dog{Animal: Animal{Name: "Rex"}, Breed: "Shepherd"}
dog.Name    // Promoted from Animal
dog.Speak() // Promoted from Animal

The mental shift: design with interfaces and composition. If you're trying to create class hierarchies, you're thinking in the wrong paradigm.

Interfaces Work Differently

Go interfaces are implicit. You don't declare that a type implements an interface—if it has the right methods, it implements the interface. This is both liberating and confusing.

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

// This type implements Reader without declaring it
type FileReader struct {
    file *os.File
}

func (f *FileReader) Read(p []byte) (int, error) {
    return f.file.Read(p)
}

Key insight: interfaces belong to the consumer, not the producer. In Java, the package that defines a type also defines its interfaces. In Go, the package that uses a type often defines the interface it needs.

Accept interfaces, return structs. This Go proverb was confusing until I understood that interfaces should be small (often one method) and defined where they're needed, not in advance.

Error Handling

Go's error handling is the most contentious topic for Java developers. No exceptions. No try-catch. Just return values.

// Java
try {
    User user = repository.findById(id);
    return user;
} catch (NotFoundException e) {
    return null;
}

// Go
user, err := repository.FindById(id)
if err != nil {
    return nil, err
}
return user, nil

Yes, you write if err != nil constantly. Yes, it's verbose. But it makes error handling explicit and local. You always know exactly where errors can occur and how they're handled.

Wrap errors for context

Go 1.13 added error wrapping. Use it. Bare errors lose context as they propagate.

user, err := repository.FindById(id)
if err != nil {
    return nil, fmt.Errorf("loading user %s: %w", id, err)
}

// Later, you can unwrap to check the original error
if errors.Is(err, sql.ErrNoRows) {
    // Handle not found
}

Concurrency Is First-Class

This is where Go shines. Goroutines and channels make concurrency approachable in ways that Java's threading model doesn't.

// Java (simplified)
ExecutorService executor = Executors.newFixedThreadPool(10);
Future future = executor.submit(() -> doWork());
Result result = future.get();

// Go
result := make(chan Result)
go func() {
    result <- doWork()
}()
r := <-result

Goroutines are cheap—you can spawn thousands without thinking. Channels provide safe communication between them. The select statement lets you wait on multiple channels.

select {
case result := <-results:
    handleResult(result)
case err := <-errors:
    handleError(err)
case <-ctx.Done():
    return ctx.Err()
case <-time.After(5 * time.Second):
    return ErrTimeout
}

The Standard Library Is Enough

In Java, you reach for Spring, Hibernate, Guava. In Go, the standard library covers most needs. HTTP servers, JSON handling, templating, testing—all built in.

// A complete HTTP server
func main() {
    http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
        json.NewEncoder(w).Encode(map[string]string{
            "message": "Hello, World!",
        })
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}

This doesn't mean Go has no ecosystem—there are excellent libraries for routing, databases, and more. But the pressure to add dependencies is lower. Fewer dependencies means faster builds and less maintenance burden.

No Generics (Until Recently)

Go added generics in 1.18, but the ecosystem still reflects years without them. You'll see interface{} (now spelled any) in older code, which is the empty interface that accepts anything—Go's equivalent of Object.

// Old style (pre-generics)
func Map(slice []interface{}, f func(interface{}) interface{}) []interface{}

// New style (with generics)
func Map[T, U any](slice []T, f func(T) U) []U

Use generics when they make code clearer. Don't over-genericize—Go still values simplicity over flexibility.

Practical Gotchas

nil interfaces are tricky

An interface value is nil only if both the type and value are nil. This causes subtle bugs:

var ptr *MyStruct = nil
var iface interface{} = ptr
fmt.Println(iface == nil) // false! The type is *MyStruct

Slices are references

Slices contain a pointer to an underlying array. Appending might or might not create a new array. If you need to pass a slice without it being modified, copy it.

original := []int{1, 2, 3}
copy := make([]int, len(original))
copy(copy, original)

Range copies values

When you range over a slice, you get copies of the values, not references:

users := []User{{Name: "Alice"}, {Name: "Bob"}}
for _, u := range users {
    u.Name = "Changed"  // Modifies the copy, not the original
}
// users still has Alice and Bob

// To modify in place, use the index
for i := range users {
    users[i].Name = "Changed"
}

What I Miss From Java

It's not all roses. Things I still miss:

  • Enums with methods — Go's constant iota is limited compared to Java enums
  • Annotation processing — No code generation at compile time (though go:generate helps)
  • IDE refactoring — Still catching up to IntelliJ's capabilities
  • Mature testing libraries — JUnit and Mockito are more powerful than Go's testing package

What I Prefer About Go

  • Fast compilation — Seconds, not minutes
  • Single binary deployment — No runtime dependencies
  • Built-in formatting — gofmt ends all style debates
  • Explicit error handling — No hidden exceptions
  • Simple concurrency — Goroutines and channels are elegant
  • Smaller codebases — Less ceremony means less code

The Transition Path

If you're making the switch, here's what helped me:

  1. Read "Effective Go" — Still the best introduction to idiomatic Go
  2. Build something real — A CLI tool or small HTTP service
  3. Read standard library code — It's well-written and educational
  4. Accept the simplicity — Stop looking for the "right" abstraction. Write simple code
  5. Embrace the verbosity — Explicit is better than magic

The biggest shift is philosophical. Java encourages abstraction, patterns, and flexibility. Go encourages simplicity, explicitness, and practicality. Neither is wrong—they're different tools for different contexts.

I now use both regularly. Java for complex business logic where its type system shines. Go for services, tools, and anything that needs to be fast, simple, and reliable.


For concrete examples, check out my gtfs-transformer project—a real-time transit data processor written in Go.