語言特性

  學習一門語言,首先要去了解下這個語言的特性,它有哪些優勢、特點,相對於其他成熟語言有什麼獨到之處。當然,如果你是單純的衝着這個語言名字來的也可以。先來看看 Golang 的語言特性

自動垃圾回收

  沒有自動垃圾回收機制的語言會存在各種非預期的原因,比如由於開發者的疏忽導致最後的釋放內存的語句沒有被調用,都會引發經典而惱人的內存泄露問題。假如該函數被調用得非常頻繁,那麼我們觀察該進程執行時,會發現該進程所佔用的內存會一直瘋長,直至佔用所有系統內存並導致程序崩潰,而如果泄露的是系統資源的話,那麼後果還會更加嚴重,最終很有可能導致系統崩潰。

  到目前爲止,內存泄露的最佳解決方案是在語言級別引入自動垃圾回收算法(Garbage Collection,簡稱GC)。所謂垃圾回收,即所有的內存分配動作都會被在運行時記錄,同時任何對該內存的使用也都會被記錄,然後垃圾回收器會對所有已經分配的內存進行跟蹤監測,一旦發現有些內存已經不再被任何人使用,就階段性地回收這些沒人用的內存。當然,因爲需要儘量最小化垃圾回收的性能損耗,以及降低對正常程序執行過程的影響,現實中的垃圾回收算法要比這個複雜得多,比如爲對象增加年齡屬性等,但基本原理都是如此。

  微軟的 C++/CLI 算是用一種偏門的方式讓 C++ 程序員們有機會品嚐一下垃圾回收功能的鮮美味道。在 C/C++ 之後出現的新語言,比如 Java 和 C# 等,基本上都已經自帶自動垃圾回收功能。

  Go 語言作爲一門新生的開發語言,當然不能忽略內存管理這個問題。又因爲 Go 語言沒有 C++ 這麼”強大”的指針計算功能,因此可以很自然地包含垃圾回收功能。因爲垃圾回收功能的支持,開發者無需擔心所指向的對象失效的問題,因此 Go 語言中不需要 delete 關鍵字,也不需要 free() 方法來明確釋放內存。如果使用 Go 語言實現垃圾回收機制,我們就完全不用考慮何時需要釋放之前分配的內存的問題,系統會自動幫我們判斷,並在合適的時候(比如 CPU 相對空閒的時候)進行自動垃圾收集工作。

更豐富的內置類型

  除了幾乎所有語言都支持的簡單內置類型(比如整型和浮點型等)外,Go 語言也內置了一些比較新的語言中內置的高級類型,比如 C# 和 Java 中的數組和字符串。除此之外,Go 語言還內置了一個對於其他靜態類型語言通常用庫方式支持的字典類型(map)。Go 語言設計者對爲什麼內置 map 這個問題的回答也頗爲簡單:既然絕大多數開發者都需要用到這個類型,爲什麼還非要每個人都寫一行 import 語句來包含一個庫?這也是一個典型的實戰派觀點,與很多其他語言的學院派氣息迥然不同。

  另外有一個新增的數據類型:數組切片(Slice)。我們可以認爲數組切片是一種可動態增長的數組。這幾種數據結構基本上覆蓋了絕大部分的應用場景。數組切片的功能與 C++ 標準庫中的 vector 非常類似。Go 語言在語言層面對數組切片的支持,相比 C++ 開發者有效地消除了反覆寫以下幾行代碼的工作量:

#include <vector>
#include <map>
#include <algorithm>
using namespace std;

  因爲是語言內置特性,開發者根本不用費事去添加依賴的包,既可以少一些輸入工作量,也可以讓代碼看起來儘量簡潔。

函數多返回值

  目前的主流語言中除 Python 外基本都不支持函數的多返回值功能,不是沒有這類需求,可能是語言設計者沒有想好該如何提供這個功能,或者認爲這個功能會影響語言的美感。

  比如我們如果要定義一個函數用於返回個人名字信息,而名字信息因爲包含多個部分——姓氏、名字、中間名和別名,在不支持多返回值的語言中我們有以下兩種做法:要麼專門定義一個結構體用於返回,比如:

struct name
{
    char first_name[20];
    char middle_name[20];
    char last_name[20];
    char nick_name[48];
};
// 函數原型
extern name get_name();
// 函數調用
name n = get_name();
或者以傳出參數的方式返回多個結果:
// 函數原型
extern void get_name(
/*out*/char* first_name,
/*out*/char* middle_name,
/*out*/char* last_name,
/*out*/char* nick_name);
// 先分配內存
char first_name[20];
char middle_name[20];
char last_name[20];
char nick_name[48];
// 函數調用
get_name(first_name, middle_name, last_name, nick_name);

  Go 語言革命性地在靜態開發語言陣營中率先提供了多返回值功能。這個特性讓開發者可以從原來用各種比較彆扭的方式返回多個值的痛苦中解脫出來,既不用再區分參數列表中哪幾個用於輸入,哪幾個用於輸出,也不用再只爲了返回多個值而專門定義一個數據結構。

  在 Go 語言中,上述的例子可以修改爲以下的樣子:

func getName()(firstName, middleName, lastName, nickName string){
    return "May", "M", "Chen", "Babe"
}

  因爲返回值都已經有名字,因此各個返回值也可以用如下方式來在不同的位置進行賦值,從而提供了極大的靈活性:

func getName()(firstName, middleName, lastName, nickName string){
    firstName = "May"
    middleName = "M"
    lastName = "Chen"
    nickName = "Babe"
    return
}

  並不是每一個返回值都必須賦值,沒有被明確賦值的返回值將保持默認的空值。而函數的調用相比 C/C++ 語言要簡化很多:

fn, mn, ln, nn := getName()

  如果開發者只對該函數其中的某幾個返回值感興趣的話,也可以直接用下劃線作爲佔位符來忽略其他不關心的返回值。下面的調用表示調用者只希望接收 lastName 的值,這樣可以避免聲明完全沒用的變量:

_, _, lastName, _ := getName()

錯誤處理

  Go 語言引入了3個關鍵字用於標準的錯誤處理流程,這3個關鍵字分別爲 deferpanicrecover。整體上而言與 C++ 和 Java 等語言中的異常捕獲機制相比,Go 語言的錯誤處理機制可以大量減少代碼量,讓開發者也無需僅僅爲了程序安全性而添加大量一層套一層的 try-catch 語句。這對於代碼的閱讀者和維護者來說也是一件很好的事情,因爲可以避免在層層的代碼嵌套中定位業務代碼。

匿名函數和閉包

  在 Go 語言中,所有的函數也是值類型,可以作爲參數傳遞。Go 語言支持常規的匿名函數閉包,比如下列代碼就定義了一個名爲 f 的匿名函數,開發者可以隨意對該匿名函數變量進行傳遞和調用:

f := func(x, y int) int {
    return x + y
}

類型和接口

  Go 語言的類型定義非常接近於 C 語言中的結構(struct),甚至直接沿用了 struct 關鍵字。相比而言,Go 語言並沒有直接沿襲 C++ 和 Java 的傳統去設計一個超級複雜的類型系統,不支持繼承和重載,而只是支持了最基本的類型組合功能。

  巧妙的是,雖然看起來支持的功能過於簡潔,細用起來你卻會發現,C++ 和 Java 使用那些複雜的類型系統實現的功能在Go語言中並不會出現無法表現的情況,這反而讓人反思其他語言中引入這些複雜概念的必要性。

  Go 語言也不是簡單的對面向對象開發語言做減法,它還引入了一個無比強大的非侵入式接口的概念,讓開發者從以往對 C++ 和 Java 開發中的接口管理問題中解脫出來。在C++中,我們通常會這樣來確定接口和類型的關係:

// 抽象接口
interface IFly
{
    virtual void Fly()=0;
};
// 實現類
class Bird : public IFly
{
public:
    Bird(){}
    virtual ~Bird(){}
public:
    void Fly()
        {
        // 以鳥的方式飛行
        }
    };

    void main()
    {
        IFly* pFly = new Bird();
        pFly->Fly();
        delete pFly;
    }

  顯然,在實現一個接口之前必須先定義該接口,並且將類型和接口緊密綁定,即接口的修改會影響到所有實現了該接口的類型,而 Go 語言的接口體系則避免了這類問題:

type Bird struct {
    ...
}
func (b *Bird) Fly() {
    // 以鳥的方式飛行
}

  我們在實現 Bird 類型時完全沒有任何 IFly 的信息。我們可以在另外一個地方定義這個 IFly 接口:

type IFly interface {
    Fly()
}

  這兩者目前看起來完全沒有關係,現在看看我們如何使用它們:

func main() {
    var fly IFly = new(Bird)
    fly.Fly()
}

  可以看出,雖然 Bird 類型實現的時候,沒有聲明與接口 IFly 的關係,但接口和類型可以直接轉換,甚至接口的定義都不用在類型定義之前,這種比較鬆散的對應關係可以大幅降低因爲接口調整而導致的大量代碼調整工作。

併發編程

  Go 語言引入了 goroutine 概念,它使得併發編程變得非常簡單。通過使用 goroutine 而不是裸用操作系統的併發機制,以及使用通信來共享內存而不是使用共享內存來通信,Go 語言讓併發編程變得更加輕盈和安全。

  通過在函數調用前使用關鍵字 go,我們即可讓該函數以 goroutine 方式執行。goroutine 是一種比線程更加輕盈、更省資源的協程。Go 語言通過系統的線程來多路派遣這些函數的執行,使得每個用 go 關鍵字執行的函數可以運行成爲一個單位協程。當一個協程阻塞的時候,調度器就會自動把其他協程安排到另外的線程中去執行,從而實現了程序無等待並行化運行。而且調度的開銷非常小,一顆 CPU 調度的規模不下於每秒百萬次,這使得我們能夠創建大量的 goroutine ,從而可以很輕鬆地編寫高併發程序,達到我們想要的目的。

  Go 語言實現了 CSP (通信順序進程,Communicating Sequential Process)模型來作爲 goroutine 間的推薦通信方式。在 CSP 模型中,一個併發系統由若干並行運行的順序進程組成,每個進程不能對其他進程的變量賦值。進程之間只能通過一對通信原語實現協作。Go 語言用 channel (通道)這個概念來輕巧地實現了 CSP 模型。channel 的使用方式比較接近 Unix 系統中的管道(pipe)概念,可以方便地進行跨 goroutine 的通信。

  另外,由於一個進程內創建的所有 goroutine 運行在同一個內存地址空間中,因此如果不同的 goroutine 不得不去訪問共享的內存變量,訪問前應該先獲取相應的讀寫鎖。Go 語言標準庫中的 sync 包提供了完備的讀寫鎖功能。

  下面我們用一個簡單的例子來演示 goroutine 和 channel 的使用方式。這是一個並行計算的例子,由兩個 goroutine 進行並行的累加計算,待這兩個計算過程都完成後打印計算結果,具體如代碼清單所示。

package main

import "fmt"

func sum(values []int, resultChan chan int) {
    sum := 0
    for _, value := range values {
        sum += value
    }
    resultChan <- sum // 將計算結果發送到channel中
}

func main() {
    values := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    resultChan := make(chan int, 2)
    go sum(values[:len(values)/2], resultChan)
    go sum(values[len(values)/2:], resultChan)
    sum1, sum2 := <-resultChan, <-resultChan // 接收結果

    fmt.Println("Result:", sum1, sum2, sum1+sum2)
}

反射

  反射(reflection)是在Java語言出現後迅速流行起來的一種概念。通過反射,你可以獲取對象類型的詳細信息,並可動態操作對象。反射是把雙刃劍,功能強大但代碼可讀性並不理想。若非必要,並不推薦使用反射。

  Go 語言的反射實現了反射的大部分功能,但沒有像 Java 語言那樣內置類型工廠,故而無法做到像 Java 那樣通過類型字符串創建對象實例。在 Java 中,你可以讀取配置並根據類型名稱創建對應的類型,這是一種常見的編程手法,但在 Go 語言中這並不被推薦。

  反射最常見的使用場景是做對象的序列化(serialization,有時候也叫 Marshal & Unmarshal)。例如,Go 語言標準庫的 encoding/jsonencoding/xmlencoding/gobencoding/binary 等包就大量依賴於反射功能來實現。

  這裏先舉一個小例子,可以利用反射功能列出某個類型中所有成員變量的值,如代碼清單所示。

package main

import (
    "fmt"
    "reflect"
)

type Bird struct {
    Name string
    LifeExpectance int
}

func (b *Bird) Fly() {
    fmt.Println("I am flying...")
}

func main() {
    sparrow := &Bird{"Sparrow", 3}
    s := reflect.ValueOf(sparrow).Elem()
    typeOfT := s.Type()
    for i := 0; i < s.NumField(); i++ {
        f := s.Field(i)
        fmt.Printf("%d: %s %s = %v\n", i, typeOfT.Field(i).Name, f.Type(), f.Interface())
    }
}

  該程序的輸出結果爲:

0: Name string = Sparrow
1: LifeExpectance int = 3

語言交互性

  由於 Go 語言與 C 語言之間的天生聯繫,Go 語言的設計者們自然不會忽略如何重用現有 C 模塊的這個問題,這個功能直接被命名爲 CgoCgo 既是語言特性,同時也是一個工具的名稱

  在 Go 代碼中,可以按 Cgo 的特定語法混合編寫 C 語言代碼,然後 Cgo 工具可以將這些混合的 C 代碼提取並生成對於 C 功能的調用包裝代碼。開發者基本上可以完全忽略這個 Go 語言和 C 語言的邊界是如何跨越的。

  與 Java 中的 JNI 不同,Cgo 的用法非常簡單,比如下面代碼就可以實現在 Go 中調用 C 語言標準庫的 puts 函數。

package main

/*
#include <stdio.h>
*/
import "C"
import "unsafe"

func main() {
    cstr := C.CString("Hello, world")
    C.puts(cstr)
    C.free(unsafe.Pointer(cstr))
}

  參考:《Go語言編程》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章