Exploring Four Potential Drawbacks of Go Language

Exploring Four Potential Drawbacks of Go Language

While rising in popularity (ranking 19th in TIOBE as of April 2018), Go's simplicity attracts developers new and old. As evidenced by software like Kubernetes, Docker, and Heroku CLI, its ease of learning makes it a compelling choice for production environments. However, this minimalist approach has trade-offs. Rob Pike, one of Go's creators, champions its simplicity, but it can sometimes lack features found in other languages, leading to workarounds or more verbose code.

The Four Go Drawbacks

1. Limited function flexibility

To illustrate, here's a code example from my work on Golang's Selenium binding. I created a function with three parameters, two of which were optional. This is the final implementation:


func (wd *remoteWD) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error {
    // the actual implementation was here
}

func (wd *remoteWD) WaitWithTimeout(condition Condition, timeout time.Duration) error {
    return wd.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval)
}

func (wd *remoteWD) Wait(condition Condition) error {
    return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval)
}        

Function overloading can necessitate writing more code, as programmers need to define multiple functions with the same name but different behaviors.

Code Improvement

Here's how it looks in JavaScript, with a few minor adjustments:

function Wait (condition, timeout = DefaultWaitTimeout, interval = DefaultWaitInterval) {
    // actual implementation here
}        

Moving on to Elixir, here's a different way to tackle this, demonstrating its expressiveness.

defmodule Waiter do
@default_interval 1
        @default_timeout 10

    def wait(condition, timeout, interval) do
            // implementation here
    end
    def wait(condition, timeout), do: wait(condition, timeout, @default_interval)
    def wait(condition), do: wait(condition, @default_timeout, @default_interval)
end

Waiter.wait("condition", 2, 20)
Waiter.wait("condition", 2)
Waiter.wait("condition")        

2. Absence of type abstraction

Go developers are eagerly calling for a specific feature: the ability to create a map function that seamlessly applies a given function to every element within an integer array.

Let's dive into how this functionality would work for integers, exploring its potential implementation and benefits.

package main

import "fmt"

func mapArray(arr []int, callback func (int) (int)) []int {
    newArray := make([]int, len(arr))
    for index, value := range arr {
     newArray[index] = callback(value)
    }
    
    return newArray;
}

func main() {
        square := func(x int) int { return x * x }
    fmt.Println(mapArray([]int{1,2,3,4,5}, square)) // prints [1 4 9 16 25]
}        

If you also need to perform this operation on strings, you'll have to create a separate function with a different name, even though the implementation is nearly identical. This is because Golang doesn't allow for function overloading (having multiple functions with the same name but different parameter lists). As a consequence, you'll end up with multiple functions that do similar things but have distinct names, potentially cluttering your code.

func mapArrayOfInts(arr []int, callback func (int) (int)) []int {
    // implementation
}

func mapArrayOfFloats(arr []float64, callback func (float64) (float64)) []float64 {
    // implementation
}

func mapArrayOfStrings(arr []string, callback func (string) (string)) []string {
    // implementation
}        

This approach doesn't align with the DRY principle, which advocates for minimizing code duplication by creating reusable functions instead of copying and pasting code.

While using a single implementation with an interface{} parameter might seem possible, it comes with drawbacks. This approach can lead to runtime errors due to potential issues with type-checking at runtime. Additionally, it can negatively impact performance, making it less efficient. Therefore, there isn't a straightforward way to combine these functions into a single one without encountering these limitations.

Code Improvement

Many programming languages offer robust generics capabilities. To illustrate, let's examine an implementation in Rust, which leverages vectors for simplicity:

fn map<T>(vec:Vec<T>, callback:fn(T) -> T) -> Vec<T> {
    let mut new_vec = vec![];
    for value in vec {
            new_vec.push(callback(value));
    }
    return new_vec;
}

fn square (val:i32) -> i32 {
    return val * val;
}

fn underscorify(val:String) -> String {
    return format!("_{}_", val);
}

fn main() {
    let int_vec = vec![1, 2, 3, 4, 5];
    println!("{:?}", map::<i32>(int_vec, square)); // prints [1, 4, 9, 16, 25]

    
    let string_vec = vec![
            "hello".to_string(),
            "this".to_string(),
            "is".to_string(),
            "a".to_string(),
            "vec".to_string()
    ];
    println!("{:?}", map::<String>(string_vec, underscorify)); // prints ["_hello_", "_this_", "_is_", "_a_", "_vec_"]
}        

The map function is remarkably versatile: it works seamlessly with any data type, including those you create yourself.

3. Managing external resources

As a seasoned developer, I've faced the dependency management monster numerous times. go get's easy installs often turned into versioning nightmares, and Dep felt like choosing between two evils: code overload or crash risk. Thankfully, Go modules emerged as a game-changer, delivering stability and control without the downsides. I'm here to share my journey and guide you through mastering Go dependencies!

Code Improvement

In the world of software development, package managers play a crucial role in streamlining code sharing and dependency management.

Here's a perfect example:

- Node.js and NPM:

  • NPM (Node Package Manager) is a vast repository that stores countless JavaScript packages.
  • It allows developers to easily access and install specific package versions, ensuring compatibility and preventing conflicts.
  • The package.json file acts as a blueprint, listing all project-specific dependencies and their required versions.
  • A simple npm install command seamlessly fetches and installs these dependencies, ensuring a smooth development experience.

Other notable examples of package management systems include:

  • RubyGems/Bundler for Ruby packages
  • Crates/Cargo for Rust libraries

Each of these systems offers similar benefits:

  • Centralized repositories for package discovery and access
  • Version control to maintain compatibility and stability
  • Dependency management to streamline project setup and updates
  • Simplified installation and updates through command-line tools

By effectively utilizing package managers, developers can tap into a rich ecosystem of reusable code, accelerate development processes, and ensure project maintainability.

4. Dealing with Errors

Go's approach requires developers to diligently check for errors after every function call. This can become repetitive and can potentially lead to oversights if not managed carefully.

err, value := someFunction();
if err != nil {
    // handle it somehow
}        

Think about creating a function that performs three tasks, each of which could potentially result in an error. It might resemble this structure:

func doSomething() (err, int) {
    err, value1 := someFunction();
    if err != nil {
            return err, nil
    }
    err, value2 := someFunction2(value1);
    if err != nil {
            return err, nil
    }
    err, value3 := someFunction3(value2);
    if err != nil {
            return err, nil
    }
    return value3;
}        
func doSomething() (err, int) {
    err, value1 := someFunction();
    if err != nil {
            return err, nil
    }
    err, value2 := someFunction2(value1);
    if err != nil {
            return err, nil
    }
    err, value3 := someFunction3(value2);
    if err != nil {
            return err, nil
    }
    return value3;
}        

The code has excessive repetition, which is a problem that's amplified in larger functions. It's going to be a lot of typing.

Code Improvement

I appreciate JavaScript's error handling mechanism, which allows functions to signal problems and enables code to gracefully address them. Here's an illustration:

function doStuff() {
    const value1 = someFunction();
    const value2 = someFunction2(value1);
    const value3 = someFunction3(value2);
    return value3;
}

try {
    const value = doStuff();
    // do something with it
} catch (err) {
   // handle the error
}        

It's much easier to understand and doesn't have redundant error handling code.

Additional notes

While I've shared some critiques of Go, I ultimately prefer it to other languages for a myriad of reasons. This just reiterates that every language has limitations, hype aside.

I appreciate you taking the time to read this article of mine.

Michael McGuiness

Full Stack Developer | React | Node.js | Next.js | MongoDB

1 年

When I transitioned from C# to JavaScript, I had no idea there were languages like Go and JS that didn't have efficient function overloading!

要查看或添加评论,请登录

David Segun的更多文章

社区洞察

其他会员也浏览了