Go

Go 1.13 优缺点整理

Posted by Fioncat on March 8, 2020

Go 1.14更新:

Module support in the go command is now ready for production use. We encourage all users to migrate to go modules for dependency management.

好消息呀!Go module终于被官方推荐使用在生产中了。官方也建议将项目迁移到Go module下管理。Go被诟病了多年的包管理问题终于解决了(似乎?)。

但是泛型功能什么时候才能来呢😂。。

Go语言已经被很多大型企业使用在其基础设施的开发了。作为一个新生语言,为什么它有这么大的魅力呢?下面我整理了网上各位大佬的观点,配合我自己对Go的理解,来总体介绍一下这门语言的优缺点(主要以C/C++、Java、Python作为比较对象)。

如果你是C/C++、Java、Python的开发者,可以完全看懂这篇文章。快来和我一切速览Go语言吧!

我这里是以go 1.13作为基准的。

本文不是Go语言教程,不介绍语言细节,只是粗略地展示一下Go语言。

Go优点

开发效率

在写Go的时候,你会很容易写出动态类型语言的感觉。这是一个非常好的特性,得益于Go的自动类型推断(类似于C++11的auto),大多数情况下,你可以不用关心变量的具体类型。

如果你是从Python来的程序员,会很喜欢这一点。

虽然Go的写法很有动态类型语言的感觉,但是它实际上是一个静态类型语言,这样就不会有动态语言的缺点,能在编译时检查出很多问题(动态语言只能在运行时检查出来)。

下面我们简单对比一下Go和Java声明变量的不同:

java:

1
2
3
4
5
6
// 简单变量
Integer a = 23;
// 对象
Person person = new Person();
// 通过方法获取
Something sth = createSomething();

go:

1
2
3
4
5
6
// 简单变量
a := 23
// 对象
person := Person{}
// 通过方法获取
sth := CreateSomething()

Go是不是很有Python的感觉呢?

运行效率

Go语言的运行效率是很高的。目前Go的运行效率和Java差不多,但是Go比优化了多年的Java年轻的多,因此潜力也更多。

在高并发的情况下,Go的表现会更好。因此很多企业将Go作为服务器语言,用于替换原先C++的位置。

虽然Go比C++还是要慢的,但是它的开发效率比C++实在是高上太多了,在硬件越来越便宜的今天,Go未来在服务器基础设施领域必定会占据更多市场。

少即是多(缺点?)

Go语言遵循“少即是多”的设计理念,提供更少的语言特性。这会让Go语言显得不那么“臃肿”。特别是OOP,Go语言在OOP上更像C语言,将OOP神秘的面纱揭示得一干二净。在Go中,仅提供了结构体、组合等少数几个功能,没有直接提供继承等功能,OOP只是一个语法糖。

甚至对于封装,Go语言也只是一个命名的事而已(首字母大写即为public,小写为private)。

下面我们用Java和Go进行一个简单的对比:

java:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Student {
    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void show() {
        System.out.println(this.name, this.age)
    }
}

go:

1
2
3
4
5
6
7
8
9
10
11
12
type Student struct {
    name string
    age  int
}

func NewStudent(name string, age int) Student {
    return Student{name: name, age: age}
}

func (student Student) Show() {
    fmt.Println(student.name, student.age)
}

这种一切从简的设计思路很受很多C语言程序员的喜爱,但是也有一些从高级语言转过来的人认为,Go提供的语言特性太少了;当然也有人觉得这种特性少的语言更能剖析编程的本质,写出来的代码更加有美感。

当然,给小白的好处就是,Go学习起来比其他语言简单,特别是对于C/C++程序员来说,转型做Go是很简单的。

当然,关于“少即是多”是好是坏,就仁者见仁智者见智了。

gofmt格式统一

go语言提供了很多小工具,其中最受欢迎的是gofmt。gofmt可以把任意格式的Go语言源代码统一格式化为统一的格式。

这样,有了官方指定的格式,我们终于不用为了代码格式的统一吵得焦头烂额了。直接gofmt一下即可。

并发

这是Go的一大卖点,Go是原生支持并发的,并且Go的并发单位是协程(在Go中被叫做goroutine)。

关于协程的介绍,请见:Go协程

在go中,只需要一个go关键字就可以实现启动一个协程并运行。这是一个非常棒的特性,对于其它大部分语言,都需要使用系统库来实现并发。

下面再来对比Java和Go:

java:

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

import java.lang.Runnable;
import java.lang.Thread;

class Worker implements Runnable {
    @Overried
    public void run() {
        System.out.println("run concurrent");
    }
}

public class Main {
    public static void main(String[] args) {
        Worker worker = new Worker();
        new Thread(worker).start()
    }
}

go:

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
    go func() {
        fmt.Println("run concurrent")
    }()
}

怎么样,是不是写起来比Java简单多了。

Java的juc包支持很多并发控制的工具,例如ExecutorLockCountDownLatchCyclicBarrierSemaphoreExchanger等。这些工具需要花一定时间去学习。

但是在Go中,goroutines之间的交互更多是用channel来实现,当然,Go也有提供LockWaitGroup(类似于Java的CountDownLatch)等功能。但是channel却可以完成goroutine之间同步的大部分需求(Go开发者也建议多使用channel)。这也符合Go的少即是多设计理念呢。

而且,channel也是Go内置支持的呢。

部署简单

Go的build输出的直接是可以运行的二进制文件,这就比Java简单多了。这意味我们的部署只需要简单地把一个二进制文件丢到服务器上运行即可(甚至服务器不需要安装Go环境),而如果是Java,还需要在服务器上安装一个jre。这对运维部署人员来说是个好事。

Go的部署也比C/C++简单,Go不需要什么繁琐的静态链接、动态链接的过程。所有代码仓库都会被编译到一个可执行文件中(当然,这也有可能导致Go的可执行文件比较大)

在大多数时候,只需要下面一行命令即可编译Go项目为可执行文件:

1
$ go build -o runnable main.go

go构建器会自动解决所有库、连接等问题。我们不用再去写冗长的makefile了,也不需要专门去搞个maven这样的第三方构建工具。

Go自带的库特别强大,特别是它的http库,可以满足大多数Web开发的需求了。更不用说它的jsonnettextruntime等库了。

自带map

Go语言是原生支持map的。在Java中要使用map需要进行导包,C++更不用说,光stlboost的选择就够你头痛的了。

下面我们还是拿Java和Go做个对比:

java:

1
2
3
4
5
6
7
8
9
Map<String, Object> man = new HashMap<>();

man.put("name", "Wang");
man.put("age", 18);
man.put("birthday", new Date());

System.out.println("name =", man.get("name"));
System.out.println("age =", man.get("age"));
System.out.println("birthday =", man.get("birthday"));

go:

1
2
3
4
5
6
7
8
9
man := make(map[string]interface{})

man["name"] = "Wang"
man["age"] = 18
man["birthday"] = time.Now()

fmt.Println("name =", man["name"])
fmt.Println("age =", man["age"])
fmt.Println("birthday =", man["birthday"])

这样的写法更像Python。

生态圈

Go有一个杀手级别的项目:Docker。以及Kubernetes。这两个东西的火热程度不用说大家心理已经知道。

以容器技术在未来的趋势,只要Docker不倒,Go在容器领域就会一直火热下去。

测试

Go自带了一套很好用的测试组件,Go的测试不再使用传统的assert。测试失败需要手动调用函数。

在测试失败后,Go也不会马上终止测试程序,而是会把测试程序坚持运行完毕。

Go的测试远不如此,Go支持并行测试、基准测试等。对于服务器-客户端程序的测试也有支持。

关于Go的测试,这里有一个很好的教程:Go testing教程

Go缺点

吹完了Go,作为一个新生语言,Go还是有很多缺点的。许多地方甚至为人诟病。作为一个转型做Go的,我当然希望Go越来越好,所以我们需要直面这些问题。

包管理(go module大法好)

在Go 1.11以前,Go使用GOPATH对Go项目进行管理。这需要把当前项目的所有依赖放到一个vendor目录下。这对开发人员来说是一个噩梦:

  • 项目之间不能复用依赖
  • 依赖没有版本控制,涉及到依赖的版本更新、回退、多版本共存等问题时,你会感到绝望
  • 如果一个依赖引用了其它依赖,你也会绝望的

还好社区有很多工具例如vgo可以在一定程度上解决这些问题,但是还是很蛋疼。

终于,在Go 1.11以后,引入了go module,这个工具类似于Python的包管理,可以通过简单的命令来下载全局的依赖包。在项目中通过定义go.mod来引入依赖包,并且这个文件可以自动生成。

有了go module,之前Go的包管理噩梦就能在一定程度上得到解决,希望未来能得到大力推广。

错误处理

Go的错误全部是通过返回来进行的。Go并没有传统的try...catch。这意味着你在进行错误处理的时候需要写大量的这种代码:

1
2
3
4
result, err := fun()
if err != nil {
    // handle error
}

当然,Go支持函数式编程,你可以通过函数式编程的方式,通过wrapper模式来省略很多这样的代码。但是在很多业务场景,还是会很蛋疼的。

所以Go并不非常适合写业务代码,目前行业内写业务还是以Java这种语言为主。

当然,Go实际上也有实现try...catch的方式,但是比较蛋疼。我们可以对比一下:

java:

1
2
3
4
5
6
7
8
9
10
public class TestTryCatch {

    public static void main(String[] args) {
        try {
            throw new Exception();
        } catch (Exception e) {
            System.out.Println("发生了错误!");
        }
    }
}

go:

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

import "fmt"

func main() {

	defer func() {
		if err := recover(); err != nil {
			fmt.Println("产生了错误!")
		}
	}()

	panicFun()

}

func panicFun() {
	panic("我是错误")
}

通过defer+recover可以修复Go的panic,但是我还是觉得没有try...catch理解和用起来简单。

缺乏泛型

这是我个人非常讨厌的一点。作为一个静态语言,Go居然没有泛型。这就像回到了几十年前的Java一样。

如果我想做一个稍微“通用”一点的功能,就会涉及到大量的类型向下转型。我们知道,从JDK1.5开始,Java就强烈不建议使用向下转型而使用泛型,因为它确实很不安全,即使在你事先知道对象类型的时候。

然而在2020年,Go仍然在使用大量的类型向下转型。

虽然Go的设计理念是“少即是多”,Go的设计者将泛型和继承作为一个整体从Go中删去了。但是作为类型安全的一个保证,我认为只要有interface{}(类似Java中的Object)的存在,泛型就必不可少。

希望在Go未来的版本看到泛型。

缺少框架(优点?)

不像Java Spring,Go没有一个大一统的框架。光是在web框架领域,就有很多选择:

  • gin
  • beego
  • iris
  • echo

当然,这也和Go自带的库很强大有关系,很多人不喜欢框架,觉得框架限制了他们发挥的空间;也有人觉得框架能够快速开发,符合现代开发的要求。

这点就因人而异了。

缺少更多的数据结构

Go语言只提供了数组、Slice和map。对于栈、堆、队列等其它数据结构,以及并发安全的数据结构,并没有直接的支持。

我觉得这很大是因为Go没有泛型,如果要做一个通用的其它数据结构,就不得不处理interface{},这对类型安全来说是一个灾难。

所以如果我们要实现某个数据结构,只能针对自己的struct手撸了。

GC

在Go GC经过了以下的发展阶段:

  • Go 1.3之前:STW(Stop The World) 非常简陋的GC算法,在内存超过阈值或定时的条件下,暂停所有任务,执行mark+sweep(标记清除)。在高内存场景下,这意味着任务的长时间停顿,是一种灾难。
  • Go 1.3:Mark STW + Sweep。将mark和sweep分开。但是也需要暂停所有任务,但是暂停过程只进行mark,mark之后恢复其它任务,sweep通过协程异步进行。这在一定程度上减少了GC的开销,减少STW的时间。
  • Go 1.5:三色标记法。对mark进行改进,使mark可以和用户任务并发执行。这种方法的mark操作是可以渐进执行的而不需每次都扫描整个内存空间,可以进一步减少STW的时间。
  • Go 1.8:混合写屏障(hybrid write barrier)允许堆栈扫描永久地使堆栈变黑(没有STW并且没有写入堆栈的障碍),这完全消除了堆栈重新扫描的需要,从而消除了对堆栈屏障的需求。使用这种方法可以将STW的时间降低到1毫秒以下

如果你的Go程序突然出现卡顿,就可能是GC的原因,就需要花时间去优化,减少Go内存的压力。

在1.8版本以后,GC的延迟性下降了很多,但是因为需要并行处理GC,线程间同步和多余的数据生成复制都会占用实际逻辑业务代码运行的时间。因此程序的吞吐量会下降,STW可以提高程序的吞吐量。

Go GC和Java JVM相比,差距还是很大的。但是Go毕竟是一个比较年轻的语言,给它时间进行发展,未来GC一定会越来越好。

字段标记灾难

看下面一段代码,这是来自Go官方文档的一个例子:

1
2
3
4
5
6
type Test struct {
    Label         *string             `protobuf:"bytes,1,req,name=label" json:"label,omitempty"`
    Type          *int32              `protobuf:"varint,2,opt,name=type,def=77" json:"type,omitempty"`
    Reps          []int64             `protobuf:"varint,3,rep,name=reps" json:"reps,omitempty"`
    Optionalgroup *Test_OptionalGroup `protobuf:"group,4,opt,name=OptionalGroup" json:"optionalgroup,omitempty"`
}

字段标签是Go提供的一个功能,用于给Go字段进行注释。在使用json的过程中,字段标签非常重要。

因为Go中公共属性首字母是大写的,而在json标准中字段首字母一般是小写的。而Go的标准库json没有这方面的自动转换,因此需要我们手动进行标注。

如果涉及到更多协议相关的内容,则标签会更长。冗长的标签会让结构体代码的可读性下降。

不是总结的总结

总体来说,Go还是一门不错的语言的。它写起来很像Python,却有着Python远不及的性能。Go也有着很低的入门门槛。

当然,Go最吸引人的地方就是它的协程了。协程的概念让我们在编程中可以几乎随心所欲地创建并发任务而不用太多考虑开销。也让我们的并发编程变得更加简单。

在高并发领域,Go可以说是越来越受欢迎,很多公司都使用Go来构建他们的基础服务器设施。Go让C/C++程序员远离构建的痛苦,专注于开发。

Go当然还有很多缺点,在业务编写上面,为很多人诟病。和Java还有很大的差别,生态圈也不如Java丰富。Go还有很多不为人知的陷阱(可以详见我的“Go基础笔记”系列)。

希望Go在未来能够越来越好。