Resource cleanup, rate-limiting strategies, bounded vs unbounded channels - in Go vs Java | Patterns, idioms, and best practices for Go

We continue the series of articles for developers who want to learn Go based on knowledge of Java, and vice versa. In this article, we will discuss three key topics: Resource Cleanup (resource release), Rate-Limiting Strategies (load limiting strategies) and Bounded vs Unbounded Channels (bounded and unbounded channels). Each topic is examined from the perspective of Go and Java, with code examples, diagrams, and practical advice.

1. Resource Cleanup — Resource Cleanup

Proper resource management is critical for application stability. Java uses try-with-resources, Go uses defer.

Go: defer

// Открываем файл и гарантируем его закрытие
package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Open("example.txt")
    if err != nil {
        fmt.Println("Ошибка при открытии файла:", err)
        return
    }
    defer file.Close() // закроется в конце функции

    // Работа с файлом
}
  

Java: try-with-resources

import java.io.*;

public class ResourceExample {
    public static void main(String[] args) {
        try (BufferedReader br = new BufferedReader(new FileReader("example.txt"))) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  

In Go, defer is conveniently used not only for files but also for closing connections, unlocking mutexes, and cleaning up any resources. This reduces the risk of leaks.

Using defer/try-with-resources is critical for working with databases, file storage, and network connections. In Go, you can open hundreds of connections at once, knowing that each will be closed automatically.

  • Pros: safe resource management, fewer errors.
  • Cons: a small overhead in defer in high-load loops (Go).

2. Rate-Limiting Strategies — Rate-Limiting Strategies

Rate-limiting allows controlling the frequency of requests, preventing overloads, and protecting services.

Go: Token Bucket

package main

import (
    "fmt"
    "time"
)

func main() {
    ticker := time.NewTicker(500 * time.Millisecond) // interval "permission"
    defer ticker.Stop()

    for i := 0; i < 5; i++ {
        <-ticker.C
        fmt.Println("Request sent", i)
    }
}
  

Java: ScheduledExecutorService

import java.util.concurrent.*;

public class RateLimitExample {
    public static void main(String[] args) throws InterruptedException {
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

        Runnable task = () -> System.out.println("Request sent " + System.currentTimeMillis());

        // Run task every 500 ms
        scheduler.scheduleAtFixedRate(task, 0, 500, TimeUnit.MILLISECONDS);

        Thread.sleep(3000);
        scheduler.shutdown();
    }
}
  
Tip: Use rate-limiting for APIs, task queues, and external services. Go makes it very easy to integrate this into channels and worker pools.
Practical Application

Rate-limiting is used for API gateways, microservices, request limits to external services.

  • Pros: prevents overload, protects services.
  • Cons: need to consider peak loads, possible delays.

3. Bounded vs Unbounded Channels — Bounded and Unbounded Channels

Channels in Go are the primary tool for communication between goroutines. They can be bounded (bounded) and unbounded (unbounded). In Java, the equivalent is BlockingQueue / LinkedBlockingQueue.

Go: Bounded Channel

package main

import "fmt"

func main() {
    ch := make(chan int, 2) // bounded buffer with 2 elements

    ch <- 1
    ch <- 2
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}
  

Java: ArrayBlockingQueue

import java.util.concurrent.*;

public class BoundedQueueExample {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(2);

        queue.put(1);
        queue.put(2);
        System.out.println(queue.take());
        System.out.println(queue.take());
    }
}
  

Go: Unbounded Channel (through goroutines and buffer)

package main

import "fmt"

func main() {
    ch := make(chan int)

    go func() {
        for i := 0; i < 5; i++ {
            ch <- i
        }
        close(ch)
    }()

    for val := range ch {
        fmt.Println(val)
    }
}
  

Java: LinkedBlockingQueue (unbounded)

import java.util.concurrent.*;

public class UnboundedQueueExample {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();

        for (int i = 0; i < 5; i++) {
            queue.put(i);
        }

        while (!queue.isEmpty()) {
            System.out.println(queue.take());
        }
    }
}
  
Tip: Bounded channels are useful for controlling memory and load, unbounded ones are for simple data streams when processing is guaranteed.
Practical Application

Bounded channels/queues are for limiting concurrent orders, database requests. Unbounded ones are for log queues, event streams, task flows without limits.

  • Pros: safe memory management (bounded), flexibility (unbounded).
  • Cons: bounded may block on overflow, unbounded — risk of OOM with a sharp increase in load.
Concept Go Java Comment
Resource Cleanup defer try-with-resources Automatic resource closing, preventing leaks
Rate Limiting time.Ticker, goroutines ScheduledExecutorService Control request frequency to protect the service
Bounded Channel chan with buffer ArrayBlockingQueue<Integer> Limiting load and memory
Unbounded Channel chan + goroutine LinkedBlockingQueue<Integer> Flexible queue without strict limits

Output

Go and Java offer different tools for similar tasks. Go emphasizes simplicity and built-in primitives (defer, channels, time.Ticker), while Java focuses on powerful classes and interfaces from the standard library. For a Java developer learning Go, the key is to get accustomed to goroutines and channels, and for a Go developer, it is to understand the richness of classes and queues in Java. In business logic, understanding these concepts allows for safe resource management, workload control, and the construction of reliable asynchronous systems.


  ASCII diagram of streams and channels (Go):

      producer
         |
       [chan] <-- buffer size N
         |
      consumer
    

🌐 На русском
Total Likes:0

Оставить комментарий

My social media channel
By sending an email, you agree to the terms of the privacy policy

Useful Articles:

Let's look at: Trace, Profiling, Integration Testing, Code Coverage, Mocking, Deadlock Detection in Go vs Java | Testing, Debugging and Profiling
Series: Go for Java Developers — analysis of trace, profiling and testing In this article we will analyze tools and practices for testing, debugging and profiling in Go. For a Java developer this wil...
Synchronization and security in Go vs Java | Concurrency part 2
← Part 1 — Basics of Concurrency in Go for Java Developers In the second part, we will dive into synchronization and safety of concurrent code in Go. For a Java developer, it is useful to see t...
Error handling and defer in Go (Concurrency and synchronization) | Patterns, idioms, and best practices in Go
Error handling in Go is significantly different from the familiar Java approach with exceptions. Instead of try/catch, Go uses returning an error as a separate value, and `defer` helps safely release ...

New Articles:

Concurrency is not about “starting many threads”. It’s about agreements between them. Imagine a restaurant kitchen: — cooks (threads / goroutines) — orders (tasks) — and the main question: how do th...
When HashMap starts killing production: the engineering story of ConcurrentHashMap
Imagine a typical production service. 32 CPU hundreds of threads configuration / session / rate limits cache tens of thousands of operations per second And somewhere inside — a regular Map. At first...
Zero Allocation in Java: what it is and why it matters
Zero Allocation — is an approach to writing code in which no unnecessary objects are created in heap memory during runtime. The main idea: fewer objects → less GC → higher stability and performance. ...
Fullscreen image