Tags:conceptGoLang Status:🟩


GoLang

Summary

Go (Golang) is a statically typed, open-source programming language developed by Google. It’s designed for simplicity, efficiency, and concurrency, making it great for building fast, scalable applications, especially in distributed systems and cloud services.

For direct code implementations, see this.

Pointers

Pointers in Go are similar to references in Java and help avoid NullPointerException. They store the memory address of a variable, allowing direct access and modification. Both o1 and o2 points to the same object.

// Java implementation
MyClass o1, o2;
o1 = new MyClass();
o2 = o1;
// GoLang implementation
var o1, o2 *MyClass
o1 = new(MyClass)
o2 = o1

*int = “pointer to an int” &x = “adress of x” In GO you can explicitly access the address of an variable. You are not talking about the value but the logical address in the memory.

x := 10          // Declare an int variable
var ptr *int     // Declare a pointer to int
ptr = &x         // ptr points to the address of x
 
fmt.Println("Value of x:", x)     // Output: Value of x: 10
 
fmt.Println("Address of x:", &x)  // Output: Address of x:<memory address>
 
fmt.Println("Value via pointer:", *ptr) // Output: Value via pointer: 10
 
fmt.Println("Address stored in ptr:", ptr) // Output: Address stored in ptr: <same memory address as x>

Slices

Slices are dynamic arrays that allow you to allocate space for a specific number of elements. Unlike static arrays, slices can grow and shrink in size without needing to create a new array and copy data.

Functions

Functions are essential in Go.
They are similar to Java methods but are not tied to a class, offering flexibility in design.
Go supports recursion and also allows methods to be associated with a structure.

Call-by-Value:
When a function is called, a copy of the value is passed, meaning changes to the parameter inside the function do not affect the original value.

func modifyValue(x int)

Call-by-Reference:
Instead of passing a copy, you can pass a reference to the value, allowing the function to modify the original value directly through the memory address.

func modifyReference(x *int)

Threads & Synchronization

Threads are used when a program needs to perform multiple tasks simultaneously.

Threads:
A single process can spawn multiple threads (goroutines in Go) that can execute in parallel.
Threads share access to memory and resources, leading to potential issues like race conditions and starvation. Synchronization is essential to avoid random results.

Goroutines

Calling a function with f() will wait for it to return.
Using go f() creates a new goroutine that calls the function without waiting.

Example: Fibonacci with Goroutine

func main() {
    go spinner(100 * time.Millisecond)
    const n = 50
    fibN := fib(n) // slow
    fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
}
 
func spinner(delay time.Duration) {
    for {
        for _, r := range `-\|/` {
            fmt.Printf("\r%c", r)
            time.Sleep(delay)
        }
    }
}

In this example, the main thread creates a new goroutine for the spinner function. The main program continues to execute, but if it terminates, the child thread (spinner) also terminates, even though it runs in a loop.

Race conditioning

A race condition occurs when two or more goroutines access a shared resource (e.g., a file or variable) and attempt to modify it simultaneously. The outcome can vary based on the scheduling order by the Go runtime, leading to incorrect or unexpected results. This issue is challenging to reproduce and diagnose since it depends on specific conditions.

Example:

package main
 
import "fmt"
 
var counter int
 
func increment() {
	counter = counter + 1
}
 
func main() {
	for i := 0; i < 1000; i++ {
		go increment()
	}
	fmt.Println("Final counter:", counter)
}

In this code, multiple goroutines increment the shared counter variable without synchronization, resulting in a race condition. The final value of counter may be less than 1000, varying unpredictably with each run.

Locks

Locks are mechanisms to ensure that only one goroutine can access a shared resource at a time, making programs safe for concurrent use. If a resource is locked by one goroutine, any other goroutine attempting to access it will be blocked until it is unlocked.

sync.Mutex: A Mutex prevents race conditions by allowing only one goroutine to access a resource at a time.

  • Declare a variable with sync.Mutex.
  • Use Lock() and Unlock() methods to guard the resource.

Deadlock

A deadlock occurs when two or more goroutines wait on each other to release resources, resulting in indefinite waiting. Use Lock() and Unlock() wisely to prevent this situation.

Example:

func main() {
    go func() {
        mu1.Lock()
        mu2.Lock()
        // Do something
        mu2.Unlock()
        mu1.Unlock()
    }()
 
    mu2.Lock()
    mu1.Lock()
    // Do something
    mu1.Unlock()
    mu2.Unlock()
}

In this example, the first goroutine locks mu1 and waits for mu2, while the second locks mu2 and waits for mu1, leading to a deadlock.

Channels

Channels are used for communication and synchronization between goroutines, allowing one goroutine to send data to another. They can be synchronous or asynchronous.

Basic Channel Operations:

ch := make(chan int) // Channel of type 'chan int'
ch <- x // Send statement
x = <-ch // Receive expression
<-ch // Discarding received result
 

Channels can lead to deadlocks.

Synchronous Channels

In synchronous channels, both sending and receiving block until the other side is ready. This ensures that when a goroutine sends data, it waits until another goroutine is ready to receive it.

Example:

ch := make(chan int)
go func() {
    ch <- 42 // Blocks until the receiver is ready
}()
fmt.Println(<-ch) // Unblocks the sender and receives the value
 

Asynchronous Channels

Asynchronous channels have a buffer, allowing them to hold a set number of values without blocking. Sending only blocks if the buffer is full, and receiving only blocks if the buffer is empty. Despite this flexibility, they can still experience deadlocks under certain conditions.

Example:

ch := make(chan int, 2) // Buffered channel with capacity 2
ch <- 1
ch <- 2
fmt.Println(<-ch) // Prints 1
fmt.Println(<-ch) // Prints 2

Synchronous vs. Asynchronous Channels

Synchronous Channels:

  • Ensure tight synchronization since operations block until both sides are ready.
  • Useful for guaranteed data exchange at specific points.

Asynchronous Channels:

  • Allow more flexibility, enabling goroutines to proceed without waiting.
  • Can improve resource efficiency but require careful handling to avoid deadlocks.