引子
年初和极客邦一起录制了关于 Go 语言学习的视频:《Go 语言从入门到实践》。这门课程的想法就是帮助那些已经掌握了一门编程语言的工程师快速学习 Go 语言。在课程中为了便于有编程基础的朋友快速掌握 Go,我采用了诸如类比等的讲述方式,非常高兴这门课程得到了很多朋友的肯定。也从留言中看到了大家的学习热情,这些都是对我最大的鼓舞。
对于计算机从业者而言不论你的母语是什么语言,中文,英语或是法语,西班牙语等,你的第一工作语言都是编程语言,你一定听说过那句话 “talk is cheap show me the code"。所以,快速学习和掌握编程语言一直以来都是每一个工程师梦最想要拥有的超能力。
我从小学开始学习编程,在后来 17 年的职业生涯中也主动和被动的学习了一众编程语言,如 C/C++,Java,Python,Haskell,Groovy,Scala,Clojure,Go 等等,在这期间付出了很多努力,取得了不少经验,当然也走过了更多弯路。下面分享一下自己的学习心得,希望可以对大家的学习有所帮助和借鉴。
掌握编程范式优于牢记语法
各种编程语言里的独特语法简直是五花八门,下面就随便选取了其中几种语言,看看你们知道他们都是什么语言吗?
1.
def biophony[T <: Animal](things: Seq[T]) = things map (_.sound)
def biophony[T >: Animal](things: Seq[T]) = things map (_.sound())
复制代码
2.
quicksort [] = []
quicksort (x:xs) =
let smaller = [a|a<-xs,a<=x]
larger = [a|a<-xs, a>x]
in quicksort smaller ++[x]++ quicksort larger
复制代码
3.
my @sorted = sort {$a <=> $b} @input;
my @results = map {$_ * 2 + 1} @input;
复制代码
很多工程师往往把学习语言的重点放在了学习不同语言的语法上,而忽略了语言背后的思想及适合的应用场景。
其实对于编程语言的学习,意义最大,收获最大的就是对于编程思想的学习。正如著名的计算机学者,首位图灵奖获得者,Alan Perlis 说的那样如果一个编程语言不能够影响你的编程思维,这个语言便不值得学习。
“A language that doesn’t effect the way you think about programming, is not worth knowing.”
- Alan Perlis
正确有效的学习编程语言对于我们编程思维的提高有着很重要的影响,即使是去学习的一些暂时还用不到的编程语言。
“Lisp 很值得学习,你掌握它以后,会感到它给你带来极大的启发。这会大大提高你的编程水平,使你成为一个更好的程序员,尽管在实际工作中极少用到 Lisp.” — 《黑客与画家》
程序语言的编程思想主要受到编程范式的影响,如果了解这点你就会发现很多新语言其实是新瓶装老酒。
至于编程范式(programming paradigm),官方的定义是:A programming paradigm is a style, or “way,” of programming. (一种编程风格和方式)
结构化 (Structured)
函数式 (Functional)
面向对象 (Object Oriented)
关于这些典型编程范式相信你已经有所耳闻,也可以在网络上找到很多详细的相关资料,这里就不再赘述,仅通过一些简单的实例对比,来让大家认识到不同编程范式对程序设计思想的影响。
结构化 vs. 函数式
我们通过快速排序的实现来看看这两种编程范式的差别:
快速排序 结构化实现:Groovy 语言实现
class QuickSort {
private static void exch(int pos1,int pos2,List data){
int tmp=data[pos1];
data[pos1]=data[pos2];
data[pos2]=tmp;
}
private static void partition (int lo,int hi, List a){
if (lo<0||hi<0||lo>=hi-1){
return;
}
int midValue=a[lo];
int i=lo+1;int j=hi;
while (true){
while(i<hi&&a[i]<midValue){
i++;
}
while(j>lo&&a[j]>midValue){
j--;
}
if (i>=j) break;
exch(i,j,a);
}
exch(i,lo,a);
partition(lo,i-1,a);
partition(i+1,hi,a);
}
public static List sort(List a){
int lo=0; int hi=a.size()-1;
partition(lo, hi, a);
return a;
}
}
复制代码
快速排序 函数式实现:Groovy 实现
def quickSort_fp(List list){
if (list.size()==0){
return []
}
def x=list[0]
def smaller=list.findAll{it<x}
def mid=list.findAll{it==x}
def larger=list.findAll{it>x}
return quickSort_fp(smaller)+mid+quickSort_fp(larger)
}
复制代码
快速排序 函数式实现:Haskell 实现
quicksort [] = []
quicksort (x:xs) =
let smaller = [a|a<-xs,a<=x]
larger = [a|a<-xs, a>x]
in quicksort smaller ++[x]++ quicksort larger
复制代码
面向对象 Go 语言实现
以下利用 Decorator 模式来为不同的 Caculator 实现的 Caculate 过程计时 (如果对于下面程序有些疑惑,建议先回顾一下设计模式里的 Decorator 模式)
type Caculator interface {
Caculate(op int) int
}
// 通过Decorator模式来实现对不同的
type TimerDecorator struct {
innerCal Caculator
}
func NewTimerFn(c Caculator) Caculator {
return &TimerDecorator{
innerCal: c,
}
}
func (td *TimerDecorator) Caculate(op int) int {
start := time.Now()
ret := td.innerCal.Caculate(op)
fmt.Println("time spent:",
time.Since(start).Seconds())
return ret
}
type SlowCal struct {
}
func (sc *SlowCal) Caculate(op int) int {
time.Sleep(time.Second * 1)
return op
}
func TestOO(t *testing.T) {
// 为了SlowCal添加Decorator
tf := NewTimerFn(&SlowCal{})
t.Log(tf.Caculate(10))
}
复制代码
函数式 Go 语言实现
以下也是通过类似于 Decorator 的思路来为不同的方法实现添加运行过程计时
// 为通过参数传人的内部方法添加运行过程计时
func timeSpent(inner func(op int) int) func(op int) int {
return func(n int) int {
start := time.Now()
ret := inner(n)
fmt.Println("time spent:",
time.Since(start).Seconds())
return ret
}
}
func slowFun(op int) int {
time.Sleep(time.Second * 1)
return op
}
func TestFn(t *testing.T) {
tsSF := timeSpent(slowFun)
t.Log(tsSF(10))
}
复制代码
可以类比,但不要翻译
在学习编程语言时,如果我们已经了掌握了一种语言,通过类比,尤其是比较不同点,可以有助于我们更快的掌握另一种新的语言。
要注意的是学习编程语言也和我们学习自然语言一样,要掌握不同语言的特点,习惯用法,否则就会出现类似于中式英语这样的问题。不管后来学习了什么语言,都是先用熟悉的语言的方式实现,然后翻译成另一种语言,我们常常可以看到 C++语言描述的 C 程序,Go 语言描述的 Java 程序等。
例 1:
是关于在方法运行结束后返回多个值。由于 Java 不支持多返回值,我们通常会把多个返回值打包成一个对象返回。
package demo;
class Result{
private int part1;
private int part2;
public Result(int part1, int part2) {
this.part1 = part1;
this.part2 = part2;
}
public int getPart1() {
return part1;
}
public int getPart2() {
return part2;
}
public String toString() {
return String.format("(%d,%d)", part1, part2);
}
}
//
public class Example {
public Result returnMultiValues() {
return new Result(1,2);
}
public static void main(String[] args) {
Example e= new Example();
System.out.println(e.returnMultiValues());
}
}
复制代码
一些 Java 的程序员,在学习 Go 语言时,常常会写出下面的程序,在 Go 中仍是通过一个结构体来返回多个值。
package main
import "fmt"
type Result struct {
Part1 int
Part2 int
}
func returnMultiValues() Result {
return Result{1, 2}
}
func main() {
fmt.Println(returnMultiValues())
}
复制代码
实际 Go 语言是支持多返回值的,程序完全可以简化为下面这样:
package main
import "fmt"
func returnMultiValues() (int, int) {
return 1, 2
}
func main() {
fmt.Println(returnMultiValues())
}
复制代码
例 2
让我们来生成一副扑克。
Java 中的实现:
public static void main(String[] args) {
List<String> cards = new ArrayList<>();
for (int i=2;i<12;i++){
cards.add(String.valueOf(i));
}
cards.addAll(List.of("J","Q","K","A"));
System.out.println(cards);
}
复制代码
Java 程序员,学习了 Python 以后,通常会实现成这样
cards = ['J','Q','K','A']
for n in range(2,11):
cards.append(str(n))
print cards
复制代码
上面的 Python 代码虽然也可以准确的实现功能,但在一个 Python 程序员眼中上面的代码总觉得不是那么地道,下面是地道的 p ython 程序
cards = [str(n) for n in range(2,11)] + list('JQKA')
print cards
复制代码
专注语言特性而不是语法糖
上面谈到写地道的程序,学好编程语言中的“语法糖”,通常可以让我的代码更简化和“看上去”很地道,因此也有很多程序员非常热衷于此,犹如对于 IDE 的快捷方式的热衷,似乎这些是一个资深,高效程序员的标志。
什么是语法糖呢?
语言中某种特殊的语法
对语言的功能并没有影响
对程序员有更好的易用性
增加程序的可读性
一些语法糖的例子:
Python 中 union 两个 list
上面由语法糖带来的地道我之所以用了“看上去”这个词来修饰,就是因为要想真正的写出地道的程序,比掌握语法糖更重要的是掌握语言的特性。
什么是语言的特性呢?
下面我们以 Go 语言为例,来聊一下:
Go 在这方面的确是非常独特的,以至于目前大家常常会讨论 Go 到底是不是面向对象语言,来让我们看看 Go 官方的回答:
“ Is Go an object-oriented language?
Yes and no.
Although Go has types and methods and allows an object oriented style of programming, there is no type hierarchy. The concept of “interface” in Go provides a different approach that we believe is easy to use and in some ways more general.
Also, the lack of a type hierarchy makes “objects” in Go feel much more lightweight than in languages such as C++ or Java.
“ -- https://golang.org/doc/faq
简单来看就是说:
Go 不支持继承
Go 以一种不同方式实现了接口机制
这里我们从 Go 的接口机制来看看语言特性对编程的影响,更多内容大家可以参考我的 Go 语言视频课程。
简单来看就是说:
Go 不支持继承
Go 以一种不同方式实现了接口机制
试想如果我们把上面这个程序进行分包,我们希望分为一个主要逻辑包(Task)和一个插件包(包含不同的 Programmer 的实现,如:GoProgrammer);
从上图你会发现,由于 Java 中具体实现的代码会依赖于接口的声明你并不能简单的分成两个包,否则就会出现上图中循环依赖。所以,这种情况我们通常是分三个包。
看完了语法特性,我们再来看看语言中并发机制特性。
Go 中采用独特的协程(Goroutine)作为基本并发单元,这一定会让 Java 程序员联想起线程(Thread),并不免在编写 Go 并发程序时引入很多编写多线程程序的思维,实际由于两者间存在着很多差异,直接以多线程的编程思想来编写多协程的程序有时是不适合的。
先让我们简单看看两者的差异:
虽然,我们没有完全列出两者的差异,但是你也可以发现像 Java 程序中常见的线程池,在 Go 程序中很多情况下并不会带来像 Java 中那样的性能提升。
再来看看他们的并发机制,Java 通常采用共享内存机制来进行并发控制,而 Go 中则支持了 CSP(Communicating Sequential Processes)机制。
下面通过典型的生产者和消费者并发任务来比较两种方式不同的特性:
共享内存方式(Java 实现)
import java.util.LinkedList;
import java.util.Queue;
import org.junit.jupiter.api.Test;
class Producer implements Runnable {
private Queue<Integer> sharedData;
public Producer(Queue<Integer> sharedData) {
this.sharedData = sharedData;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (this.sharedData) {
try {
while (this.sharedData.size() != 0) {
this.sharedData.wait();
}
this.sharedData.add(i);
System.out.printf("Put data %d \n", i);
this.sharedData.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class Consumer implements Runnable {
private Queue<Integer> sharedData;
public Consumer(Queue<Integer> sharedData) {
this.sharedData = sharedData;
}
@Override
public void run() {
while (true) {
synchronized (this.sharedData) {
try {
while (this.sharedData.size() == 0) {
this.sharedData.wait();
}
System.out.println(this.sharedData.poll());
if (this.sharedData.size() == 0) {
this.sharedData.notify();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class ProducerConsumer {
@Test
void test() throws InterruptedException {
Queue<Integer> sharedData = new LinkedList<>();
new Thread(new Producer(sharedData)).start();
new Thread(new Consumer(sharedData)).start();
Thread.sleep(2 * 1000);
}
}
复制代码
利用共享的 Queue 来实现生产者和消费者之间的数据传递,为了保证数据在多线程间同步,我们使用了锁。
CSP 方式 (Go 实现)
package Demo
import (
"fmt"
"testing"
"time"
)
func Producer(ch chan int) {
for i := 0; i < 100; i++ {
fmt.Printf("put %d \n", i)
ch <- i
}
close(ch)
}
func Consumer(ch chan int) {
for {
select {
case i, ok := <-ch:
if !ok {
fmt.Println("done.")
return
}
fmt.Println(i)
}
}
}
func TestFn(t *testing.T) {
ch := make(chan int)
go Producer(ch)
go Consumer(ch)
time.Sleep(time.Second * 3)
}
复制代码
Go 这是利用 CSP 机制中的 channel 在生产者和消费者之间传递数据。
好的代码风格不能代替好的设计
代码是软件实现的最终形式。但是对于更为复杂的软件而言,我们仅仅在代码层面整洁,复用及高可读性还是远远不够。
人们总是通过更高层次的抽象来实现简化。犹如,编程语言由机器语言,到汇编语言,再到高级语言不断演进,抽象层次不断提高。更高的抽象层次,能够更加有助于人们去了理解和构建更复杂的软件。
所以,在每个抽象层面,都要考虑简洁,复用和易理解。而且软件的设计过程及人们理解软件设计的过程也通常是自顶向下的。这就产生了指导人们做好高层抽象的设计模式,甚至更高抽象层面的架构模式。
作为合格程序员光学习代码的整洁之道是不够的,还有学习更高层面的整洁和复用。
这里模拟我们要实现一个可以扩展使用不同支付方式的支付过程:
type PayChannel int
const (
AliPay PayChannel = 1
WechatPay PayChannel = 2
)
func payWithAli(price float64) error {
fmt.Printf("Pay with Alipay %f\n", price)
return nil
}
func payWithWechat(price float64) error {
fmt.Printf("Pay with Wechat %f\n", price)
return nil
}
func PayWith(channel PayChannel, price float64) error {
switch channel {
case AliPay:
return payWithAli(price)
case WechatPay:
return payWithWechat(price)
default:
return errors.New("not support the channel")
}
}
复制代码
上面的代码在编码风格上是整洁的,可读性也不错。但我们会发现每增加一种支付模式我们都要修改 PayWith 这个方法,在 switch 中加入一个对应的分支。
通过利用面向对象中的命令模式的设计思想,我们可以将代码优化为如下(当然这里是采用函数式编程来实现这个命令模式的思想的)
type PayFunc func(price float64) error
func payWithAli(price float64) error {
fmt.Printf("Pay with Alipay %f\n", price)
return nil
}
func payWithWechat(price float64) error {
fmt.Printf("Pay with Wechat %f\n", price)
return nil
}
func Pay(payMethod PayFunc, price float64) error {
return payMethod(price)
}
复制代码
这里给大家推荐两本设计模式方面的经典书籍
不要害怕遗忘和混淆
“学了也用不上,很快就忘记了”常常会成为很多程序员拒绝学习新的编程语言的借口。
遗忘和混淆都是正常的,人类记忆就是这样。
这是遗忘曲线,在一开始的遗忘率是最高的。对于语言学习,我的经验是我们会很快忘记我们学到的特殊语法,留存下来会是我们对语言编程范式,编程特性的理解。所以,正如前面我们已经提到过的《黑客与画家》中观点:学习了 Lisp,即使你在工作中极少使用到它,你也会成为一个更优秀的程序员。
如果你是按照我们前面所说的方式充分掌握每种语言的特性并了解编程范式,你所遗忘和混淆的更多的是语法。通过写上一,两天,甚至几小时的程序,你很快就会发现所有那些对于这种语言的技能就都回来了。这好比练过健身的人,一段时间不练,肌肉会有流失,但是与从来没有练过的人不同,他们通过训练,肌肉很快能够恢复原有的状态,就是所谓的肌肉记忆,我们的大脑记忆也是这样的。
所以,不要因为害怕遗忘和混淆就不去学习新的语言,他们不仅可以拓宽你的编程思路,一旦需要你便可以经过较短时间从回巅峰!
不要让遗忘成为你放弃学习的借口,遗忘有时是一种提炼。
“Stay hungry,stay foolish." -- Steve Jobs.
评论