My Experience with Go Programming

I’ve been using Go for a while now, both for personal projects and professionally. I’ve taken courses through Ardan Labs and read several books including The Go Programming Language, 100 Go Mistakes and How to Avoid Them, and I’m currently working through Writing An Interpreter In Go.

Go has proven to be a special language where you can become very productive very quickly.

The Standard Library

Go’s standard library truly feels second to none. For most use cases, you don’t need to reach for third-party libraries unless you want a specific integration. This comprehensive standard library is one of Go’s greatest strengths.

The Concurrency Trap

Go is often marketed as an excellent concurrent language, and I understand why people get excited about it. The goroutines (lightweight, green threads) model makes it incredibly easy to spawn new threads using the go keyword:

go func(){
  // whatever you want to run in the goroutine
} ()

However, one thing that’s often overlooked is how easy it is to shoot yourself in the foot when writing concurrent programs. Whether you’re creating data races, trying to access closed channels, or running into deadlocked mutexes, the pitfalls are numerous. While Go includes a race condition detector that catches some obvious issues, you’re largely on your own for the rest.

I didn’t fully appreciate this problem until I tried other languages. Elixir/Erlang’s immutable nature paired with its message-passing system feels like a first-class solution to these concurrency challenges. You do sacrifice some performance and control, but in return you get a much safer multithreaded framework.

My experience with Rust is more recent, but its richer type system seems to force you into safer multithreaded programs. It prevents you from creating multiple mutable references unless they’re wrapped in an Arc<Mutex<T» or another structure designed for concurrent access. This feels like a stronger foundation for building reliable software with fewer hidden pitfalls.

Hidden Details

Go is full of subtle gotchas and rules that take time to master. For example, understanding when you’re passing a copy of a value versus a reference (or copy of a reference) to a function:

func stringAction(in string) {
    in = "another string"
}
s := "some string"
stringAction(s)
fmt.Println(s) // "some string" - unchanged

func mapAction(in map[string]string) {
    in["here"] = "there"
}
m := map[string]string{
    "value": "first",
}
mapAction(m)
fmt.Println(m) // map[here:there value:first] - modified

Memory Leaks

Of all the sections in this post, this might be the most attributable to skill issues on my part. The combination of the garbage collector, Go’s “magic,” and the available tooling makes finding memory leaks in your program extremely difficult. I found myself reading extensively and watching debugging courses to gain better observability into my programs.

Null Pointers

This is something I feel spoiled by in other languages that have better solutions, like the Option type. Null pointer issues bite me occasionally, and I wish the language had built-in protection. That said, it’s not the end of the world checking pointers before use is straightforward enough:

func something(ptr *string) {
    if ptr == nil {
        // handle null case
    }
    // handle non-null case
}

Null Interfaces

This is another small gotcha that catches you periodically. Again, it’s not a major issue since you can check for it, but it’s one of those things that leaves a bad taste:

type Something interface {
    action()
}

func caller(s Something) {
    // s can be a null pointer
    s.action() // this will panic if s is null
}

var something Something // initialized as null pointer
caller(something) // panic!

Error Handling

I have fewer complaints about Go’s error handling than many others seem to. I genuinely don’t mind being forced to deal with errors explicitly, and I much prefer errors as values over errors as exceptions (my experience with JavaScript solidified this preference). Pattern matching and enums would make handling different error types more granular and easier, but it’s not something that would push me away from the language.

ret, err := randomFunction()
if err != nil {
    // handle error
}

Performance

The memory footprint and performance of Go applications I’ve worked with have been excellent. I primarily write web services, and performance has never been a concern. Go performs orders of magnitude better than comparable services I’ve written in Node.js and Elixir.

No Function Coloring

In JavaScript and other languages, you must mark async functions with async and await keywords:

async function asyncFunction() {}
await asyncFunction();

In Go, there’s no such differentiation the runtime handles asynchronous operations transparently:

func asyncFunction() {}
asyncFunction()

This simplicity keeps your code clean, though it does hide what’s actually happening in the program.

Testing

Testing is one of Go’s strongest suits. The simplicity just works. You create files ending with _test.go containing exported functions that start with Test and have the signature func(*testing.T). The built-in testing tools run your tests with the go test command.

// file_test.go
package module_test // test external APIs with 'module_test' or internal with 'module'

func TestFunction(t *testing.T) {}

It feels like a well thought out, first-class solution. No need to bring in external libraries or add macros to your code. Go’s testing framework has become the standard I compare others against.

Conclusion

Go is a special language that truly deserves its popularity. It does an excellent job of balancing performance, safety, and productivity. You can quickly build applications that scale effectively.

What makes Go special is its simplicity. It doesn’t try to include every possible feature or make everyone happy it focuses on doing a specific set of things very well.

My main criticism of Go is how much it hides from you. The lack of control and the hidden nature of certain language features make it less suitable for lower-level systems programming or complex problems where correctness is your top priority. But for the vast majority of applications, especially web services and distributed systems, Go excels.