Learning Golang with a Java Background: A Developer’s Journey — Part III
We have already published two parts of this series. If you are new to Golang and have a Java background, check out the previous parts to start from scratch:
In this article, we will explore three core concepts of Golang: Pointers, Goroutines, Channels and Context. These concepts are fundamental to writing efficient and concurrent programs in Go.
Pointers
Java:
In Java, pointers are handled implicitly by the JVM. Java does not expose memory addresses directly but provides methods like hashCode()
to retrieve an identifier for an object. Java primarily follows pass-by-reference when working with objects.
Example:
public class Main {
public static void main(String[] args) {
MyClass myClassObject = new MyClass();
myClassObject.value = 5;
System.out.println(myClassObject.value); // prints 5
update(myClassObject);
System.out.println(myClassObject.value); // prints 6
}
public static void update(MyClass myClassObject) {
myClassObject.value = 6;
}
}
class MyClass {
int value;
}
In the above example, the object reference is passed to the update
method, and modifications are reflected in the caller method.
Golang:
Unlike Java, Golang requires explicit pointer handling using the &
operator. Go follows pass-by-value by default, meaning that when an object is passed to a function, a copy of it is created. If you want to modify the original object, you need to pass a pointer.
Example (Without Pointers — No Change):
type MyObject struct {
Value int
}
func main() {
object := MyObject{Value: 5}
fmt.Println(object.Value) // prints 5
update(object)
fmt.Println(object.Value) // prints 5
}
func update(object MyObject) {
object.Value = 6
}
Example (With Pointers — Value Gets Updated):
type MyObject struct {
Value int
}
func main() {
object := MyObject{Value: 5}
fmt.Println(object.Value) // prints 5
update(&object)
fmt.Println(object.Value) // prints 6
}
func update(object *MyObject) {
object.Value = 6
}
Goroutines and Channels
Java developers familiar with Threads, Callables, and Runnables will find Goroutines and Channels to be Go’s equivalents.
Java:
In Java, we use CompletableFuture
for asynchronous programming. If the result is not required immediately, we can use runAsync()
, otherwise, we use supplyAsync()
.
Example:
import java.util.concurrent.*;
public class MyClass {
public static void main(String args[]) throws Exception {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("Executing the supplyAsync");
return 1;
});
System.out.println("Completed: " + future.get());
}
}
Golang:
Go does not have a Future
concept but instead uses channels to synchronize goroutines.
Example (Using Channels):
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan bool)
go func(ch chan bool) {
fmt.Println("Processing the thread")
time.Sleep(2 * time.Second)
fmt.Println("2-second wait is complete")
ch <- true // pass down results to the channel
}(ch)
fmt.Printf("Processed Successfully: %v", <-ch) // <- ch wait until the result published in the channel
}
Channels in Go help synchronize goroutines and pass data between them. Another way to manage concurrency is using sync.WaitGroup
.
Context in Golang
Unlike Java, Go provides a built-in context package for managing request-scoped values, cancellation signals, and timeouts across API boundaries and goroutines.
Core Functions in context
Package:
context.WithCancel(parent Context)
: Allows manual cancellation of operations.context.WithTimeout(parent Context, timeout time.Duration)
: Cancels operations after a timeout.context.WithValue(parent Context, key, val interface{})
: Passes request-scoped values.
Example (Cancellation):
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Worker canceled:", ctx.Err()) // Execute this Line
return
default:
fmt.Println("Working...")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx)
time.Sleep(2 * time.Second)
cancel()
time.Sleep(1 * time.Second)
}
Example (Timeout):
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context) {
select {
case <-time.After(2 * time.Second):
fmt.Println("Work done!")
case <-ctx.Done():
fmt.Println("Worker canceled:", ctx.Err()) // Execute this as timeout is 1 second
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
go worker(ctx)
time.Sleep(2 * time.Second)
}
Example (Request Scoped Values):
package main
import (
"context"
"fmt"
)
func main() {
ctx := context.WithValue(context.Background(), "userID", 123)
userID := ctx.Value("userID").(int)
fmt.Println("User ID:", userID)
}
When to Use Context
- HTTP Requests: Handle timeouts or cancellations.
- Database Calls: Enforce query timeouts.
- Goroutines: Manage concurrent processes efficiently.
- Microservices: Pass request metadata across services.
This marks the end of our comparison between Java and Golang. While we’ve covered key differences in pointers, concurrency, and context handling, the best way to solidify these concepts is through hands-on practice. Try implementing small projects in Golang to experience its unique features firsthand!