本文是Tony Bai在2017年第三届GopherChina大会上所作,来源如下
https://tonybai.com/2017/04/20/go-coding-in-go-way/
今天我要分享的题目是“Go coding in go way”,中文含义就是用“Go语言编程思维去写Go代码”。看到这个题目大家不禁要问:究竟什么是Go语言编程思维呢?关于什么是Go语言变成思维其实并没有官方说法。这里要和大家交流的内容都是基于Go诞生七年多以来我个人对Go的设计者、Go team以及Go主流社区的观点和代码行为的整理、分析和总结。希望通过我的这次“抛砖引玉”,让在座的Gopher们对“Go语言编程思维”有一个初步的认知,并在日常开发工作中遵循Go语言的编程思维,写出idiomatic的Go代码。
在人类自然语言学界有一个很著名的假说:”萨丕尔-沃夫假说“,这个假说的内容是这样的:语言影响或决定人类的思维方
("Language inuences/determines thought" - Sapir-Whorf hypothesis )
说到这个假说,我们不能不提及在2017年初国内上映了一部口碑不错的美国科幻大片《降临》,这部片子改编自雨果奖获得者华裔科幻小说家Ted姜的《你一生的故事》,片中主线剧情的理论基础就是就是“萨丕尔-沃夫假说”。更夸张的是片中直接将该假说应用到外星人语言上,将其扩展到宇宙范畴,片中的女主作为人类代表与外星人沟通,并学会了外星语言,从此思维大变,拥有了预知未来的“超能力”。由此我们可以看出“选择对一门语言是多么的重要”。奇妙的是,在编程语言界,有位大师级人物也有着与”萨丕尔-沃夫假说”异曲同工的观点和认知。他就是首届图灵奖得主、著名计算机科学家Alan J. Perlis(艾伦·佩利)。他从另外一个角度提出了:“不能影响到你的编程思维方式的编程语言不值得去学习和使用”
不能影响到你的编程思维方式的编程语言不值得去学习和使用”
A language that doesn't aect the way you think about programming is not worth knowing
从上述大师们的理论和观点,我们似乎看到了语言与思维之间存在着某种联系。那么两者间的这种联系在真实编程世界中的投影又是什么样子的呢?我们来看一个简单的编程问题
[问题: 素数筛] 问题描述:素数是一个自然数,它具有两个截然不同的自然数除数:1和它本身。要找到小于或等于给定整数n的素数。针对这个问题,我们可以采用埃拉托斯特尼素数筛算法。算法描述:先用最小的素数2去筛,把2的倍数剔除掉;下一个未筛除的数就是素数(这里是3)。再用这个素数3去筛,筛除掉3的倍数... 这样不断重复下去,直到筛完为止。
func generate(ch chan<- int) { for i := 2; i < 100; i++ { ch <- i // Send 'i' to channel 'ch'. } close(ch)}func filter(src <-chan int, dst chan<- int) { prime, ok := <-src if !ok { close(dst) return } fmt.Println(prime) out := make(chan int) go filter(out, dst) for num := range src { if num%prime != 0 { out <- num } } close(out)}//筛选素数func Sieve() { origin, wait := make(chan int), make(chan int) // Create a new channel. go generate(origin) go filter(origin, wait) <-wait}
我想大家用Go语言实现这个编程问题一定想不到这种方式,如果C实现大家可能是命令式编程,用Java实现可能多数人是面向对象思维,Go版本的素数筛实现采用的是goroutine的并发组合。程序从2开始,依次为每个素数建立一个goroutine,用于作为筛除该素数的倍数。ch指向当前最新输出素数所位于的筛子goroutine的源channel,这段代码来自于Rob Pike的一次关于concurrency的分享slide
通过上述这个现实中的问题我们可以看到:面对同一个问题,来自不同编程语言的程序员给出了思维方式截然不同的解决方法:C的命令式思维、Java的面向对象思维,和Go的并发思维。这一定程度上印证了前面的假说:编程语言影响编程思维。Go语言诞生较晚(2007年设计、2009年发布Go1),绝大多数Gopher(包括我在内)第一语言都不是Go,都是“半路出家”从其他语言转过来的,诸如:C、C++、Java、Python等,甚至是Javascript、Haskell、Lisp等。由于Go语言上手容易,在转Go的初期大家很快就掌握了Go的语法。但写着写着,就是发现自己写的代码总是感觉很别扭,并且总是尝试在Go语言中寻找自己上一门语言中熟悉的语法元素;自己的代码风格似乎和Go stdlib、主流Go开源项目的代码在思考角度和使用方式上存在较大差异。而每每看到Go core team member(比如:rob pike)的一些代码却总有一种醍醐灌顶的赶脚。这就是我们经常说的go coding in c way、in java way、in python way等。出现这种情况的主要原因就是大脑中的原有语言的思维方式在“作祟”。我们学习和使用一门编程语言,目标就是要用这门语言思维方式去Coding。学习Go,就要用Go的编程思维去写Go代码,而不是用其他语言的思维方式。
人类语言如何影响人类思维这个课题自然要留给人类语言学家去破解。但编程语言如何影响编程思维,每个程序员都有着自己的理解。作为一个有着十几年编程经验的程序员,我认为可以用下面这幅示意图来解释一门编程语言思维的形成机制:
决定编程语言思维的根本在于这门编程语言的价值观!什么是价值观?个人认为:一门编程语言的价值观就是这门语言的最初设计者对程序世界的认知。不同编程语言的价值观不尽相同,导致不同编程语言采用不同的语法结构,不同语言的使用者拥有着不同的思维方式,表现出针对特定问题的不同的行为(具现为:代码设计上的差异和代码风格上的不同),就像上面素数筛那样。比如:
C的价值观摘录:
C++价值观摘录:
此外,从上述模型图,我们可以看出思维和结构影响语言应用行为,这是语言应用的核心;同时存在一个反馈机制:即语言应用行为会反过来持续影响/优化语言结构。我们常用冰山表示一个事物的表象与内涵。如果说某种语言的惯用法idiomatic tips或者best practice这些具体行为是露出水面上的冰山头部,那么价值观和思维方式就是深藏在水面以下的冰山的基座
从上述模型来看,编程语言思维形成的主导因素是这门编程语言的价值观,因此我们首先来看一下Go语言价值观的形成以及Go语言价值观的具体内容。Go语言的价值观的形成我觉得至少有三点因素
Go语言价值观形成是与Go的初期设计者不无关系的,可以说Go最初设计者主导了Go语言价值观的形成!这就好比一个企业的最初创始人缔造企业价值观和文化一样。
图中是Go的三位最初设计者,从左到右分别是罗伯特·格瑞史莫、罗伯·派克和肯·汤普逊。Go初期的所有features adoption是需要三巨头达成一致才行。三位设计者有一个共同特征,那就是深受Unix文化熏陶。罗伯特·格瑞史莫参与设计了Java的HotSpot虚拟机和Chrome浏览器的JavaScript V8引擎,罗博·派克在大名鼎鼎的bell lab侵淫多年,参与了Plan9操作系统、C编译器以及多种语言编译器的设计和实现,肯·汤普逊更是图灵奖得主、Unix之父。关于Unix设计哲学阐述最好的一本书莫过于埃瑞克.理曼德(Eric S. Raymond)的《UNIX编程艺术》了,该书中列举了很多unix的哲学条目,比如:简单、模块化、正交、组合、pipe、功能短小且聚焦等。三位设计者将Unix设计哲学应用到了Go语言的设计当中,因此你或多或少都能在Go的设计和应用中找到这些哲学的影子
Go并发模型CSP理论的最初提出者Tony Hoare曾提出过这样一个观点:The task of the programming language designer " is consolidation not innovation ". (Tony Hoare, 1973).编程语言设计者的任务不是创新,而是巩固。
和其他语言一样,Go也是站在巨人肩膀上的。这种基因继承,不仅仅是语法结构的继承,还有部分价值观的继承和进一步认知改进。当然不可否认的是Go也有自己的微创新,比如:implicit interface implementation、首字母大小写决定visibility等。虽然不受学院派待见,但把Go的这些微创新组合在一起,你会看到Go迸发出了强大的表现力。
有一种开玩笑的说法:Go诞生于C++程序的漫长compile过程中。如果c++编译很快,那么上面的Go语言三巨头也没有闲暇时间一起喝着咖啡并讨论如何设计一门新语言。面对时代变迁、面对新的基础设施环境、多核多处理器平台的出现,很多传统语言表现出了“不适应”,这直接导致在开发大规模软件过程中出现了各种各样的问题,比如:构建缓慢、依赖失控、代码风格各异、难用且复杂无法自动化的工具、跨语言构建难等。Go的设计初衷就是为了面向新环境、面向解决问题的。虽然这些问题不都是能在语言层面解决的,但这些环境和问题影响了设计者对程序世界的认知,也就影响了Go的价值观
在明确了Go价值观的形成因素后,我认为Go语言的价值观至少包含以下几点:
用一句话概括Go的价值观(也便于记忆):Go是在偏好并发的环境下的简单概念/事物的正交组合
Go is about orthogonal composition of simple concepts with preference in concurrency.
无论是Go语言设计还是Go语言使用,都受到上述价值观的影响。接下来我们逐个来看一下Go语言价值观主导下的Go语言编程思维,并看看每种编程思维在语言设计、标准库实现以及主流Go开源项目中的应用体现。我将按“价值观”,“(价值观下的)语言设计”,“编程思维”,“编程思维体现”的顺序展开
Go的第一个价值观就是”全面简单”。图灵奖获得者迪杰斯特拉说过:”简单和优雅不受欢迎,那是因为人们要想实现它们需要更努力地工作,更多自律以及领会更多的教育。” 而Go的设计者们恰恰在语言设计初期就将复杂性留给了语言自身的设计和实现阶段,留给了Go core team,而将简单留给了gopher们。因此,Simplicity价值观更多地体现在了Go语言设计层面。这里简单列举一些:- 简洁、正规的语法:大大降低Go入门门槛,让来自其他语言的初学者可以轻松使用Go语言;
如今,Go语言的简单已经从自身设计扩展到Go应用的方方面面,但也正是由于在语言层面已经足够简单了,因此在应用层面,“简单”体现的反倒不是很明显,更加顺其自然。接下来,我总结整理几个在“全面简单”价值观下形成的Go编程思维,我们一起看一下。
在gofmt的帮助下,Go语言一统coding style。在这样的一个情形下,naming成了gopher们为数不多可以“自由发挥”的空间了。但对于naming,Go有着自己的思维:短命名。即在并不影响readablity的前提下,尽可能的用长度短小的标识符,于是我们经常看到用单个字母命名的变量,这与其他一些语言在命名上的建议有不同,比如Java建议遵循“见名知义”的命名原则。Go一般在上下文环境中用最短的名字携带足够的信息,我们可以对比一下Java和Go:
java vs. go "index" vs. "i" value" vs. "v"
除了短小,Go还要求尽量在一定上下文中保持命名含义的一致性,比如:在一个上下文中,我们声明了两个变量b,如果第一个b表意为buf,那么后续的b也最好是这个含义。在命名短小和一致性方面,stdlib和知名开源项目为我们做出表率
码农们是苦逼的,每天坐在电脑前一坐就是10多个小时,自己的“业余”时间已经很少了。Go语言的设计者在这方面做的很“贴心”,考虑到为了让男Gopher能有时间撩妹,女Gopher能有时间傍帅哥,Go语言倡导minimal thought,即尽可能的降低gopher们的投入。这种思维体现在语言设计、语言使用、相关工具使用等多个方面。比如:
C++程序员看到这里是不是在“抹眼泪”,这里并非黑C++,C++的复杂和多范式是客观的,C++98标准和C++17标准的差异甚至可以用“两门语言”来形容,用泛型的代码和不用泛型的代码看起来也像是两门完全不同的语言,这种心智负担之沉重可想而知。
正交组合是我总结出来的第二个Go语言的价值观。如果说上一个价值观聚焦的是Go程序提供的各种小概念实体或者说”零件”的话,那么Composition就是在考虑如何将这些”零件”联系到一起,搭建程序的静态骨架。在Go语言设计层面,Go设计者为gopher提供了正交的语法元素,供后续组合使用,包括:
正交性为”组合”策略的实施奠定了基础,提供了前提。Rob Pike曾说过:“If C++ and Java are about type hierarchies and the taxonomy(分类)of types, Go is about composition.”。组合是搭建Go程序静态结构的主要方式。“组合”的价值观贯穿整个语言设计和语言应用:
interface是水平组合的关键,好比程序肌体上的“关节”,给予连接“关节”的两个部分各自“自由活动”的能力,而整体上又实现了某种功能。
Go语言的第三个价值观是偏好并发。如果说interface和组合决定了程序的静态结构组成的话,那么concurrency则影响着程序在执行阶段的动态运行结构。从某种意义上说,Go语言就是关于concurrency和interface的设计!并发不是并行(paralell),并发是关于程序结构的,而不是关于性能的。并发让并行更easy,并且通常性能更好;对于程序结构来说,concurrency是一个比interface组合更大的概念。并发是一种在程序执行层面上的组合:goroutines各自执行特定的工作,通过channels+select将goroutines连接起来。原生对并发的支持,让Go语言更适应现代计算环境。并发的存在鼓励程序员在程序设计时进行独立计算的分解。在语言层面,Go提供了三种并发原语