作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
谢尔盖·佩什科夫的头像

Sergei Peshkov

Sergei作为一个专攻Node的web应用程序的后端开发人员已经工作了三年多.. js与MondoDB/PostgreSQL.

Expertise

工作经验

7

Share

Go (a.k.a. 高朗语是人们最感兴趣的语言之一. 截至2018年4月, 它在TIOBE指数中排名第19位. 越来越多的人从PHP转向Node.js和其他语言,并在生产环境中使用它. 很多很酷的软件(比如Kubernetes、Docker和Heroku CLI)都是用Go编写的.

那么,围棋成功的关键是什么呢? 这门语言中有很多东西让它变得很酷. 但让Go如此受欢迎的主要原因之一是它的简单性, 正如它的创造者之一罗布·派克所指出的那样.

简单很酷:你不需要学习很多关键词. 它使语言学习非常容易和快速. However, 另一方面, 有时,开发人员缺乏其他语言和语言中具有的一些特性, therefore, 从长远来看,他们需要编写变通方法或编写更多代码. 不幸的是,Go在设计上缺乏很多特性,有时真的很烦人.

Golang是为了加快发展, 但在很多情况下, 您编写的代码比使用其他编程语言编写的代码要多. 我将在下面的Go语言评论中描述一些这样的情况.

4 Go语言批评

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

我将在这里发布一个真实的代码示例. 当我在做Golang的Selenium绑定时, 我需要写一个有三个参数的函数. 其中两个是可选的. 下面是实现后的样子:

function (wd *remoteWD) WaitWithTimeoutAndInterval(condition条件,超时时间,间隔时间.持续时间)错误{
    //实际的实现在这里
}

function (wd *remoteWD) WaitWithTimeout(condition条件,超时时间.持续时间)错误{
    return wd.WaitWithTimeoutAndInterval(条件,超时,DefaultWaitInterval)
}

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

我必须实现三个不同的函数,因为我不能重载函数或传递默认值——go在设计上没有提供这些功能. 想象一下,如果我不小心打错了电话会发生什么? 这里有一个例子:

我会得到一堆

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

如何改进?

下面是JavaScript中的相同(好吧,几乎相同)示例:

function Wait (condition, timeout = DefaultWaitTimeout, interval = DefaultWaitInterval) {
    //实际实现在这里
}

如你所见,它看起来清晰多了.

我也喜欢Elixir的方法. 这是Elixir中的效果(我知道我可以使用默认值), 就像上面的例子一样,我只是把它作为一种可行的方法来展示):

defmodule服务员做什么
@default_interval 1
        @default_timeout 10

    Def wait(condition, timeout, interval)
            //在这里实现
    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.等待(“条件”,2)
Waiter.等待(“条件”)

2. Lack of Generics

这可以说是Go用户最需要的功能.

假设您想要编写一个映射函数, 你在哪里传递整数数组和函数, 将应用于它的所有元素. 听起来很简单,对吧??

让我们对整数做一下:

package main

import "fmt"

函数mapArray(arr []int,回调函数func (int) (int)) []int {
    newArray:= make([]int, len(arr))
    对于索引,value:= range {
     newArray[index] = callback(value)
    }
    
    return newArray;
}

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

Looks good, right?

想象一下,你也需要对字符串这样做. 您需要编写另一个实现,除了签名之外,它完全相同. 这个函数需要一个不同的名称,因为Golang不支持函数重载. As a result, 你会有一堆相似的函数,但名字不同, 它看起来是这样的:

函数mapArrayOfInts(arr []int,回调函数func (int) (int)) []int {
    // implementation
}

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

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

这绝对违背了DRY(不要重复自己)原则, 哪个声明你需要写尽可能少的复制/粘贴代码,而不是将其移动到函数中并重用它们.

缺少泛型意味着有数百种不同的函数

另一种方法是使用单个实现 interface{} as a parameter, 但这可能导致运行时错误,因为运行时类型检查更容易出错. 而且它会更慢,所以没有简单的方法来实现这些函数.

如何改进?

有很多优秀的语言都包含了对泛型的支持. 例如,下面是我在Rust中使用的相同代码 vec instead of array 简单点说):

fn map(vec:Vec, callback:fn(T) -> T) -> Vec {
    让mut new_vec = vec![];
    对于vec{中的值
            new_vec.推动(回调(值));
    }
    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::(int_vec, square)); // prints [1, 4, 9, 16, 25]

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

注意,只有一个实现 map 函数,它可以用于您需要的任何类型,甚至是自定义类型.

3. 依赖关系管理

任何有Go经验的人都会说依赖管理真的很难. Go工具允许用户通过运行安装不同的库 go get . 这里的问题是版本管理. 如果库维护者做了一些向后不兼容的更改并将其上传到GitHub, 任何试图在此之后使用您的程序的人都会得到一个错误, because go get does nothing but git clone 将存储库放入库文件夹中. 此外,如果没有安装库,程序将因此无法编译.

你可以通过使用Dep来管理依赖关系(http://github.com/golang/dep), 但这里的问题是,您要么将所有依赖项存储在存储库中(这并不好), 因为您的存储库不仅包含您的代码,还包含成千上万行依赖代码), 或者只是存储包列表(但是再次, 如果依赖项的维护者进行了向后不兼容的更改, 一切都会崩溃).

如何改进?

我认为Node就是最好的例子.js(和JavaScript一般,我想)和NPM. NPM是一个包存储库. 它存储不同版本的包, 所以如果你需要某个包的特定版本, 没问题,你可以从那里拿到. 同样,在任何Node中.. js/JavaScript应用程序是 package.json file. Here, 列出了所有依赖项及其版本, 因此,您可以安装它们(并获得与您的代码一起工作的版本) npm install.

此外,包管理的好例子还有RubyGems/Bundler(用于Ruby包)和Crates.io/Cargo(用于Rust库).

4. Error Handling

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

err, value:= someFunction();
if err != nil {
    //处理它
}

现在假设您需要编写一个函数,该函数执行三个返回错误的操作. 它看起来像这样:

函数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在这方面的方法. 函数可以抛出错误,而您可以捕获它. 考虑一下这个例子:

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

try {
    const value = doStuff();
    //用它做点什么
} catch (err) {
   //处理错误
}

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

Go中的好东西

尽管Go在设计上有很多缺陷,但它也有一些非常酷的功能.

1. Goroutines

在Go中,异步编程变得非常简单. 而多线程编程在其他语言中通常是困难的, 生成一个新线程并在其中运行函数,这样它就不会阻塞当前线程,这真的很简单:

函数doSomeCalculations() {
    //执行一些CPU密集型/长时间运行的任务
}

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

2. 与Go捆绑的工具

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

  • gofmt -静态代码分析工具. 与JavaScript相比,JavaScript需要安装额外的依赖项,比如 eslint or jshint,这里是默认包含的. 如果你不编写go风格的代码(不使用声明的变量),程序甚至无法编译, 导入未使用的包, etc.).
  • go test -测试框架. Again, 与JavaScript相比, 您需要为测试安装额外的依赖项(Jest), Mocha, AVA, etc.). 这里,它是默认包含的. 默认情况下,它允许你做很多很酷的事情, 比如基准测试, 将文档中的代码转换为测试, etc.
  • godoc -文档工具. 很高兴将其包含在默认工具中.
  • 编译器本身. 与其他编译语言相比,它的速度非常快!

3. Defer

我认为这是语言中最好的特性之一. 假设您需要编写一个打开三个文件的函数. 如果某些操作失败,则需要关闭已打开的文件. 如果有很多这样的建筑,就会看起来一团糟. 考虑以下伪代码示例:

函数openManyFiles() {
    让file1, file2, file3;
    try {
        File1 = open(' path-to-file1 ');
    } catch (err) {
        return;
    }

    try {
        File2 = open(' path-to-file2 ');
    } catch (err) {
        //我们需要关闭第一个文件,记住?
        close(file1);
        return;
    }

    try {
        File3 = open(' path-to-file3 ');
    } catch (err) {
        //现在我们需要关闭第一个和第二个文件
        close(file1);
close(file2);
        return;
    }

    //对文件执行一些操作

    //处理成功后关闭文件
    close(file1);
    close(file2);
    close(file3);
    return;
}

看起来很复杂. Go就在那里 defer 就位:

package main

import (
    "fmt"
)

函数openFiles() {
    //假装正在打开文件
    fmt.Printf("打开文件1\n");
    defer fmt.Printf("正在关闭文件1\n");
    
    fmt.Printf("打开文件2\n");
    defer fmt.Printf("正在关闭文件2\n");
    
    fmt.Printf("打开文件3\n");
    //假装文件打开错误
    //在实际产品中,这里将返回一个错误.
    return;
}

func main() {
    openFiles()

    /* Prints:

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

    */

}

As you see, 如果打开第三个文件时出现错误, 其他文件将自动关闭, as the defer 语句在按相反顺序返回之前执行. Also, 在同一位置打开和关闭文件,而不是在函数的不同部分打开和关闭文件,这很好.

Conclusion

我并没有提到围棋的所有优点和缺点, 只有我认为最好和最坏的东西.

Go确实是当前使用的有趣的编程语言之一, 它确实有潜力. 它为我们提供了非常酷的工具和功能. 然而,还有很多事情可以改进.

If we, as Go developers, 将实现这些更改, 这将使我们的社区受益匪浅, 因为它会让用Go编程变得更加愉快.

与此同时,如果你想用Go来改进你的测试,那就试试 测试你的Go应用:以正确的方式开始 由Toptaler同事Gabriel Aszalos撰写.

了解基本知识

  • Go是一种脚本语言吗?

    脚本和程序的定义只有一线之隔, 但我得说它不是脚本语言, 因为Go程序不是在运行时运行的——它们是作为可执行文件编译和运行的.

  • Is Go perfect?

    No. Go is great, 它改善了开发者的体验, 但它并不完美, 正如我在本文中所描述的那样. 它可能永远不会完美,但我相信我们可以让它接近完美.

就这一主题咨询作者或专家.
Schedule a call
谢尔盖·佩什科夫的头像
Sergei Peshkov

Located in 沃罗涅日,沃罗涅日州,俄罗斯

Member since February 20, 2018

About the author

Sergei作为一个专攻Node的web应用程序的后端开发人员已经工作了三年多.. js与MondoDB/PostgreSQL.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

工作经验

7

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.