Why I Am Switching to Go in 2024

Go is famous for its simplicity, but in this article you’ll see that this is just the tip of the iceberg. There are many other interesting features that make Go perfect for developers who want to build amazing products while enjoying a clean, efficient dev experience.

For a bit of context, I’m a developer with 15 years of experience, and my background is mostly in languages like Java, Kotlin and JavaScript. I experimented with various other languages during this time, but Go is the only one winning me over because it allows me to write better, safer code. On top of that, it forces me to break some of the bad habits I gained in my hardcore OOP Java days. This brings us to one of the features I believe shapes the way you think about and solve problems in Go - its approach to object oriented programming.

1. Go and Object Oriented Programming

If your background is in one of the many established OOP languages the first thing you’ll notice is the lack of a “class” keyword. The second thing you’ll notice is that the OOP paradigm does not affect the way you write code. This is somewhat liberating, especially if you are coming from Java where everything revolves around classes, objects and all the design patterns under the sun.

The usefulness of OOP is still debated in the software world. Alan Kay, one of the pioneers of object-oriented programming has this to say on the topic:

“I regret the word ‘object’. It gets many people to focus on the lesser idea. The real issue is messaging. Messaging is the abstraction of the communication mechanism. It’s what allows different parts of a system to interact without knowing anything about each other. That’s the power of objects, and it’s what makes them so useful.”

In other words, the real focus of this paradigm is the reliable exchange of information between different parts of a system, and objects are just a means to an end. You’ll also find more drastic opinions on the subject, but the usefulness of OOP in modern development is a topic for a different video.

Back to Go, we first need to sort out if is Go an object oriented language ?. The answer to this question is… kind of. Go supports most of the concepts commonly associated with OOP, but it does it in a non-invasive manner.

Let’s look at some code to better understand what I’m talking about.

public class Movie {
  public String name;
  public double score;

  public Movie() {}

  public Movie(String name, double score) {
    this.name = name;
    this.score = score;
  }
}

This is a Plain Old Java Object, which is basically a simple structure allowing you to group together and encapsulate data. Even though you are not familiar with the language, you’ll be able to identify a lot of keywords and constructs the developer is forced to work with: You need to use keywords to define the visibility and accessibility of classes, fields and methods. Then, you need to explicitly define constructors so that you can instantiate your class, and you’ll use the special “this” keyword to refer to the current instance of the class.

Things are getting even more verbose when encapsulating your data, since a long list of repetitive getters and setters have to be defined.

public class Movie {
   protected String name;
   private double score;


   public Movie() {}


   public Movie(String name, double score) {...}


   public String getName() {...}
   public void setName(String name) {...}
   public double getScore() {...}
   public void setScore(double score) {...}
}

And, of course, the sh*t hits the fan if you have to factor in object comparison and equality.

public class Movie {
  protected String name;
  private double score;

  { ... }

  @Override
  public boolean equals(Object o) { ... }


  @Override
  public int hashCode() { ... }
}

In all fairness, JVM based languages like Kotlin will remove most of this clutter, and projects like Lombok can make Java development a bit more bearable thanks to code generation.

data class Movie(
   var name: String = "",
   var score: Double = 0.0
)

However, we can do much better. This is the equivalent of the previous class in Go.

type Movie struct {
 Name  string
 Score float64
}

func main() {
 m := Movie{Name: "Seven", Score: 10}
}

We can easily group together data via the struct type, and fields are public when starting with an uppercase letter, and private when starting with a lowercase letter. Also, there is no protected field since Go favors composition through embedded typings over inheritance.

Polymorphism in Go is achieved through interfaces, which are special types that specify a set of method signatures.

type Watchable interface {
  Play()
  Stop()
}

type Movie struct {
  Name  string
  Score float64
}

func (m *Movie) Play() {
  fmt.Printf("Playing %s\n", m.Name)
}

func (m *Movie) Stop() {
  fmt.Printf("Stopping : %s\n", m.Name)
}

func main() {
  m := Movie{Name: "Seven", Score: 10}
}

Any type that implements those methods will automatically satisfy the interface, so, in practice interfaces and associated structs can be fully decoupled.

It is also worth mentioning that the language designers fought the idea of supporting generics in the language for a long while. One of their main goals is to keep the language simple and easy to understand, especially for those coming from other programming languages. Generics can increase the complexity of the language, making it harder to learn and use efficiently. However due to community pressure, the idea was revisited and generics are supported in Go starting with March 2022.

func Print[T any](elements []T) {
  for _, element := range elements {
    fmt.Println(element)
  }
}

func main() {
  Print([]int{1, 2, 3})
  Print([]string{"Like", "&", "Subscribe"})
}

We discussed OOP principles, so let’s now move to something a bit more interesting.

2. Go and Functional Programming

While Go is not a functional programming language by design, it does support several functional programming techniques aimed to give you more programming flexibility.

Probably the most important aspect here is the first-class function support, which means that functions can be assigned to variables, passed as arguments and returned from other functions.

type operation func(int, int) int

func apply(a int, b int, op operation) int {
  return op(a, b)
}

func add(x int, y int) int {
  return x + y
}

func main() {
  addFunc := add
  fmt.Println(apply(5, 3, addFunc))

  fmt.Println(apply(2, 4, func(x int, y int) int {
    return x * y
  }))
}

Go is strongly typed, so you have to use type definitions for function signatures. While this might be a bit overwhelming at first, especially for higher order functions, it will improve code readability and safety in the long run.

Anonymous functions and capturing the enclosing variables surrounding the environment via closures are also supported features

, so you should get the picture. Just like Go is “kind of” object oriented, it is also “kind of” functional, in a sense that it gives you the right tools for the job without imposing any specific paradigm on you.

Don’t get used to this though because Go is not all sunshines and rainbows. You’ll see a bit later in the article that some programming behaviors are enforced by the language in an attempt to force you to write more reliable code.

3. Concurrency in Go

One of the most impressive achievements of Go, especially in this day and age, is its built-in concurrency support through goroutines.

To better understand this, let’s first make sure we are all on the same page, and we fully understand the difference between parallel and concurrent programming.

In short, you can do concurrent programming on a single core CPU. All operating systems have a scheduler which manages the execution of concurrent tasks. It decides which task runs at any given time, how long it runs, and when it should be paused to allow another task to run. In practice, when you have multiple apps running on a single core, they will run concurrently, with the scheduler giving them access for a few milliseconds. So, on a single core, processes can’t run synchronously, but the end user experience is rarely affected since all threads get their turn to advance their computations in a very short amount of time.

On the other hand, parallel programming requires multiple cores, with each core executing code at the same time, truly running tasks simultaneously.

Under the hood, the Go runtime includes a scheduler that can distribute goroutines across multiple operating system threads, which in turn can be run on multiple CPU cores. This allows your programs to take full advantage of both single and multi-core processors for concurrent execution.

Goroutines can be paired with channels and the sync package to handle various other concurrent programming scenarios, and this whole topic is deserving of a dedicated article. Please let me know in the comments if you are interested in a hands on video on the goroutines topic.

func worker(id int, wg *sync.WaitGroup, ch chan<- string) {
 defer wg.Done()
 msg := fmt.Sprintf("Worker %d completed", id)
 ch <- msg // Send message to the channel
}


func main() {
  var wg sync.WaitGroup
  ch := make(chan string, 5)


  for i := 1; i <= 5; i++ {
    wg.Add(1)
    go worker(i, &wg, ch)
  }


  go func() {
    wg.Wait() // Wait for all goroutines to finish
    close(ch) // Close the channel
  }()


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

I mentioned earlier that Go is not enforcing any programming styles or ideas in your code base. Well… I lied.

4. Go error handling

One of the most frowned upon language features in Go is its error handling. I’ll be honest - this one was hard for me to accept at first, since error handling becomes such an integral part of your implementation. Most standard library functions will return two values, one which is the actual result and the second one being any potential error.

func readFile(filename string) (string, error) {
  file, err := os.Open(filename)
  if err != nil {
    return "", fmt.Errorf("failed to open file: %w", err)
  }
  defer file.Close()


  info, err := file.Stat()
  if err != nil {
    return "", fmt.Errorf("failed to get file info: %w", err)
  }


  content := make([]byte, info.Size())
  _, err = file.Read(content)
  if err != nil {
    return "", fmt.Errorf("failed to read file: %w", err)
  }

  return string(content), nil
}

Then you have to make the conscious decision to ignore errors, which will make you rethink any corner case scenarios you might be too lazy to care about.

So, sure, your functions will not look as compact and clean as they used to in other languages, but the tradeoff is really worth it.

5. The Go compiler

Before wrapping things up, I have to mention that Go programs compile into single standalone binaries. Everybody mentions how fast the Go compiler is, but the single binary is where I find the most value in. It can be easily distributed and deployed through any type of CI/CD pipeline without worrying about dependencies or environment configuration.

Of course, these single binaries come with performance benefits, since everything is included so there is no overhead in loading shared libraries at runtime, and there are security benefits in reducing the number of dependencies and the attack surface.

However, at the end of the day, I’m just a lazy developer who doesn’t want to waste hours on configuring servers. With go you just need to put your binary somewhere in the cloud and simply run your application.

By the way, this article was focused on higher level concepts, and the impact a programming language like Go might have over your day to day life as a programmer.

Even though I didn’t mention features like memory management, control via pointers or the famous Go standard library, all these are contributing to an amazing, modern dev experience.

You can check some of my other videous if you want a deeper dive into some of the go basics.

Until next time, thank you for reading!