《Go语言精进之路》读书笔记 | 理解Go语言的设计哲学

Posted COCOgsta

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Go语言精进之路》读书笔记 | 理解Go语言的设计哲学相关的知识,希望对你有一定的参考价值。

书籍来源:《Go语言精进之路:从新手到高手的编程思想、方法和技巧》

一边学习一边整理读书笔记,并与大家分享,侵权即删,谢谢支持!

附上汇总贴:《Go语言精进之路》读书笔记 | 汇总_COCOgsta的博客-CSDN博客


究竟是什么让大量的开发人员开始学习Go语言或从其他语言转向Go语言呢?笔者认为,Go语言的魅力就来自Go语言的设计哲学。

笔者将根据自己对他们以及Go社区主流观点和代码行为的整理、分析和总结,列出4条Go语言的设计哲学。

3.1 追求简单,少即是多

当我们问Gopher“你为什么喜欢Go语言”时,排名靠前而又占据多数的总是“简单”(Simplicity)。

Go的设计者们在语言设计之初就拒绝走语言特性融合的道路,而选择了“做减法”,选择了“简单”,他们把复杂性留给了语言自身的设计和实现,留给了Go核心开发组自己,而将简单、易用和清晰留给了广大Gopher。

Go设计者推崇“最小方式”思维,即一件事情仅有一种方式或数量尽可能少的方式去完成,这大大减少了开发人员在选择路径方式及理解他人所选路径方式上的心智负担。

此外,Go的简单哲学还体现在Go 1兼容性的提出。当初使用Go 1.4编写的代码如今也可以顺利通过最新的Go 1.16版本的编译并正常运行起来。

在Go演化进入关键阶段(走向Go 2)的今天,有人向Go开发团队提出过这样一个问题:Go后续演化的最大难点是什么?Go开发团队的一名核心成员回答道:“最大的难点是如何继续保持Go语言的简单。”

3.2 偏好组合,正交解耦

Go语言是如何将程序的各个部分有机地耦合在一起的呢?

Go语言为我们呈现了这样一幅图景:一座座没有关联的“孤岛”,但每个岛内又都很精彩。现在摆在面前的工作就是以最适当的方式在这些孤岛之间建立关联(耦合),形成一个整体。Go采用了组合的方式,也是唯一的方式。

Go语言提供的最为直观的组合的语法元素是类型嵌入(type embedding)。通过类型嵌入,我们可以将已经实现的功能嵌入新类型中,以快速满足新类型的功能需求。

在通过新类型实例调用方法时,方法的匹配取决于方法名字,而不是类型。这种组合方式,笔者称之为“垂直组合”,即通过类型嵌入,快速让一个新类型复用其他类型已经实现的能力,实现功能的垂直扩展。

下面是一个类型嵌入的例子:

// $GOROOT/src/sync/pool.go
type poolLocal struct 
    private interface
    shared  []interface
    Mutex
    pad     [128]byte

复制代码

我们在poolLocal这个结构体类型中嵌入了类型Mutex,被嵌入的Mutex类型的方法集合会被提升到外面的类型(poolLocal)中。比如,这里的poolLocal将拥有Mutex类型的Lock和Unlock方法。但在实际调用时,方法调用会被传给poolLocal中的Mutex实例。

interface是Go语言中真正的“魔法”,通过interface将程序各个部分组合在一起的方法,笔者称之为“水平组合”。一种常见的方法是通过接受interface类型参数的普通函数进行组合,例如下面的代码。

// $GOROOT/src/io/ioutil/ioutil.go
func ReadAll(r io.Reader)([]byte, error)

// $GOROOT/src/io/io.go
func Copy(dst Writer, src Reader)(written int64, err error)
复制代码

函数ReadAll通过io.Reader这个接口将io.Reader的实现与ReadAll所在的包以低耦合的方式水平组合在一起了。

综上,组合原则的应用塑造了Go程序的骨架结构。类型嵌入为类型提供垂直扩展能力,interface是水平组合的关键,它好比程序肌体上的“关节”,给予连接“关节”的两个部分各自“自由活动”的能力,而整体上又实现了某种功能。

3.3 原生并发,轻量高效

与传统的单核CPU相比,多核CPU带来了更强的并行处理能力、更高的计算密度和更低的时钟频率,并大大减少了散热和功耗。Go的设计者敏锐地把握了CPU向多核方向发展的这一趋势,果断将面向多核、原生内置并发支持作为新语言的设计原则之一。

Go语言原生支持并发的设计哲学体现在以下几点。

(1)Go语言采用轻量级协程并发模型,使得Go应用在面向多核硬件时更具可扩展性

提到并发执行与调度,我们首先想到的就是操作系统对进程、线程的调度。

为了解决复杂和难于扩展问题,Go果断放弃了传统的基于操作系统线程的并发模型,而采用了用户层轻量级线程或者说是类协程(coroutine),Go将之称为goroutine。

操作系统的眼中只有线程,它甚至不知道goroutine的存在。goroutine的调度全靠Go自己完成,将这些goroutine按照一定算法放到CPU上执行的程序就称为goroutine调度器(goroutine scheduler)。

(2)Go语言为开发者提供的支持并发的语法元素和机制

Go提供了语言层面内置的并发语法元素和机制。

执行单元:goroutine。 创建和销毁方式:go+函数调用;函数退出即goroutine退出。 并发goroutine的通信:通过语言内置的channel传递消息或实现同步,并通过select实现多路channel的并发控制。

(3)并发原则对Go开发者在程序结构设计层面的影响

Go官方鼓励大家使用goroutine来充分利用多核资源。但并不是有了goroutine就一定能充分利用多核资源,或者说即便使用Go也不一定能写出好的并发程序。

并发是一种程序结构设计的方法,它使并行成为可能。

并发程序的结构设计不要局限于在单核情况下处理能力的高低,而要以在多核情况下充分提升多核利用率、获得性能的自然提升为最终目的。

并发是一个更大的组合的概念,它在程序设计层面对程序进行拆解组合,再映射到程序执行层面:goroutine各自执行特定的工作,通过channel+select将goroutine组合连接起来。

3.4 面向工程,“自带电池”

Go语言最初设计阶段就将解决工程问题作为Go的设计原则之一去考虑Go语法、工具链与标准库的设计,这也是Go与那些偏学院派、偏研究性编程语言在设计思路上的一个重大差异。

Go设计者将所有工程问题浓缩为一个词:scale(笔者总觉得将scale这个词翻译为任何中文词都无法传神地表达其含义,暂译为“规模”吧)。

生产规模:用Go构建的软件系统的并发规模,比如这类系统并发关注点的数量、处理数据的量级、同时并发与之交互的服务的数量等。 开发规模:包括开发团队的代码库的大小,参与开发、相互协作的工程师的人数等。

那么Go是如何解决工程领域规模化所带来的问题的呢?我们从语言、标准库和工具链三个方面来看一下。

(1)语言

Go语言是一门简单的语言,简单意味着可读性好,容易理解,容易上手,容易修复错误,节省开发者时间,提升开发者间的沟通效率。

(2)标准库

Go被称为“自带电池”(battery-included)的编程语言。如果说一门编程语言“自带电池”,则说明这门语言标准库功能丰富,多数功能无须依赖第三方包或库,Go语言恰是这类编程语言。

Go团队还在golang.org/x路径下提供了暂未放入标准库的扩展库/补充库供广大Gopher使用,包括text、net、crypto等。这些库的质量也是非常高的。

(3)工具链

开发人员在做工程的过程中需要使用工具。Go语言提供了十分全面、贴心的编程语言官方工具链,涵盖了编译、编辑、依赖获取、调试、测试、文档、性能剖析等的方方面面。

构建和运行:go build/go run 依赖包查看与获取:go list/go get/go mod xx 编辑辅助格式化:go fmt/gofmt 文档查看:go doc/godoc 单元测试/基准测试/测试覆盖率:go test 代码静态分析:go vet 性能剖析与跟踪结果查看:go tool pprof/go tool trace 升级到新Go版本API的辅助工具:go tool fix 报告Go语言bug:go bug

值得重点提及的是gofmt统一了Go语言的编码风格,在其他语言开发者还在为代码风格争论不休的时候,Go开发者可以更加专注于领域业务。

以上是关于《Go语言精进之路》读书笔记 | 理解Go语言的设计哲学的主要内容,如果未能解决你的问题,请参考以下文章

《Go语言精进之路》读书笔记 | 理解Go语言代码块与作用域

《Go语言精进之路》读书笔记 | 理解Go语言表达式的求值顺序

《Go语言精进之路》读书笔记 | 汇总

《Go语言精进之路》读书笔记 | 选择适当的Go语言版本

《Go语言精进之路》读书笔记 | 了解Go语言的诞生与演进

《Go语言精进之路》读书笔记 | 使用Go语言原生编码思维来写Go代码