引子
年初和極客邦一起錄製了關於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 larger3.
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通過以上比較你會發現:對於結構化編程,我們要通過程序來告訴機器怎麼做,而函數式編程則更像是通過程序告訴機器我們想要什麼。
函數式 vs 面向對象
這裡我們通過實現一個通用的計算過程計時功能來比較。大家可以細細體會其中的不同。
面向對象 Go語言實現
以下利用Decorator模式來為不同的Caculator實現的Caculate過程計時 (如果對於下面程序有些疑惑,建議先回顧一下設計模式裡的Decorator模式)
type Caculator interface { Caculate(op int) int}
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) { 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語言的時候,要特別關注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語言視頻課程。
試想如果我們把上面這個程序進行分包,我們希望分為一個主要邏輯包(Task)和一個插件包(包含不同的Programmer的實現,如:GoProgrammer);
從上圖你會發現,由於Java中具體實現的代碼會依賴於接口的聲明你並不能簡單的分成兩個包,否則就會出現上圖中循環依賴。所以,這種情況我們通常是分三個包。
而Go語言的接口實現,則不需要在代碼中顯式的聲明對於接口定義的依賴,採用的是Duck type的方式。
看完了語法特性,我們再來看看語言中並發機制特性。
1 基本並發單元
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 Demoimport ( "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)}現在可以看到,新增支付方式時,Pay方法完全不用做任何修改。
這裡給大家推薦兩本設計模式方面的經典書籍
不要害怕遺忘和混淆
「學了也用不上,很快就忘記了」常常會成為很多程式設計師拒絕學習新的程式語言的藉口。
遺忘和混淆都是正常的,人類記憶就是這樣。
這是遺忘曲線,在一開始的遺忘率是最高的。對於語言學習,我的經驗是我們會很快忘記我們學到的特殊語法,留存下來會是我們對語言編程範式,編程特性的理解。所以,正如前面我們已經提到過的《黑客與畫家》中觀點:學習了Lisp,即使你在工作中極少使用到它,你也會成為一個更優秀的程式設計師。
如果你是按照我們前面所說的方式充分掌握每種語言的特性並了解編程範式,你所遺忘和混淆的更多的是語法。通過寫上一,兩天,甚至幾小時的程序,你很快就會發現所有那些對於這種語言的技能就都回來了。這好比練過健身的人,一段時間不練,肌肉會有流失,但是與從來沒有練過的人不同,他們通過訓練,肌肉很快能夠恢復原有的狀態,就是所謂的肌肉記憶,我們的大腦記憶也是這樣的。
所以,不要因為害怕遺忘和混淆就不去學習新的語言,他們不僅可以拓寬你的編程思路,一旦需要你便可以經過較短時間從回巔峰!
「Stay hungry,stay foolish." -- Steve Jobs.