Go Explained: Compilation and Limits
Go (or Golang) is a popular programming language, created by Google, designed to be simple, fast, and efficient. It’s mostly used for building web servers, cloud services, and other backend applications. Based on my experience with it, Go is easy to learn and has the ability to handle many tasks at the same time, making it great for modern, scalable software systems.
How Go works?
In a nutshell
This is the most exciting part and the main reason I wanted to write the article: to explain how Golang works under the hood, how it’s compiled, its concurrency model, and its garbage collector.
In a nutshell, Go is a compiled language, meaning it gets turned into machine code that creates executable files. These files can run on any system without needing to install Go or any extra software.
For example, if you compile a Go program on a Linux machine, you can run it on any other Linux machine without needing to install Go again. The same goes for a program compiled for Windows; it can run on any Windows machine without installing Go or any other dependencies.
Compilation steps
Step 1
The Go compiler reads the source code and converts it into an intermediate representation (IR) using a parser. The IR is then passed to the semantic analysis stage, where the compiler checks for errors and resolves any symbols or names used in the code.
Step 2
The next stage is code generation, where the compiler creates machine code from the IR. This process includes optimizations, such as inlining and constant folding.
Step 3
The final stage is linking, where the compiler combines the machine code for all package dependencies and creates a single executable binary.
The runtime
When the program runs, the Go runtime system initializes, which includes the garbage collector, memory allocation, and the scheduler for goroutines.
Go doesn’t use a virtual machine. This means it’s directly converted into machine code, unlike languages like Java, which compile to bytecode that runs on the Java Virtual Machine (JVM). The JVM then interprets the bytecode and executes the machine code on your computer.
For example, if you have a Go program that adds two numbers, the Go compiler will create machine code that directly does the calculation. In Java, the same program would be compiled to bytecode first, and then the JVM would interpret that bytecode to perform the calculation.
Go’s concurrency model
Go is great at handling many tasks at once using goroutines and channels. You can run tasks in parallel without worrying about locks or syncing them.
Go’s concurrency model is built on Communicating Sequential Processes (CSP). This is a way to make concurrent programming easier by clearly showing how different processes talk to each other. It helps make systems more scalable and reliable.
Go’s garbage collector
Go has a garbage collector that cleans up unused memory in the background, so it doesn’t slow down your program.
How Go Handles Garbage Collection
Go (Golang) uses a smart method to clean up unused memory called the concurrent mark-sweep garbage collector. This approach helps keep apps running smoothly without long pauses.
Here’s how it works:
- Mark Phase:
- The garbage collector starts by thinking of memory objects in three colors:
- Black for used objects,
- White for unused objects,
- Gray for objects being checked.
- It marks all important objects (like global variables and those in use) as gray. Then it looks at these gray objects and marks any objects they refer to as gray too.
- The garbage collector starts by thinking of memory objects in three colors:
- Sweep Phase:
- After marking is done, it goes through memory and collects all the white objects (the ones no one is using).
- This sweep happens while the app is still running, which helps keep things fast.
- Reclaiming Memory:
- The garbage collector returns the space from white objects back to the memory pool, so it can be reused later.
- The gray and black objects stay available for the app to use.
By using this concurrent approach, Go minimizes long pauses that can slow down applications, allowing for a smoother user experience. The garbage collector is regularly updated, so it keeps getting better with each new version of Go.
Garbage collection in action
Here’s a simple code example in Go that demonstrates garbage collection in action:
package main
import (
"fmt"
"runtime"
)
func main() {
// Print memory stats before allocation
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
fmt.Println("Before allocation:")
fmt.Printf("Alloc = %v MiB", memStats.Alloc/1024/1024)
// Allocate memory
slice := make([]int, 0, 1e6) // Allocate space for 1 million integers
for i := 0; i < 1e6; i++ {
slice = append(slice, i)
}
// Force garbage collection
runtime.GC()
// Print memory stats after garbage collection
runtime.ReadMemStats(&memStats)
fmt.Println("\nAfter garbage collection:")
fmt.Printf("Alloc = %v MiB", memStats.Alloc/1024/1024)
}
Go’s tradeoffs
When we use a programming language like Go, we should think about its downsides along with its benefits. Every language has its strengths and weaknesses, so we just need to choose one that fits our needs without worrying too much about the downsides.
Here are some tradeoffs with Go:
- No Generics: Go doesn’t support generics directly, but you can find ways around it.
- Error Handling: Instead of using exceptions, Go handles errors with return values and a panic/recover system.
- Limited Object-Oriented Features: Go has structs and methods but lacks inheritance, so you have to use composition and interfaces for your designs.
- Strict Typing: You need to specify the type of every variable and function argument, which might be new if you’re coming from a language like JavaScript. But if you know TypeScript, it won’t be a big change.
Thanks,
Youssef Ameachaq