0%

Domain-Specific Languages

译注:我看标题以为本节的内容是说要注重客户/业务方的领域术语,但其实不是,本节所说的领域语言是指:前端、后端、数据库、桌面软件等不同的开发领域,所使用的不同的语言

不论何时你听到任何领域内的专家在讨论,可以是国际象棋选手,幼儿教师,或者保险代理人,你都能发现他们的词汇与日常用语有很大的不同。这是领域专用语言(DSLs)的一部分:一个特定领域内的特殊词汇,用来描述领域内特定的某件事情。

在软件世界里,DSLs是通过一种语言写成的一些可执行表达式,专门用在一个领域中,这种语言具备有限的词汇和语法,可读、可理解,并希望领域专家可以编写。针对开发者和科学家的软件DSLs已经存在很长时间了。例如,可以从配置文件中找到Unix的“小语种”,而通过LISP宏创建的语言就是一些更古老的例子了。

DSLs通常根据内部或外部来分类:

  • 内部DSLs通过一种通用的编程语言编写,使其语法看起来更像是自然语言。相比于其他不用这种方式的语言(如Java),这可以更方便给语言提供更多语法糖和格式化的可能性(例如Ruby和Scalar)。很多内部DSL打包了现有的API、库、或者业务代码,然后提供一个封装来减少对某些功能的诡异访问。只需要运行(run)它们就可以直接执行(excutable)。根据实现和领域,它们被用于构建数据结构,定义依赖,运行处理或任务,和其他系统通信,或者校验用户输入。内部DSL语法受限于它的宿主语言(译注:host language应该是指用于编写DSL的基础语言——母语)。有很多模式——例如,表达式构建器、方法链、以及注解——能够帮你把宿主语言转换为你的DSL。如果宿主语言不需要重新编译,那DSL可以很快被领域专家拿来做开发。
  • 外部DSLs是文本或图形化表达式语言——尽管文本DSLs要比图形化的更常见。文本表达式可能通过如词法/语法分析器、模型转换器、生成器,或者任何后期处理类型的工具链进行处理。外部DSL主要用于读入内部模型,作为进一步处理的基础。它用来定义一个语法很有用(例如EGNF——译注:扩展巴科斯范式)。一个语法提供工具链生成部分的起点(例如编辑器、可视化、解析器、生成器)。对于一个简单的DSL来说,手工做的分析器可能足够了——用于创建实例、正则表达式。但如果要求太多,自定义的解析器就会变得很笨,所以查看专为语言语法和DSL工作而设计的工具是有意义的——例如,openArchitectureWareAntlrSableCCAndroMDA。把外部DSLs定义为XML方言也很常见,尽管读起来经常有问题——尤其对没有技术基础的读者而言。

你必须始终考虑你DSL的目标受众。他们是开发者、管理人员、商业消费者、或者终端用户?你不得不去调整语言的技术门槛、可用的工具、语法帮助(如智能感知)、早期校验、可视化、以及代表目标受众。通过隐藏技术细节,DSL可以让用户无需在开发人员的帮助下根据需求自主调整系统。它可以加速开发,因为在初始化语言框架到位后就存在分散工作的可能。现有的表达式和语法也有不同的迁移路径。

Do Lots of Deliberate Practice

刻意练习不是简单的执行任务。如果你问自己“为什么我要执行这项任务?”,你回答是“为了完成任务”,那么你没有在刻意练习。

你做刻意练习事未了提高你执行任务的能力。它是技巧和技术。刻意练习意味着重复。它意味着执行任务的目的是提高你对任务各个部分的掌握程度。它意味着周而复始。慢慢地,一遍又一遍。直到你达到自己预期的精通程度。你做刻意练习的主要任务不是为了完成任务。

开发的主要目的是为了完成一个产品,然而刻意练习的主要目的是为了提升你的能力。它们是不一样的。扪心自问,你会花多少时间去开发别人的产品?又会花多少来开发自己?

需要多少刻意练习才能获得专场?

  • 彼得·诺维格(Peter Norvig)写道:“10000小时[…]可能是个神奇的数字。”
  • 引用自玛丽·波捧迪克的《精益软件》(Leading Lean Software Development Mary Poppendieck):“精英至少需要10000小时的专注刻意练习才能成为专家”。

专长是随着时间推移而到来的——不是在第10000小时那一刻一次性获得。尽管如此,10000小时太大了:每周20小时都需要10年。鉴于这种级别的承诺,你可能会担心自己又不是专家的料。你是!重要的是意识层面的选择。你的选择。过去二十年的研究表明,获得专业知识的主要因素是花时间刻意练习。“天资聪慧”不是主要因素。

  • 玛丽:“专业技能研究人员之间已经达成共识,天生的才能无法超出一个门槛;你不得不拥有一个最小的自然能力以开启运动或专业能力。在此之后,高水平的人往往是工作最努力的人。”

对于你已经很擅长的事情少点刻意练习。刻意练习意味着练习那些你做不好的事情。

  • 彼得:“(开发技能的)秘诀是刻意练习:不只是一次又一次的重复做,更重要的是通过刚好超出你当前能力的任务进行自我挑战,尝试去做,完成之后要分析你的表现,并纠正任何错误。”
  • 玛丽:“刻意练习不是去做你很擅长的事;它意味着自我挑战,做那些你做不好的事。所以它注定会让你不爽。”

刻意练习是一种学习(方式)。学习如何改变你;学习改变你的行为。祝好运。

Distinguish Business Exceptions from Technical

导致运行时出错的事情根本上来说有两种原因:阻止我们使用程序的技术问题,以及组织我们滥用程序的业务逻辑。很多现代语言,如LISP、Java、Smalltalk和C#,运用异常来标记这两种情况。然而,这两种情况完全不同,要严格区分开。用相同级别的异常来描述他们会混淆潜在的来源,不要继承相同的异常类。

但有一个程序错误时,可能是一个无法解决的技术问题导致的。例如,如果你尝试去访问长度为17的数组中的第83个元素,程序明显会跑飞,得到一些异常结果。有些小版本调用一些参数不当的代码库,导致内部库出现相同的情况。

试图解决你自己造成的情况可能会是个错误。我们会把这个异常上升到顶层架构级别,并采用一些异常处理机制让其确保系统处于一个安全状态,比如回滚、日志和通知管理员,以及反馈(客气的)给用户。

当你处在“库情况”中,调用者还违反你方法的调用规则,是这些情况的一个变种,例如,传入一个奇怪的参数或者没有正确设置依赖的对象。这就等同于从17个(长度数组)里面访问第83个:调用者应检查;否则就成了客户端开发程序员的错误了。正确的响应是抛出一个技术异常。

有个不同的,但也属于技术范畴的情况,当一个程序由于运行环境问题导致无法继续,例如数据库无响应。在这种情况下,你必须假设基础设施能够解决这些问题——修复连接以及合理的重连几次——最后失败。即便原因不同,对于调用者而言这些情况是类似的:能做的很少。所以,我们把这个情况当作异常信号抛出交给一般异常处理机制。

与此相反,我们遇到的情况是你无法完成领域逻辑调用的原因。因此我们将其视作异常情况。即不常见且让人不爽的,但并非怪异或者编程错误。例如,我尝试从一个余额不足的储蓄账户中取钱。换而言之,这种情况是约定的一部分,抛出一个异常仅用来取代返回路径,它是模型的一部分,客户能够意识到并着手应对。对于这些情况,适合创建一个特定异常或者异常此层结构,以便客户能根据自己的项目处理这些情况。

把技术异常和业务异常混淆成相同的层次结构,含糊的差异,让调用者困惑于这些方法的约定是什么,什么是调用前必须的前提条件,它应该处理什么情况。分清这些情况,可能增加技术异常被一些程序框架处理掉的几率,而业务领域的异常实际上是由客户端代码考虑并处理的事情。

努力做一个富人,而不仅仅是有钱人。

谁该拥有资本

我们可能会习惯于用收入来衡量一个人的社会价值,这难免有失偏颇,我们应该站在生产的角度,用一个人生产力来衡量他对社会价值。

即在有限的生产资源面前,应该优先把资本分配给生产效率高的群体,因为他们能够为社会创造更大的价值。

这话说起来容易,做起来难,哪些才是生产效率高的群体,每个群体的配比是多少?这些问题书中归根结底就一个答案——交给市场去自我调节。

当然,本书并不是要在政府干预和自由市场的意识形态层面争夺高下,主要还是列举一些常见的经济学谬误,给我们这种经济学小白普及一下基本知识,但我也看得出来,作者多少要倾向于自由市场,政府能不插手就不要插手。

阅读全文 »

原文链接

调试部署和安装过程经常推迟到项目快结束时。在一些项目中会写好安装工具并委派给发布工程师,他们会视这个“烫手山芋”。评估和演示在手工制造的环境中完成以确保一切正常。结果就是团队无法通过部署过程和部署坏境中获得经验,每当做出改变时已经太晚了。

安装/部署过程是业主最先看到的过程,安装/部署过程应该是生产环境中最可靠的第一步(或者至少很容易调试)。部署软件是给业主用的。用不靠谱的部署想把应用程序正确安装,在他们完整地使用你的软件之前,你将收到你客户的各种问题。

通过安装程序启动你的项目,将让你有时间在产品开发周期内改进流程,并有机会修改程序代码让其更容易安装。定期在干净的环境中运行和测试安装程序也能够验证代码是否在开发和测试环境中存在依然的假设。

将部署放到最后意味着可能需要更复杂地解决代码中的预估。在IDE中你能完全掌控环境,这看起来是个好主意,但也可能使部署过程变得更加复杂。早点了解全局利弊总比晚点知道好。

相比于还跑在开发者笔记本中的程序,“具备可部署性”似乎没有很多早期的商业价值,直到你可以在目标环境中演示你的程序这都是事实,在你提供商业价值前还有很多工作要做。如果你推迟部署是因为它微不足道,那就更是要做(提前部署),因为它低成本。如果它非常复杂,或者有很大的不确定性,你应该对应用程序代码进行:随时试验、评估、以及重构部署过程。

安装/部署过程是对你客户和专业服务团队在生产效率方面至关重要,所有你要随时测试和重构这个过程。我们测试和重构遍布整个项目的源码。部署也不能少。

⚠️警告⚠️
本书第二章不适合新手阅读!
本书第二章不适合新手阅读!
本书第二章不适合新手阅读!

如果说第一章读起来还算愉快,就像上高数课一样,刚开始老师告诉你,这是Σi是求和的符号,i从0到100,很好理解对吧,然后你走神一秒钟,黑板上写满了各种微积分公示和计算过程…what the fuck?!

本章涉及到的知识点较多且杂:

  • RSS、XML和JSON语法规则
  • GO语言的并发操作goroutine
  • GO语言的数据同步
  • GO语言类型、接口的定义与使用
  • GO语言数组、切片的定义与使用

其实本章一开始就说了,没必要第一次就读懂本章所有内容,我想作者的本意应该只是让读者感受一下GO语言的整体和编程思想,让读者明白,自己对力量的一无所知。

本章相关的源码放在https://github.com/goinaction/code/tree/master/chapter2/sample,我读完本章并自我感觉理解之后自己“手抄”了一遍,大体和原著框架一致,细节上有所变动,基本上可以在当前文档执行go run main.go查看效果。

我的第一个RSS阅读器

本章其实就做了一件事,写一个自己的RSS阅读器,根据用户的rss订阅表和关键字输入,搜集网上所有含有关键字的“新闻”,并在终端打印出来。

用户的rss订阅表是由json写的,大概长这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[
{
"site" : "npr",
"link" : "http://www.npr.org/rss/rss.php?id=1001",
"type" : "rss"
},
{
"site" : "npr",
"link" : "http://www.npr.org/rss/rss.php?id=1008",
"type" : "rss"
},
{
"site" : "npr",
"link" : "http://www.npr.org/rss/rss.php?id=1006",
"type" : "rss"
},

...
]

所以我们的RSS阅读器程序需要完成一下几个步骤:

  1. 读取并反序列化用户订阅表——data.json
  2. 为订阅表中的每个URL启动一个并发任务——新闻搜索
  3. 抓到之后,根据用户输入关键字,采用正则表达式过滤
  4. 将过滤结果打印至终端后,单个并发任务退出
  5. 启动一个并发任务,专门监听其他新闻搜索任务是否退出
  6. 等待所有新闻搜索任务退出后,程序退出

根据以上流程,书中对整个程序设计的架构图已经表达得比较清楚了:
RSS阅读器程序架构图

整个工程的目录结构:

1
2
3
4
5
6
7
8
9
10
11
- sample
- data
data.json -- 包含一组数据源
- matchers
rss.go -- 搜索 rss 源的匹配器
- search
default.go -- 搜索数据用的默认匹配器
feed.go -- 用于读取 json 数据文件
match.go -- 用于支持不同匹配器的接口
search.go -- 执行搜索的主控制逻辑
main.go -- 程序的入口

1. 关于数据源data.json和feed.go

可以简单理解,feed.go就是data.json的ORM。

  • Feed是一个类型,根据订阅表的数据结构site/link/type来对应其内部的三个属性。
  • RetrieveFeeds()仅负责加载data.json文件,并反序列化所有项目,装到Feed列表里。

搞清楚结构体的定义就行:

1
2
3
4
5
type Feed struct {
Name string `json:"npr"`
URI string `json:"link"`
Type string `json:"type"`
}

注意每个属性最后那个由反引号括起来的标签,这很重要,应为数据源是json格式,而GO语言的json.Decoder可以根据这个标签的内容标记对原始的json数据进行反序列化。

2. 关于match.go接口,以及rss.go和default.go的实现

这三个文件一定要结合起来看,否则就云里雾里了。

  • match模块声明了Matcher.Search()接口行为、结果的数据结构和统一输出结果。
  • default/rss都是是Search()接口的具体实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// match.Matcher 定义搜索匹配接口行为
type Matcher interface {
Search(feed *Feed, keyword string) ([]*Result, error)
}

// default.defaultMatcher 实现默认搜索匹配接口
type defaultMatcher struct{}
func (m defaultMatcher) Search(feed *search.Feed, searchTerm string) ([]*search.Result, error) {
return nil, nil
}

// rss.rssMatcher 实现rss规则的搜索匹配接口
type rssMatcher struct{}
func (m rssMatcher) Search(feed *search.Feed, searchTerm string) ([]*search.Result, error) ...

default.go里其实啥也没有,它的存在只是为了容错(或者说展示一下接口最简单的运用方式),如果再看看Feed.Type就会发现,它映射的data.json里的type是字符串rss,而主函数search.Run()里其实是根据类型来查找相关匹配器的,换而言之,如果type的值不是rss而是其他类型,程序很可能会类似C/C++语言,越界访问数组而出错。所以干脆把找不到的类型全部设置成default类型。

rss.go模块就是具体的Http、XML、RSS、正则匹配的实现了,这部分相对独立与整体框架,不必太拘泥于此。

注意一点

default和rss都有个init()函数,而且实现得也很像,正如上面说的,init函数就是负责把自己以同名的方式保存到search.matchers当中,确保其他模块能后通过字符串的形式找到它们,这是个search模块的私有变量。

1
2
3
4
5
// 在main函数之前执行,将自己的匹配器类型注册到search模块
func init() {
var matcher rssMatcher
search.Register("rss", matcher)
}

3. 关于search.go

是整个阅读器的业务逻辑实现,也就是架构图当中左半部分获取数据、执行搜索、跟踪结果、显示结果几大步骤的调用,它本身并不负责实现任何搜索和匹配相关的功能。

  • 负责调用feed模块,获取订阅表(数据源),并保存到feeds的切片当中
  • 为每一个feed常见一个goroutine,具体业务由Matcher接口相关的实现去执行
  • 创建一个统一的goroutine,监听/等待所有feeds匹配业务执行结束

我认为search.Run()比较重要,关于Matcher接口、JSON/XML、http等其他模块的实现看不懂也没关系,书后面的相关章节会深入解析,而Run函数中关于goroutine的并发启动和chan数据通道这两个概念一定要搞清楚。

以最简单的主监听器waitGroup的并发启动为例:

1
2
3
4
go func() {
waitGroup.Wait()
close(results)
}()

关键字go foo()负责启动一个goroutine,紧随其后是一个闭包(当然,也可以在其他地方先定义一个函数),在这里可以看到,GO语言启动一个“线程”有多么愉快

4. 关于Display()函数及其调用过程

Display是非常值得拎出来说道说道的,仔细观察就会发现Display是在所有goroutine启动完之后,才仅仅被调用了一次!

而该函数的内部实现却非常简单,就是个for循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func Run(keyword string) {
// 各种加载订阅表,匹配器之后
...
// 启动goroutine,等待所有任务结束,最后关闭通道
go func() { ... }()

// 根据订阅表,启动相应数量的并发任务
for _, feed := range feeds {
// 启动goroutine,执行新闻搜索
go func(matcher Matcher, feed *Feed) { ... }(matcher, feed)
}

// Display仅仅被调用了一次
Display(results)
}

func Display(results chan *Result) {
for result := range results {
fmt.Printf("%s: \n%s\n\n", result.Field, result.Content)
}
}

按照一个正常猿类的思维模式,如果集合被遍历完之后,循环就会自动跳出,而且由于并发情况下,一开始的results里面其实啥也没有,应该直接退出。而事实上的执行过程是:一旦results被写入数据,for循环就会执行一次。

为什么?

注意results的类型chan *Result在for-range一个通道的情况下,只要通道没有被关闭,该循环就会被阻塞,一旦通道内写入数据,循环就会被唤醒,直到通道被关闭,循环跳出。(这种机制太风骚了)

5. 关于main.go

主模块唯一值得留意的地方是import _ "./matchers",这是刻意也是必须这样写的:

  1. GO语言不允许导入一个包,却不使用它
  2. matchers里面的rss和default模块都有init函数,不导入就不会被执行,会导致程序出错
  3. 下划线’_’表示一个占位符,类似for循环的占位操作,就是告诉编译器,我确实需要导入这个包,但并不调用它,我只需要它自己执行init函数

小结一下

  • 每个代码文件都必须属于一个包,原则上包名和目录名一致
  • 不论函数还是变量,首字母大写的标识符相当于public,否则相当于private
  1. 任何包内的**init()**函数,都会先于main函数执行,前提是包被导入了。

  2. 多返回值是GO语言的一大特性,GO的很多核心库都是result, err两个返回值。

  3. 根据“江湖规矩”,如果定义一个变量需要初始化为零值,采用var name type声明,如果是定义变量且被赋值,则采用name := value的形式定义

  4. range关键字

  • 可用于迭代数组、字符串、切片、映射和通道
  • 每次迭代都有两个返回值,第一个是索引,第二个是当前元素的副本。
  • 下划线'_'表示一个占位符,就是说,“虽然你给我了,但老子就是不要”的意思
  1. defer关键字表示函数返回时才执行,可以在open一个文件后,立刻调用defer close,但关闭这个动作会等到调用函数返回的时候才真的执行。(妈妈再也不用担心我的句柄,so easy)

  2. goroutine,使用go foo()的形式来启动一个并发,foo也可以是一个闭包。

  3. channel,通过chan关键字声明一个通道,当使用range循环来遍历一个通道时,只要通道不关闭,循环就是阻塞,直到通道内的数据有变化,当通道关闭后,循环退出。

原文链接

有关优秀API设计的重要与挑战已经说过很多了。一步到位很难,以后改起来更难。有点像把小孩举起来。经验丰富的程序员会认识到,好的API遵循统一的抽象层次,展现出一致性和对称性,并形成富有表达力的语言词汇表。唉,要意识到,知道原则并不会自动转化为合适的行为。吃甜食对你并不好。

我想获取的是一个特定的API设计’策略,’而不是站在高处讲道,我一次又一次遇到:便利的论证。它最典型的就是以以下“洞察”之一开头:

  • 我不希望其他类必须单独调用两次才能完成这件事。
  • 如果新的方法几乎和现在的一样,为什么我还要实现它呢?我仅仅想添加一个简单的选择判断。
  • 看吧,很简单:如果第二个字符串参数是以“.txt”结尾,这个方法就可以自动假设,第一个参数是一个文件名,因此我根本不需要两种方法。

想法是好的,但这种参数容易在代码调用API时降低可读性。一个方法的调用会像这样:

1
parser.processNodes(text, false)

不了解如何实现的情况下实际上是毫无意义的,或者至少要查看下文档。这个方法更像是为了实现者的方便而设计的,而不是调用者的方便——“我不希望调用者必须单独调用两次”可以被理解为“我不想代码被分成两个方法”。如果这是为了对抗乏味、笨拙和尴尬,这些便利本质上来说也没什么错。无论如何,如果我们进一步仔细思考,解决这些症状应该是高效的、统一的且优雅的,而便利性不是必须的。APIs被认为是将复杂性隐藏在其下,所以我们可切实地期望良好的API设计需要一些努力。一个长的方法肯定会比经过一系列深思熟虑的操作更容易编写,但他会更容易使用吗?

API作为一种语言的比喻可以指导我们在这些情况下做出更好的设计决策。API应该提供一种富有表达力的语言,给予下一层充足的词汇表以问答有用的问题。这并不意味着它应该为每个可能有价值的问题都提供确切的方法或动词。一个多样的词汇表能让我们精妙地表达意境。例如:我们倾向于用run来代替walk(true),哪怕是它们本质上是一回事,仅仅是速度上的不同。一个统一且深思熟虑的API词汇可以在下一层上生动表达且易于理解。更重要是,可组合的词汇表允许程序员以你可能没有预料到的方式使用API——确实,API为用户带来了极大的便利!下次你想把一些事情归并到API方法时,请记住,英语是不存在这样一个单词的MakeUpYourRoomBeQuietAndDoYourHomeWork,尽管这种频繁请求方式看起来很便利。

本书是我难得会感觉读不下去的书,以至于读后感的书评我都懒得取了。只能大概归结于三点:

  • 作者比较博学,见多识广,内容很有深度,我看不懂
  • 创意(群体)管理这个领域离我太遥远,无法感同身受
  • 我读此书的时候心不在焉

当初购买此书的时候只是被标题吸引,加之很多大咖推荐。我以为是一本讲述个人创新、个体崛起以及未来个人工作和生活方式相关的思考和观点,但就本书内容及主旨而言,只能说似是而非。

阅读全文 »

  • 时间:2007年9月的某个下午
  • 地点:Google公司
  • 人物:
    Rob Pike(罗勃特·派克,unix成员,参与开发UTF-8)
    Ken Thompson(肯尼斯·汤普逊,unix和c语言作者)
    Robert Griesemer(罗伯特·格瑞史莫,参与v8引擎、甲骨文JVM开发)

刚刚被C++标准委员会的人叫去讨论的他们,看到了下一代C++的新颖功能,由衷地发出感慨:你们还觉得C++的特性不够多,不够复杂么!回到办公室,三人接着各种喷,山河日下啊,是时候展现的技术了!

所以他们利用Google给他们的20%自由支配时间,开始倒腾一种新语言,要简洁、高效、便于大型复杂软件开发。由于三人都是各种领域中的翘楚,吸纳了各种语言的优点,用实力告诉全世界什么叫做——less than more。

他们把这门语言命名为GO,并配了一只土拨鼠作为吉祥物,为什么呢?估计是某个下午,他们当中的谁正在撸着C++代码,然后忍无可忍蹦出一句👇:

以上是我结合历史背景瞎编的


Why GO?

  1. 较高的开发速度

大型软件往往需要极漫长的编译时间,我曾尝试过手动编译gcc、chrome等经典软件,少了1个小时出不来,而且还是在不出错的情况下。而GO程序可以在1秒内完成编译,较大型的软件也仅需几十秒。

此外,GO继承了静态语言和动态语言的优势,你可以像运行脚本一样运行go语言快速看结果,也可以直接编译成二进制提高性能。同时,GO不存在像javascript的变量类型不明确的问题,类似C/C++/Java一样,如果某个变量类型错误,在编译阶段你就能定位到。

  1. 为并发而生

现如今,100多核的高性能服务器早已司空见惯,而传统的编程语言如C++/Java依旧是单核思想,尽管有线程机制,但开发者不得不谨慎思考全局变量、共享内存、IO这些资源访问方式,从而产生大量与业务无关的代码。

GO语言提供goroutine和channel两种机制,也是该语言的核心思想之一:

  • goroutine负责启动某个业务函数独立运行
  • 业务所生产/消费的数据直接放到channel中
    两个goroutine间共享一个channel,其中的数据是同步的,开发者大可不必操心所谓的互斥/同步等访问机制,只需专注写业务代码。
  1. 强大的类型系统

和C语言一样,GO语言仅内置了几种如int、string等类型,同时支持开发者自定义类型。

如果从面向对象的视角来看,GO和Java/C#不一样,它不支持类型继承。换而言之,类似Student -> Peopler -> Object这种类型继承的思想在GO中是不存在的。GO提供一种全新的理念——行为建模。

如果一只动物叫起来像鸭子,那它可能就是鸭子!

记住上面这句话,这是GO对“继承”的重新定义,提供一种叫做“接口”的概念来对各种类型进行打包组合。初识GO的时候我很不理解这个概念,毕竟接口的概念面向对象也有(简单的理解就是留给继承者去实现的方法),我仅在此试图做个简单总结:

假设我们要为某个软件实现将数据/文件拷贝到USB设备的业务,同时要便于今后千奇百怪的存储设备的扩展。

如果用Java,大概会这么实现:

1
2
3
4
5
6
7
8
9
public interface UsbDisk {
void read();
void write();
}

public class UsbHDD implements UsbDisk ...
public class UsbSSD implements UsbDisk ...

file.copyTo(UsbDevice device);

说白了,为了满足file.copyTo()这个方法能够支持各种各样的类型,我们需要先抽象一个UsbDisk (不管是接口还是基类),而实现它的UsbHDD UsbSSD总存在着千丝万缕的关系,否则就无法被copyTo调用。

有意思的地方来了,USB口不仅可以插存储设备,还可以插鼠标、键盘、手机等,这些设备也存在读写操作,比如iPhone这个类没有继承或实现,或者干脆就不属于UsbDisk,怎么办?

可能有一波自诩大师的架构者们会说:哪个二货会这么干?从一开始就应该抽象一个UsbDevice。是的,正如我们今天看到的,很多面向对象的框架下,数以百计的类型都有一个共同的基类——Object。

但如果用GO,则完全不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 为当前业务定义好某种接口
type UsbDisk interface {
Read()
Write()
}

// 定义一个UsbHDD类型,实现read/write方法,但没有“继承”
type UsbHDD struct {}
func (d UsbHDD) Read() ...
func (d UsbHDD) Wirte() ...

// UsbSSD和IPhone的实现同上👆
type UsbSSD struct {} ...
// 就算iPhone不属于存储设备,只要实现读写操作,一样可用
type IPhone struct {} ...

// file.copyTo,接受任何实现UsbDisk接口方法的类型
func copyTo(device UsbDisk) {
device.Read()
device.Write()
}

可以看到,GO语言中UsbHDD UsbSSD IPhone这三个类之间根本没有任何关系,也不继承UsbDisk,但却可以通过以下方法来调用:

1
2
3
4
5
6
7
func main() {
var hdd UsbHDD
var ssd UsbSSD

copyTo(hdd)
copyTo(ssd)
}

也就是说,实现了相同方法的类型(叫起来像鸭子),可以被当作同一类型(它就是鸭子)。如果再遇到业务扩展时,不需要推翻之前的架构,或者陷入“抽象”的哲学思考当中。(啊~人和咸鱼,到底有什么共性呢)

PS:相比于面向对象,GO语言的接口概念比较颠覆,一口气写多了😅

  1. 内存垃圾回收

还是原来的配方,还是原来的味道,本书也只是提了这么一句。我觉得垃圾回收机制根本算不上GO的优势,毕竟很多经典语言都有的功能好伐。(C/C++笑而不语,谁敢和我比经典)

总之,作者的意思应该是说,憋管内存分配的问题,放心大胆地用。另外GO虽然有指针,但不是你想的那样。

Hello GO!

好了,说了那么多,无非就是helloworld:

1
2
3
4
5
6
7
package main  // 每个go源码都所属一个包,参考java
import "fmt" // 导入依赖包,参考java

// 入口函数,main函数必须在main包当中,必须!
func main() {
fmt.Println("Hello world!")
}

小结一下

  • GO是一门现代计算机技术驱动下的语言
  • GO通过goroutine和channel机制优雅地解决并发问题
  • GO同时具备静态和动态类型语言的有点,可以二进制或脚本形式运行
  • GO的接口思想是一种“鸭子类型”的继承概念
  • GO同样提供内存垃圾回收管理机制
  • GO提供了类似Java/NodeJS/dotnet一样功能丰富的包
  • The Go Playground可以在web上执行代码,前提是科学上网

原文链接

我们活在一个有趣的时代。随着开发(这种技能)散布于全球,你要认识到大量有能力的人可以胜任你的工作。你需要持续学习来保持市场。否则,你会变得落伍,栓死在同样的工作圈内,有一点,你将不再被需要,或者你的岗位被更便宜的外包团队拿走。

那么你应该如何应对?一些老板可能比较有良心,会提供足够的培训来扩大你的能力圈。其他人可能不会拿出时间和金钱做任何的培训。为了安全起见,你需要担当起自我教育的责任。

下面是让你保持学习的清单。其中有很多都可以从网上免费获取到:

  • 阅读书籍、杂志、博客、微博提要、网站等。如果你想要深入某个主题,考虑加入邮件群或新闻组。
  • 如果你确实想要浸泡在技术当中,动手——写代码!
  • 总要尝试去和导师一起工作。作为领路人也可能阻碍你的教育,尽管你能从任何人那里学到任何事,但你能从更聪明或比你有经验的人那里学到多得多的东西。如果你找不到一个导师,请接着找!
  • 利用虚拟导师。从网上找一些你确实很喜欢的作者和开发者,并会阅读他们写的任何东西。订阅他们的博客。
  • 获取知名的框架和库给你用。去了解他们是如何工作的,以便让你更好的利用它。如果它们是开源的,那你就太幸运了。用调试器逐步跑完一段代码,看看幕后会发生什么。你会看到代码是被一群非常聪明的人写成并审查过的。
  • 当你出错时,修复bug或带着问题跑一遍,尝试去真的了解到底发生了什么。在网上很可能有人也遇到了相同的问题,此时Google时非常有用的。
  • 加入或开启一个学习小组(社区模式),或者你感兴趣的任何语言、技术、学科类的本地用户组。
  • 参加研讨会。如果你不能去,很多研讨会都会把他们的会谈内容免费发放到网上。
  • 通勤路程很长?听一些播客吧(听书、广播)。
  • 曾在代码库中运行静态分析工具或者IDE中看到警告?去了解所报告的内容以及为什么。
  • 听取《务实的程序员》The Pragmatic Programmers,每年学习一门新语言,至少是一种新技术或工具。学习分支的输出会让你在运用自己的技术栈时带来新的想法。
  • 你的学习不应该只局限于技术。学一些你工作领域的知识,可以帮助你更好的理解业务需求,帮助解决业务上的问题。学习如何高效输出——更好的工作————这是一个很不错的选项。
  • 重返校园。

如果能有一种尼奥在矩阵中的能力就相当nice了,可以简单地把我们需要的信息下载到大脑。(不懂得,请看黑客帝国)。然而我们不行,所以需要对自己的时间负责。你不得不花费每天醒着的时光学习。每周都做一点,总比什么都没有要好。这是(起码是)你业余生活的一部分。

技术迭代太快,别被甩在身后!