Go 语言之美

前言

如遇排版问题,请点击 阅读原文

我最近由于一些私活开始了对 Go 语言的探索,在这个过程中,我真的被 Go 语言的美妙震惊到了!

gologo.png

我发现,他在易用性(相比于动态类型和解释性语言)、性能和安全(类型安全、内存安全)(相比于静态类型与编译型语言)之间取得了一个很好的平衡。

除此之外,还有两个属性让它更加适合现代程序开发。由于篇幅略长,我们将在下面进行分别讲解:

这两个特殊属性之一,便是在语言层面上优先支持并发。并发,在它设计上,允许你更充分地发挥 CPU 的性能,即使你的是单核 CPU,也能够高效地使用。这也是你能在单台机器上拥有成千上万 goroutines(go 协程)的原因。Channel(通道)和 goroutines 是基于 生产者与消费者模型 的核心发布系统。

另一个我真的很喜欢的属性便是 interface(接口)。Go 语言的 interface 定义非常宽松,则允许你能够为你的系统定义耦合或者非耦合的组件。这意味着,你的一部分代码完全可以仅仅依赖于 interface 且并不需要关心是谁在使用或者怎么使用这个 interface。你的控制器只需要满足这个 interface 的依赖即可使用了(满足这个 interface 中的所有函数)。这也让我们有了一个非常整洁的代码结构,便于以后的单元测试(只需要依赖注入即可)。现在,你的控制器只需要注入一个模拟的依赖到该代码的 interface,就能够测试这段代码的运行情况了。

牢记这些特性,我觉得 Go 语言是一个非常伟大的语言。尤其是在云服务系统(Web 服务器、CDN 服务器、缓存服务器等)、分布式系统、微服务等。因此,如果你是一个工程师或者正在寻找一门语言来学习,Go 值得你尝试!

在这篇文章中,我会谈到 Go 语言以下几个方面的内容:

  1. Go 语言简介
  2. 为什么需要 Go 语言
  3. 目标用户
  4. Go 语言的优势
  5. Go 语言的不足
  6. 迎接 Go 语言 2 版本
  7. Go 的设计哲学
  8. 如何开始
  9. 谁在使用 Go

1. Go 语言简介

Go 语言是一门开源的语言,由 Google 的 Robert Griesemer,Rob Pike 以及 Ken Thompson 创建。开源意味着任何人都能够依照开源协定来打造 Go 语言,为其增加新功能,修复 Bug 等等。其源码可以在 Github 上获得 golang/go,关于如何参与贡献的文档在这里

2. 为什么需要 Go 语言

该语言的作者们提到说:设计 Go 这门新语言的目的是为了解决 Google 在软件开发中遇到的一些问题。他们同样表示,设计 Go 是为了能在 C++ 之外有另一个选择。

Rob Pike 表示,设计 Go 语言的意图在于:

“Go’s purpose is therefore not to do research into programming language design; it is to improve the working environment for its designers and their coworkers. Go is more about software engineering than programming language research. Or to rephrase, it is about language design in the service of software engineering.”

Go 的设计意图不在于对编程语言的设计做研究,而是为了改善它的设计者与同事之间的工作环境而已。Go 更多的是为了工程需要而不是专注于语言研究。换句话说,它的设计是为软件工程服务的。

来自 Go at Google中描述的问题:

  1. 极慢的构建,有的时候会花掉一个多小时去构建项目;
  2. 不可控的依赖;
  3. 每个程序员的语言栈不同;
  4. 极差的代码可读性,难以理解的代码、劣质的文档等等;
  5. 重复的工作;
  6. 语言版本升级代价高;
  7. 版本分支混乱;
  8. 难以编写自动化工具;
  9. 跨语言构建;
Go 想要成功,则必须要解决这些问题:
  1. Go 必须要是有条理的。以应对大的团队协作以及繁多的依赖管理;
  2. Go 必须要是熟悉的。大致上和 C 差不多。Google 需要编程人员尽快地适应 Go 语言,这意味着,它的语法不能太过于激进;
  3. Go 必须要够现代。它应该具有像并发这样的特性,这样才能够最大限度发挥多核处理器的性能。它应当内建网络和 Web 服务器的库,这样他才能有助于现代化的软件开发。

3. 目标用户

Go 是一门系统编程语言。Go 非常适合在云服务器系统(Web Server、缓存服务器等)、微服务、分布式系统(基于并发的支持)上应用。

4. Go 语言的优势

  1. 静态类型:Go 是静态类型语言。这意味着,在编写的过程中,你需要为你所有的变量以及函数的参数和其返回值指定具体的类型。尽管这听起来不太方便,但是这极大地避免了错误的发生,很多错误都能够在编译的时候被发现。随着你团队的增大,这个特性将会愈来愈重要,因为静态类型能让你的函数或者库更易读也更容易被理解;
  2. 编译速度:Go 的代码编译真的很快,所以,你根本无需为编译你的代码而等待。事实上,当你使用 go run 这段代码运行你程序的时候,你几乎感受不到这个程序还要经过编译!使用起来就像解释型语言;
  3. 执行速度:根据不同的运行环境(OS:Linux/Windows/MacOS,CPU 类型:x86/x86-64/arm等),Go 语言的代码最终是被编译为了机器码。所以,它运行得非常快;
  4. 可移植性:因为是直接编译为的机器码,我们得到的就是最终的二进制文件,所以它的可移植性非常的好。例如,你可以将你电脑(假设是 Linux,x86-64)中编译的二进制代码直接复制到服务器(假设也是 Linux,x86-64)上运行。
    因为 Go 是静态链接的,所谓静态链接,就是 Go 在编译的时候,会将所有用到的库统一打包进最终的二进制文件,并不会共享系统的任何运行库。这就让他在运行的时候不再依赖当前操作系统库(比如像平时在 Windows 上运行程序经常会出现各种库文件找不到的情况)。
    这个特性在你部署大量服务器时会产生巨大的作用。假设你数据中心有 100 台服务器需要部署,你仅仅只需要用 scp 这个命令将二进制文件自动拷贝到所有服务器(二进制文件是对应这些平台编译的),直接运行就行了。你不用再关心它们运行的是哪个版本的 Linux,无须检查依赖,直接运行二进制程序即可,一切就都搞定了;
  5. 并发:Go 最优先地支持了并发,并发是 Go 语言主要的卖点之一。它的设计者根据 Tony Hoare 的论文 Communicating Sequential Processes 设计了 Go 的并发模型。
    Go 的协程允许你在单台机器上拥有成千上万的 goroutine。一个 goroutine 是一个极其轻量的可执行线程,Go 能够在系统线程之上调度这些协程运行,这意味着,大量的 goroutine 可以并发的在单个系统线程中运行。
    这个方式有两个好处:

    • 一个 goroutine 在被创建的时候只有 4KB 的栈大小,这比起系统线程的 4MB 来说真的很小。这在你有成千上万 goroutine 的时候变得至关重要。如果你想运 行成千个系统线程,那里的 RAM 将会成为瓶颈。
    • Go 可以遵循和 Java 一样的模型,支持和系统线程一样的上下文环境。但是,在不同系统线程上下文之间切换的开销比在不同 goroutine 上下文切换的开销大多了。

至此,我在文中已经多次提到了并发这个概念,我建议你去看一下 Rob Pike 的演讲并发并不是并行 - YouTube。在编程中,并发是指一组互不相关的处理任务队列,而并行则是同时执行的一组任务,这些任务有可能相关。除非你有一个多核的 CPU 或者有多个 CPU,你无法达到真正的并行,因为一个 CPU 在任何时间点只能执行一个任务。在单核环境中,在后台环境中,只有并发。系统通过调度处理队列(通常是调度线程,每个进程都至少会有一个主线程),在每个 CPU 时间片中执行。所以,在任何时间点,你只可能有一个线程(进程)在处理器中执行。只是由于这些任务都切换得非常快(每秒能发生成千上万次调度),所以感觉这些任务都是在同时执行的,但事实上他们真的只是排着队在依次执行而已;

  1. Interface 接口:接口使系统之间的耦合变得更加宽松。在 Go 中,一个 interface 可以定义一组函数。就这么简单。任何实现了这组函数功能的类型就实现了这个 interface,换句话说,你并不需要显性地指明这个类型实现了这个接口。这些都由编译器在编译的过程中自动检测的。
    这意味着,你的某些代码中只需要依赖于一个接口,而不用关心是谁实现了它,或者怎么实现的。你的主函数或者控制器会提供一个依赖给这个接口(实现它里面的所有方法)给这段代码。这也让你的整个项目结构更加清晰整洁,也更有利于单元测试(通过依赖注入)。现在你的测试函数只需要注入一个模拟的接口实现,便可以检测函数是否正确运行。
    这对于解耦来说当然就是利器,还有一个好处就是,你能按照微服务的思考方式来构建你的软件结构(即使你只有一台服务器或者你刚开始着手做这个事情)。你可以将你软件中的依赖想像成微服务,这些微服务都对应实现某个接口(按照它定义的)。这样,其他服务或者控制器就只需要按照 interface 调用你的方法即可,并不需要关心这背后是如何实现的;
  2. 垃圾回收:不像 C,在 Go 中,你不需要时刻记住释放指针,也不用担心错误的 dangling pointers 悬垂指针,垃圾回收器会自动帮你完成这些工作;
  3. 没有异常,手动管理 Errors:我喜欢这个功能,Go 并没有像其他语言具有的标准异常处理逻辑。Go 强制要求开发者自己处理基础的错误,比如“不能打开文件”等,而不是通过 try{}catch(){} 来处理,这也强制开发者去思考每个错误后应该怎样去处理。
  4. 绝赞的工具:Go 语言最好的方面之一便是它的工具生态。它有以下的工具:

    • Gofmt:它会自动格式化你的代码文件,按照标准格式。所以能够实现地球上所有程序员开发出来的 Go 代码都是同样的风格。这对代码可读性的提升有至关重要的作用。
    • Go run:这个代码直接编译并运行你的代码。所以,即使 Go 是需要编译才能运行的语言,他也能让你感觉这就像是在用解释型语言一样(它的编译太快了)。
    • Go get:这个工具会自动从 Github 下载库,并拷贝到你的 GoPath 目录下,你就直接能够引用并运行了。
    • Godoc:Godoc 能够解释你的源代码(包含注释),然后将它们输出为 HTML 文件或者简单的 txt 文件。通过 Godoc 的 Web 接口,你就可以看文档所对应的那段代码了。你可以直接点击一个方法名,然后打开这个方法对应的文档。

你可以在这里找到更多的工具

  1. 绝佳的内建库:Go 有大量的内建库,以适应现代编程所需。比如下面这些:

    • net/http:提供 HTTP 客户端与服务端的实现
    • database/sql:用作连接数据库
    • encoding/json:处理 json 内容
    • html/templates:HTML 模版库
    • io/ioutil:提供 I/O 所需的一些方法实现

还有很多工具以及库能够在这里找到。

5. Go 语言的不足

  1. 缺少泛型:泛型能让我们在定义数据结构的时候不用事先传入数据类型,在后面的使用中再传入。比如,你写了一个给列表中 int 数据排序的程序,一会儿,你又需要写一个给 string 数据排序的函数。第一反应你会觉得,这两个函数其实很多都是一样的,但是你却不能使用之前那个函数,因为它只能处理 int 列表,而不能处理 string 列表。这就会导致写重复的代码。因此,泛型允许你定义一个数据结构,里面的数据类型可以稍后确定。所以,上面的例子你就可以这样实现:先定义一个排序方法,数据的类型用 T 代替,然后你就能代入 int/string/任何 类型到同一个函数中去,对应每个类型都会有独自的比较函数,编译器在编译的时候就能检测对应的类型,然后使用该类型的比较函数进行比较即可。

    其实能够通过声明一个空的 interface(interface{})来在 Go 中实现一些泛型的结构。然而,这并不是理想的使用方式。

    泛型这个特性也是最具争议的话题。一些开发者发誓要将其添加进 Go,但另一些则表示,不添加泛型是考虑到编译速度和执行速度而做的权衡。

    之前有说,Go 的作者们有意向在 Go 中实现一个泛型的机制。然而,这并不是泛型,泛型要能够与其他所有特性协调工作才有可能被实现。我们可以等等看 Go 2 版本中会不会针对这个特性有解决方案。

  2. 缺少完整的依赖管理:Go 1 版本承诺了,它的所有 API 在 1 版本这个生命周期中都不会改变,这意味着,你的源码能够同时工作在 Go 1.5 或者 Go 1.9。在此之后,大部分的三方插件都遵循了这个承诺。我们获得三方库主要是通过 go get 这个命令工具,因此,当你执行 go get github.com/vendor/library 的时候,你会更希望最新代码的 API 并没有发生改变。虽然大多数三方库的开发者都承诺不会改变 API,这在平时开发中倒是很不错,但是对于产品来说,这并不是一个理想的方式。

    我们需要有一个理想的依赖版本控制功能,只有这样,你才能简单地引入指定版本的三方库到你项目中。即使它们的 API 改变了,你也不用担心,因为新的 API 改变肯定会是新的版本。你可以稍后去查看他哪些地方改变了,然后决定是否要更新到最新的版本(升级这意味着需要对之前的代码做相应改动以适应最新的 API)。

    Go 官方实现库 dep 很理想地解决了这个问题,不过要等正式发布可能会到 Go 2 去了。

6. 迎接 Go 语言 2 版本

我非常喜欢 Go 语言的作者通过开源这种方式发布语言。如果你想在 Go 2 中添加新功能,你需要将你的请求写成一个文档:

  1. 描述你的使用场景或者问题
  2. 阐述为什么你不能使用 Go 实现这个功能或解决这个问题
  3. 描述这个问题的影响(一些影响较小的问题可能会被排在最后来解决)
  4. 可选,针对上面的问题提出自己的解决方案

作者会查看你的文档,并将其放到这里。所有的讨论都会通过公开的媒体展示出来,比如邮件列表或者 Github issue。

两个亟待解决的问题,我认为就是泛型依赖管理。依赖管理更像是一个发布工程或者工具问题。希望 dep(官方实现)能够成为正式的解决方案。基于该语言的作者是抱着开放的态度,我很好奇等到泛型实现的时候,会对编译速度与执行速度有什么影响。

7. Go 的设计哲学

Rob Pike 的 PPT 简单即复杂 中有一些思想让我眼前一亮。

尤其是,下面这些:

  • 基本上,其他语言仅仅只是想不断的为其增加新特性。确实,几乎所有的语言都是不断的增加新功能,让其变得臃肿,编译器也越来越复杂,语言的使用手册也是一样。长此以往,所有的编程语言都会变得一样,因为他们一直在增加自身没有的功能。想想,JavaScript 增加面向对象的功能。Go 的作者刻意地控制着不让太多功能增加到这门语言上。只有所有作者都一致同意并且真正能带来价值的功能才会被添加进 Go(前提是 Go 能够实现这个功能)。
  • 如果当前所有解决方案空间是一个维度,那新功能则是与其正交的另一个维度。真正重要的是,你如何选取并组合这些不同的向量来解决你的问题。并且,这些向量与向量都很自然地工作在一起。意味着,所有的属性都能够非常融洽的结合。只有这样,这些属性向量才能够支撑起整个解决方案空间。引入所有这些功能,并且要能够让他们协调地运行,这对于整个系统的实现来说,带来更高的复杂度。但是这个语言将一切复杂的东西都做了抽象,只留给一个简单的、易于理解的接口给用户。因此,简洁其实更是一种隐藏复杂的艺术。
  • 可读性的重要性常常被低估。可读性其实是批判地、有争议的,它是设计编程语言中最重要的因素之一,因为它和软件的维护成本息息相关。太多的特性会降低语言的可读性。
  • 可读性通常也意味着可靠性。如果一个语言很复杂,你必须要了解很多知识才能读并用这个语言工作(调试甚至是修复程序)。这也意味着,你团队中的新成员在能够开始编码前,需要花费更多的时间来学习、理解。

8. 如何开始

image.png

你可以下载并依照着安装说明来在你的电脑上安装 Go 编程环境。由于 Go 在中国真的很火爆,但是它的官网由于某些原因在中国很难打开,所以 Google 专门为 Go 做了中国专用官网,在这里

你可以通过学习板块,输入上面给出的命令,在本地搭建一个 Go 语言实时学习环境,也可以点击 Github Go Learn 来寻找一个你最喜欢的学习地址,Go by Example 很不错。

如果你想找本相关的书学习,The Go Programming Languag 是不错的一本。

至于更多的资料,援引知乎的文章系统学习GO,推荐几本靠谱的书?

9. 谁在使用 Go

大量的公司都在积极尝试使用 Go。这里是一些名气比较大的:

在你 Go 之前

欢迎点赞? 以及回复交流。如果本文有显示不正确的地方,可以转移到我的博客Go 语言之美查看。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值