4个对Go语言批评:优点和缺点

本文概述

Go(又称Golang)是人们最感兴趣的语言之一。截至2018年4月, 它在TIOBE索引中排名第19位。越来越多的人从PHP, Node.js和其他语言切换到Go, 并在生产中使用它。使用Go编写了很多很棒的软件(例如Kubernetes, Docker和Heroku CLI)。

那么, Go成功的关键是什么?语言中有很多东西使它真正酷。但是让Go如此受欢迎的主要因素之一就是它的简单性, 正如其创建者之一Rob Pike所指出的那样。

简单很酷:你不需要学习很多关键字。它使语言学习变得非常轻松快捷。但是, 另一方面, 有时开发人员缺少某些其他语言所具有的功能, 因此, 从长远来看, 他们需要编写变通办法或编写更多代码。不幸的是, Go在设计上缺少很多功能, 有时确实很烦人。

Golang的目的是加快开发速度, 但是在许多情况下, 你编写的代码比使用其他编程语言编写的代码更多。我将在下面的Go语言批评中描述一些此类情况。

4个Go语言的批评

1.缺少函数重载和参数的默认值

我将在此处发布一个真实的代码示例。在研究Golang的Selenium绑定时, 我需要编写一个具有三个参数的函数。其中两个是可选的。实施后的外观如下:

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)
}

我必须实现三个不同的功能, 因为我不能只是重载该功能或传递默认值-Go并不是设计提供的。想象一下, 如果我不小心打错电话会发生什么?这是一个例子:

我会得到一堆`undefined`

我不得不承认, 有时函数重载会导致代码混乱。另一方面, 因此, 程序员需要编写更多的代码。

如何改进?

这是JavaScript中相同的示例(几乎相同):

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

如你所见, 它看起来更加清晰。

我也喜欢Elixir的方法。这是Elixir的外观(我知道我可以使用默认值, 就像上面的示例一样, 我只是将其显示为可以完成的方式):

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.缺少泛型

可以说, 这是Go用户要求最多的功能。

假设你要编写一个映射函数, 在该函数中传递整数数组和该函数, 该函数将应用于所有元素。听起来很简单, 对吧?

让我们对整数进行操作:

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]
}

看起来不错吧?

好吧, 假设你还需要对字符串进行处理。你需要编写另一个实现, 除了签名完全相同。由于Golang不支持函数重载, 因此该函数将需要使用其他名称。结果, 你将拥有许多具有不同名称的相似函数, 看起来像这样:

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
}

这绝对违背了DRY(请勿重复自己)原则, 该原则指出, 你需要编写尽可能少的复制/粘贴代码, 而是将其移至函数中并重用它们。

缺少泛型意味着数百种变体功能

另一种方法是使用将interface {}作为参数的单个实现, 但这会导致运行时错误, 因为运行时类型检查更容易出错。而且它会更慢, 因此没有简单的方法可以将这些功能实现为一体。

如何改进?

有很多好的语言都包含泛型支持。例如, 这是Rust中相同的代码(为了简化起见, 我使用了vec而不是数组):

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_"]
}

请注意, 地图功能只有一个实现, 可用于你需要的任何类型, 甚至是自定义类型。

3.依赖管理

任何有Go经验的人都可以说依赖管理真的很困难。 Go工具允许用户通过运行go get <library repo>安装不同的库。这里的问题是版本管理。如果库维护者进行了一些向后不兼容的更改并将其上传到GitHub, 那么在此之后尝试使用你的程序的任何人都会遇到错误, 因为go get除了将git克隆到库文件夹外什么都没有。同样, 如果未安装该库, 则由于该原因该程序将无法编译。

你可以使用Dep来管理依赖项(https://github.com/golang/dep)来做得更好, 但是这里的问题是你要么将所有依赖项都存储在存储库中(这不好, 因为存储库会不仅包含你的代码, 还包含成千上万行依赖关系代码), 或者仅存储程序包列表(但同样, 如果依赖关系的维护者进行了向后不兼容的更改, 则全部崩溃)。

如何改进?

我认为这里的完美示例是Node.js(我想应该是JavaScript)和NPM。 NPM是一个软件包存储库。它存储了不同版本的软件包, 因此, 如果你需要特定版本的软件包, 则没问题, 你可以从那里获取。另外, 任何Node.js / JavaScript应用程序中的一件事是package.json文件。在这里, 列出了所有依赖项及其版本, 因此你可以使用npm install将它们全部安装(并获取绝对适用于你的代码的版本)。

另外, 包管理的好例子是RubyGems / Bundler(对于Ruby包)和Crates.io/Cargo(对于Rust库)。

4.错误处理

Go中的错误处理非常简单。在Go中, 基本上, 你可以从函数返回多个值, 并且函数可以返回错误。像这样:

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

现在想象你需要编写一个函数, 该函数执行三个返回错误的操作。它看起来像这样:

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;
}

这里有很多可重复的代码, 这不好。而具有大型功能, 情况可能更糟!为此, 你可能需要键盘上的一个键:

键盘上错误处理代码的幽默形象

如何改进?

我喜欢JavaScript的方法。该函数可能会引发错误, 你可以捕获它。考虑示例:

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
}

它更加清晰, 并且不包含可重复的错误处理代码。

Go中的优点

尽管Go在设计上存在许多缺陷, 但它也具有一些非常酷的功能。

1. Goroutines

异步编程在Go中变得非常简单。虽然在其他语言中通常很难进行多线程编程, 但是产生一个新线程并在其中运行函数以使其不会阻塞当前线程确实非常简单:

func doSomeCalculations() {
    // do some CPU intensive/long running tasks
}

func main() {
    go doSomeCalculations(); // This will run in another thread;
}

2.与Go捆绑在一起的工具

在其他编程语言中, 你需要为不同的任务(例如测试, 静态代码格式等)安装不同的库/工具, 而默认情况下, Go中已经包含了许多很酷的工具, 例如:

  • gofmt-静态代码分析工具。与JavaScript相比, 你需要在其中安装其他依赖项(例如eslint或jshint), 默认情况下已将其包含在此处。如果你不编写Go风格的代码(不使用声明的变量, 导入未使用的包等), 则该程序甚至都不会编译。
  • 去测试-一个测试框架。同样, 与JavaScript相比, 你需要安装其他依赖项以进行测试(Jest, Mocha, AVA等)。在这里, 它是默认包含的。而且它默认允许你做很多很酷的事情, 例如基准测试, 将文档中的代码转换为测试等。
  • godoc-一个文档工具。最好将其包含在默认工具中。
  • 编译器本身。与其他编译语言相比, 它的速度非常快!

3.推迟

我认为这是该语言中最好的功能之一。假设你需要编写一个打开三个文件的函数。而且如果失败, 则需要关闭现有打开的文件。如果有很多这样的结构, 看起来会很混乱。考虑以下伪代码示例:

function openManyFiles() {
    let file1, file2, file3;
    try {
        file1 = open(‘path-to-file1’);
    } catch (err) {
        return;
    }

    try {
        file2 = open(‘path-to-file2’);
    } catch (err) {
        // we need to close first file, remember?
        close(file1);
        return;
    }

    try {
        file3 = open(‘path-to-file3’);
    } catch (err) {
        // and now we need to close both first and second file
        close(file1);
close(file2);
        return;
    }

    // do some stuff with files

    // closing files after successfully processing them
    close(file1);
    close(file2);
    close(file3);
    return;
}

看起来很复杂。这就是Go的延期生效的地方:

package main

import (
    "fmt"
)

func openFiles() {
    // Pretending we’re opening files
    fmt.Printf("Opening file 1\n");
    defer fmt.Printf("Closing file 1\n");
    
    fmt.Printf("Opening file 2\n");
    defer fmt.Printf("Closing file 2\n");
    
    fmt.Printf("Opening file 3\n");
    // Pretend we've got an error on file opening
    // In real products, an error will be returned here.
    return;
}

func main() {
    openFiles()

    /* Prints:

    Opening file 1
    Opening file 2
    Opening file 3
    Closing file 2
    Closing file 1

    */

}

如你所见, 如果在打开第三个文件时遇到错误, 则其他文件将自动关闭, 因为defer语句在返回之前以相反的顺序执行。另外, 最好在同一位置而不是在函数的不同部分打开和关闭文件。

总结

我没有提到Go中所有好的和坏的事情, 只是我认为最好和最坏的事情。

Go确实是当前使用的有趣的编程语言之一, 并且确实具有潜力。它为我们提供了非常酷的工具和功能。但是, 有很多地方可以改进。

如果我们作为Go开发人员能够实现这些更改, 那么它将使我们的社区受益匪浅, 因为它将使使用Go进行编程变得更加愉快。

同时, 如果你想使用Go改善测试, 请尝试测试你的Go应用:srcminier Gabriel Aszalos的同伴入门。

相关:结构良好的逻辑:Golang OOP教程

微信公众号
手机浏览(小程序)
0
分享到:
没有账号? 忘记密码?