• Home
  • Testimonials
  • Blog
  • Contact Us

At a Glance of a Key

Crafting Dreams into Ventures, Code into Excellence: Your Journey to Success

  • Home
  • Testimonials
  • Blog
  • Contact Us

To Go or not to Go

2022-09-08 Development 4 Comments 7 minute read

Throughout my career, I have worked with a variety of programming languages. The majority of them have similar concepts, with the main difference being in the structure, syntax, and eco-system (compiler, libraries, etc). I enjoyed writing code in C and C++ at Dell, and when I joined Velostrata (which was acquired by Google), I was thrilled to switch to C++ 11, which added a lot of excellent functionality and eliminated the need for external libraries (like BOOST). I also used Python for small utilities and testing on occasion. At AWS, I completely switched to Java. I learned to appreciate the fully managed eco-system, complete with a GC that looks after your memory and thousands of libraries that help you save time and focus on your business logic. Before joining Highline, I was informed that our primary programming language is Go, and I was eager to take on the challenge of learning a new language πŸ™‚ Here are my thoughts after a few months of writing in Go.

Initially, I began looking for an editor. I was familiar with JetBrains and had used their products before (CLion, PyCharm, IntelliJ). Although they offer a Go IDE (called GoLand), I discovered that there is no free version, and I was unwilling to pay so much out of pocket unless there was a compelling reason. I chose VSCode based on the team’s positive recommendations. After remembering it being heavy and slow a few years ago, I was surprised to see it running fast, and it even had an official Golang plugin that made writing code, running it, and debugging simple. So, I guess I’ll stick with VSCode for the time being. Yes, it has a few quirks, particularly with the unit tests, but nothing major that would irritate me too much.

The Good

One of the great things is the speed and simplicity. With a few lines of code, you can start an HTTP server, add a few endpoints, and route them to specific functions. You can use middleware modules to intercept requests, and structure your application in a few easy steps. There are also ready-to-go libraries to work with SQL (and non-SQL) databases (e.g. Gorm), define and emit metrics to Prometheus (to eventually integrate them into Grafana dashboards) and many more. There is probably a library out there that you can leverage and use almost everything you need.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/hello", HelloServer)
    http.ListenAndServe(":8080", nil)
}

func HelloServer(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World!")
}

Running it is as simple as executing: “go run hello.go” (assuming this is your file name). Then, from another terminal session making a request will give us the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
➜  Projects βœ— curl -i http://localhost:8080/
HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Sun, 04 Sep 2022 21:08:55 GMT
Content-Length: 19

404 page not found

➜  Projects βœ— curl -i http://localhost:8080/hello
HTTP/1.1 200 OK
Date: Sun, 04 Sep 2022 21:08:58 GMT
Content-Length: 12
Content-Type: text/plain; charset=utf-8

Hello World!%

➜  Projects βœ—

Go also has support for pointers, allowing you to have better control of your memory and the way things are being passed between functions. Go also has a GC that runs in the background and is cleaning up stuff that you don’t use anymore.
Running functions in the background is also a simple task, and requires you to only add the keyword “go”. A small example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
    "fmt"
    "time"
)

func main() {
    go printNumbers()
    time.Sleep(3 * time.Second)
}

func printNumbers() {
    for {
        fmt.Println("I am running in the background!")
        time.Sleep(500 * time.Millisecond)
    }
}

Running it will produce the following output:

1
2
3
4
5
6
7
8
➜  Projects βœ— go run hello.go
I am running in the background!
I am running in the background!
I am running in the background!
I am running in the background!
I am running in the background!
I am running in the background!
➜  Projects βœ—

When required to pass data between different threads, you don’t have to think about locks and other mechanisms, but just use channels and let it take care of everything else.

The Bad

OOP. Seriously. After working with C++ and Java for so many years, it’s hard for me to accept a modern programming language that doesn’t provide object-oriented capabilities. Well, kinda. Go allows you to define structures, and then you can create methods (so there is a similar notion to “this”), but it doesn’t allow you to do function overloading, and inheritance is replaced by composition (you can include another type within your type) and you can leverage interfaces to do boxing and unboxing. There is no notion of “private”, but everything that starts with a lowercase letter will be considered to exist in the “package” scope, and everything that starts with an uppercase letter will be considered to be public.

Here is an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package main

import "fmt"

type Name struct {
    First string
    Last  string
}

// This is part of the Stringer interface
func (n Name) String() string {
    return fmt.Sprintf("%s %s", n.First, n.Last)
}

// Note that Name can also be a pointer.
func (n *Name) private() {
    fmt.Println("Ha! You can call me only from within the package")
}

type Person struct {
    Name  // Composition of structs. Note that String() will be inherited
    Phone string
}

func (p Person) DoSomething() {
    fmt.Printf("%s is doing something...\n", p.Name.First)
}

func main() {
    p := Person{
        Name:  Name{First: "Alexander", Last: "Sirotin"},
        Phone: "N/A",
    }

    // Name.String() will be invoked here as we don't have Person.String() implementation.
    fmt.Println(p)

    // Although the function name starts with a lowercase, I can still call it from the outside.
    p.private()

    // You can box structs into interfaces with a specific set of actions
    var boxed interface{ DoSomething() } = p
    boxed.DoSomething()
}

Running it will produce the following output:

1
2
3
4
5
➜  Projects βœ— go run hello.go
Alexander Sirotin
Ha! You can call me only from within the package
Alexander is doing something...
➜  Projects βœ—

Another thing is unit tests. Writing unit tests in Java was a real pleasure, especially with the JUnit5 framework. It allows you to create classes and methods, and have different annotations to control the flow. @BeforeClass, @Before, @After, etc. There is also something that is called Parameterized tests, which allows you to invoke the same test method, using different inputs that you feed from a “method source”. Unfortunately, I haven’t found anything similar in Go out of the box. Yes, there is a powerful testing framework that gives you a LOT of flexibility, as you can write tests and subtests, and do different kinds of stuff, but it doesn’t give you much structure, which makes it easier to have test methods that span over hundreds of lines of code and include different initializations and sub-tests that cannot run (or be debugged) independently. Enforcing a structure takes some effort, and without that, it can become a big mess real quick.

The Ugly

Error Handling. With Go, we are back to the dark ages of returning errors from functions and having to check them after almost every function call. Go doesn’t provide you with an exception mechanism, but instead, it asks you to keep passing “error” objects between function calls. Yes, it has the keywords “panic” and “recover”, which can simulate exceptions-like behaviour, but the common practice is to just propagate errors up the stack.

Let’s take a simple example of the two approaches. With the first approach, where we keep propagating the error, the code is inflated and looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import "fmt"

func main() {

    err := run()
    if err != nil {
        fmt.Printf("Something went wrong: %s\n", err)
        return
    }

    fmt.Println("Completed successfully!")
}

func run() error {

    v, err := load()
    if err != nil {
        return err
    }

    n := calculate(v)
    return persist(n)
}

func load() (int, error) {
    fmt.Println("Loading")
    return 5, nil
}

func calculate(value int) int {
    fmt.Println("Calculating")
    return value * 2
}

func persist(new_value int) error {
    fmt.Println("Persisting")
    return fmt.Errorf("Could not persist the value")
}

Running it will produce the following output:

1
2
3
4
5
6
➜  Projects βœ— go run hello.go
Loading
Calculating
Persisting
Something went wrong: Could not persist the value
➜  Projects βœ—

On the other hand, if we use “panics”, the code will look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import "fmt"

func main() {

    if ok := run(); !ok {
        return
    }
    fmt.Println("Completed successfully!")
}

func run() bool {

    defer func() {
        fmt.Println("Recovering")
        if r := recover(); r != nil {
            fmt.Printf("Something went wrong: %s\n", r)
        }
    }()

    v := load()
    n := calculate(v)
    persist(n)
    return true
}

func load() int {
    fmt.Println("Loading")
    return 5
}

func calculate(value int) int {
    fmt.Println("Calculating")
    return value * 2
}

func persist(new_value int) {
    fmt.Println("Persisting")
    panic("Could not persist the value")
    fmt.Println("Persisted")
}

Running it will produce the following output:

1
2
3
4
5
6
7
➜  Projects βœ— go run hello.go
Loading
Calculating
Persisting
Recovering
Something went wrong: Could not persist the value
➜  Projects βœ—

Note that we still need to have a return value from “run()” to indicate whether we should continue with the flow or not, and there is still overhead with writing the recovery function (“defer” is a nice keyword in Go that basically says, run this when going out of scope, regardless of what happened). Using panics as a pattern, and forgetting a “recover” statement may crash your whole application, even if it happened in one thread.

As mentioned before, the most common pattern in Go is to return errors, and most of the libraries I worked with are following it, which makes the code long and messy.

In Conclusion
Go is nice πŸ™‚ Yes, it has some quirks, but the bottom line is that it’s a powerful modern language which provides you with great tools to focus on the business logic of your application, and with great performance. The language is still evolving, and version 1.19 has some kind of “Generics” and I am confident that it will become even more powerful with time.

– Alexander

Oh hi there πŸ‘‹
It’s nice to meet you.

Sign up to receive a notification when new posts are published!

We don’t spam!

Check your inbox or spam folder to confirm your subscription.

GoOOPProgrammingWebService

Hit the Ground Running

Monolith or Microservices?

4 thoughts on “To Go or not to Go”
  1. Ilya
    2022-09-08 at 1:40 PM

    This is actually a very insightful and well written post. I enjoyed reading it and learning about Go, especially since I never worked with it before. Thank you Alexander

    Reply
  2. Yong
    2022-09-12 at 10:04 PM

    How did you make it to write docs day and night? Lol writing docs during work hours is already enough to me

    Reply
  3. Keira
    2024-03-14 at 11:35 AM

    Spot on with this write-up, I truly feel this web site needs a lot more attention. I’ll probably be returning to read more, thanks for the information!

    Reply
  4. writeablog.net
    2024-04-07 at 9:57 PM

    Wow, that’s what I was searching for, what a material!
    present here at this webpage, thanks admin of this site.

    Reply
Leave a Reply Cancel reply

About Me

Principal Software Engineer and an industry leader with startup and FAANG experience. I specialize in distributed systems, storage, data protection services and payment processors.

Beyond technical expertise, I am passionate about supporting fellow engineers in their careers. Through approachable blogs and hands-on guidance, I help navigate the ever-evolving landscape of technology, empowering individuals to thrive in their professional journeys.

Open LinkedIn

Recent Posts

  • Building a Delayed Message System with Redis and FastAPI
  • Go Concurrency, Practical Example
  • Using GORM – Part 3: Models and Idempotency
  • Using GORM – Part 2: Transactions and Save Points
  • Using GORM – Part 1: Introduction

Archives

  • January 2025
  • December 2024
  • March 2023
  • February 2023
  • September 2022
  • July 2022
  • July 2021
  • June 2021
  • February 2021
  • April 2018
  • March 2018
  • January 2018
  • July 2017
  • June 2017
  • May 2017

Categories

  • AWS
  • Career Growth
  • Cyber Security
  • Debugging
  • Development
  • Storage
  • Tips & Tricks

Tags

API AWS Azure Bash Brainfuck C++ Challenge Cloud Cloud Bursting Concurrency Database DevOps Disassembly DLL Documentation DynamoDB Go Golang Guice Java Jenkins Mossad NoSQL OOP Performance Programming Python Redis Security Serverless Singleton Streams Testing Unit Tests WebService

All Rights Reserved 2025 Β© Sirotin Enterprises Inc.
Proudly powered by WordPress | Theme: Doo by ThemeVS.